Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(loki): add push api #210

Merged
merged 1 commit into from
Dec 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/Loki.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,34 @@ async series<T = Record<string, string>>(
): Promise<T[]>
```

### push(logs: LokiIngestLogs): Promise< void >
Send log entries to Loki.

```ts
const logs: LokiIngestLogs[] = [
{
stream: { app: "foo" },
values: [["173532887432100000", "hello world"]]
}
];
await api.Loki.push(logs);
```

The `LokiIngestLogs` type is defined as follows:

```ts
export type LogEntry = [unixEpoch: string, log: string];
export type LogEntryWithMetadata = [unixEpoch: string, log: string, metadata: Record<string, string>];

export interface LokiIngestLogs {
stream: Record<string, string | number | boolean>;
values: (LogEntry | LogEntryWithMetadata)[];
}
```

> [!IMPORTANT]
> The unixEpoch must be in **nanoseconds**

## Pattern usage

**queryRange** and **queryRangeStream** APIs allow the usage of pattern.
Expand Down
16 changes: 15 additions & 1 deletion src/class/Loki.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
RawQueryRangeResponse,
QueryRangeLogsResponse,
QueryRangeStreamResponse,
QueryRangeMatrixResponse
QueryRangeMatrixResponse,
LokiIngestLogs
} from "../types.js";
import { ApiCredential } from "./ApiCredential.class.js";

Expand Down Expand Up @@ -235,4 +236,17 @@ export class Loki {

return listSeries.status === "success" ? listSeries.data : [];
}

async push(logs: LokiIngestLogs[]): Promise<void> {
const uri = new URL("loki/api/v1/push", this.remoteApiURL);
const { headers } = this.credential.httpOptions;

await httpie.post(uri, {
body: { streams: logs },
headers: {
...headers,
"Content-Type": "application/json"
}
});
}
}
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,11 @@ export interface QueryRangeMatrixResponse {
}

export type { TimeRange };

export type LogEntry = [unixEpoch: string, log: string];
export type LogEntryWithMetadata = [unixEpoch: string, log: string, metadata: Record<string, string>];

export interface LokiIngestLogs {
stream: Record<string, string | number | boolean>;
values: (LogEntry | LogEntryWithMetadata)[];
}
35 changes: 35 additions & 0 deletions test/Loki.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { MockAgent, setGlobalDispatcher, getGlobalDispatcher } from "@myunisoft/
// Import Internal Dependencies
import {
GrafanaApi,
LogEntry,
LokiIngestLogs,
LokiStandardBaseResponse
} from "../src/index.js";
import { mockMatrixResponse, mockStreamResponse } from "./utils/logs.factory.js";
Expand Down Expand Up @@ -383,6 +385,39 @@ describe("GrafanaApi.Loki", () => {
assert.strictEqual(result.length, 0);
});
});

describe("push", () => {
const agentPoolInterceptor = kMockAgent.get(kDummyURL);

before(() => {
process.env.GRAFANA_API_TOKEN = "";
setGlobalDispatcher(kMockAgent);
});

after(() => {
delete process.env.GRAFANA_API_TOKEN;
setGlobalDispatcher(kDefaultDispatcher);
});

it("should call POST /loki/api/v1/push with the provided logs", async() => {
const dummyLogs: LokiIngestLogs[] = [
{
stream: { app: "foo" },
values: [["173532887432100000", "hello world"]]
}
];

const agentPoolInterceptor = kMockAgent.get(kDummyURL);
agentPoolInterceptor
.intercept({
path: (path) => path.includes("loki/api/v1/push"),
method: "POST"
}).reply(204);

const sdk = new GrafanaApi({ remoteApiURL: kDummyURL });
await assert.doesNotReject(async() => await sdk.Loki.push(dummyLogs));
});
});
});

function mockLabelResponse<T>(
Expand Down
Loading