diff --git a/docs/Loki.md b/docs/Loki.md index 866eb0b..16864b3 100644 --- a/docs/Loki.md +++ b/docs/Loki.md @@ -222,6 +222,34 @@ async series>( ): Promise ``` +### 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]; + +export interface LokiIngestLogs { + stream: Record; + values: (LogEntry | LogEntryWithMetadata)[]; +} +``` + +> [!IMPORTANT] +> The unixEpoch must be in **nanoseconds** + ## Pattern usage **queryRange** and **queryRangeStream** APIs allow the usage of pattern. diff --git a/src/class/Loki.class.ts b/src/class/Loki.class.ts index d7bf176..b759c25 100644 --- a/src/class/Loki.class.ts +++ b/src/class/Loki.class.ts @@ -19,7 +19,8 @@ import { RawQueryRangeResponse, QueryRangeLogsResponse, QueryRangeStreamResponse, - QueryRangeMatrixResponse + QueryRangeMatrixResponse, + LokiIngestLogs } from "../types.js"; import { ApiCredential } from "./ApiCredential.class.js"; @@ -235,4 +236,17 @@ export class Loki { return listSeries.status === "success" ? listSeries.data : []; } + + async push(logs: LokiIngestLogs[]): Promise { + 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" + } + }); + } } diff --git a/src/types.ts b/src/types.ts index d9a65a9..5a812a6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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]; + +export interface LokiIngestLogs { + stream: Record; + values: (LogEntry | LogEntryWithMetadata)[]; +} diff --git a/test/Loki.spec.ts b/test/Loki.spec.ts index bf298e3..1313a82 100644 --- a/test/Loki.spec.ts +++ b/test/Loki.spec.ts @@ -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"; @@ -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(