diff --git a/README.md b/README.md index 5e5dd2f..c9d49ee 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ export interface LokiQueryOptions { ## API +### queryRange + You can provide a custom parser to queryRange (by default it inject a NoopParser doing nothing). ```ts @@ -107,6 +109,82 @@ for (const data of logs) { The parser will automatically escape and generate a RegExp with capture group (with a syntax similar to Loki pattern). +### labels + +```ts +const labels = await api.labels(); +``` + +`labels(options = {})` retrieves the list of known labels within a given time span. Loki may use a larger time span than the one specified. It accepts the following options: + +```ts +interface LokiLabelsOptions { + /** + * The start time for the query as + * - a nanosecond Unix epoch. + * - a duration (i.e "2h") + * + * Default to 6 hours ago. + */ + start?: number | string; + /** + * The end time for the query as + * - a nanosecond Unix epoch. + * - a duration (i.e "2h") + * + * Default to now + */ + end?: number | string; + /** + * A duration used to calculate start relative to end. If end is in the future, start is calculated as this duration before now. + * + * Any value specified for start supersedes this parameter. + */ + since?: string; +} +``` + +### labelValues + +```ts +const appLabelValues = await api.labelValues("app"); +``` + +`labelValues(label, options = {})` retrieves the list of known values for a given label within a given time span. Loki may use a larger time span than the one specified. + +```ts +interface LokiLabelValueOptions { + /** + * The start time for the query as + * - a nanosecond Unix epoch. + * - a duration (i.e "2h") + * + * Default to 6 hours ago. + */ + start?: number | string; + /** + * The end time for the query as + * - a nanosecond Unix epoch. + * - a duration (i.e "2h") + * + * Default to now + */ + end?: number | string; + /** + * A duration used to calculate start relative to end. If end is in the future, start is calculated as this duration before now. + * + * Any value specified for start supersedes this parameter. + */ + since?: string; + /** + * A set of log stream selector that selects the streams to match and return label values for . + * + * Example: {"app": "myapp", "environment": "dev"} + */ + query?: string; +} +``` + ## Contributors ✨ diff --git a/src/class/GrafanaLoki.class.ts b/src/class/GrafanaLoki.class.ts index 4f8bfad..95a9049 100644 --- a/src/class/GrafanaLoki.class.ts +++ b/src/class/GrafanaLoki.class.ts @@ -3,7 +3,7 @@ import * as httpie from "@myunisoft/httpie"; // Import Internal Dependencies import * as utils from "../utils.js"; -import { QueryRangeResponse } from "../types.js"; +import { LabelResponse, LabelValuesResponse, QueryRangeResponse } from "../types.js"; import { NoopLogParser, LogParserLike } from "./LogParser.class.js"; export interface LokiQueryOptions { @@ -28,6 +28,40 @@ export interface GrafanaLokiConstructorOptions { remoteApiURL: string | URL; } +export interface LokiLabelsOptions { + /** + * The start time for the query as + * - a nanosecond Unix epoch. + * - a duration (i.e "2h") + * + * Default to 6 hours ago. + */ + start?: number | string; + /** + * The end time for the query as + * - a nanosecond Unix epoch. + * - a duration (i.e "2h") + * + * Default to now + */ + end?: number | string; + /** + * A duration used to calculate start relative to end. If end is in the future, start is calculated as this duration before now. + * + * Any value specified for start supersedes this parameter. + */ + since?: string; +} + +export interface LokiLabelValuesOptions extends LokiLabelsOptions { + /** + * A set of log stream selector that selects the streams to match and return label values for . + * + * Example: {"app": "myapp", "environment": "dev"} + */ + query?: string; +} + export class GrafanaLoki { private apiToken: string; private remoteApiURL: URL; @@ -84,4 +118,45 @@ export class GrafanaLoki { utils.inlineLogs(data) ); } + + async labels(options: LokiLabelsOptions = {}): Promise { + const uri = new URL("loki/api/v1/labels", this.remoteApiURL); + if (options.start) { + uri.searchParams.set("start", utils.durationToUnixTimestamp(options.start)); + } + if (options.end) { + uri.searchParams.set("end", utils.durationToUnixTimestamp(options.end)); + } + if (options.since) { + uri.searchParams.set("since", options.since); + } + + const { data: labels } = await httpie.get( + uri, this.httpOptions + ); + + return labels.data; + } + + async labelValues(label: string, options: LokiLabelValuesOptions = {}): Promise { + const uri = new URL(`loki/api/v1/label/${label}/values`, this.remoteApiURL); + if (options.start) { + uri.searchParams.set("start", utils.durationToUnixTimestamp(options.start)); + } + if (options.end) { + uri.searchParams.set("end", utils.durationToUnixTimestamp(options.end)); + } + if (options.since) { + uri.searchParams.set("since", options.since); + } + if (options.query) { + uri.searchParams.set("query", options.query); + } + + const { data: labelValues } = await httpie.get( + uri, this.httpOptions + ); + + return labelValues.data; + } } diff --git a/src/types.ts b/src/types.ts index a081988..7592d17 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,3 +42,13 @@ export interface QueryRangeResponse { }; } } + +export interface LabelResponse { + status: "success"; + data: string[]; +} + +export interface LabelValuesResponse { + status: "success"; + data: string[]; +} diff --git a/test/GrafanaLoki.spec.ts b/test/GrafanaLoki.spec.ts index ffe296f..e9d14ca 100644 --- a/test/GrafanaLoki.spec.ts +++ b/test/GrafanaLoki.spec.ts @@ -9,7 +9,7 @@ import { MockAgent, setGlobalDispatcher, getGlobalDispatcher } from "@myunisoft/ // Import Internal Dependencies import { GrafanaLoki } from "../src/class/GrafanaLoki.class.js"; import { LogParser } from "../src/class/LogParser.class.js"; -import { QueryRangeResponse } from "../src/types.js"; +import { LabelResponse, QueryRangeResponse } from "../src/types.js"; // CONSTANTS const kDummyURL = "https://nodejs.org"; @@ -115,6 +115,59 @@ describe("GrafanaLoki", () => { ); }); }); + + describe("labels", () => { + const agentPoolInterceptor = kMockAgent.get(kDummyURL); + + before(() => { + process.env.GRAFANA_API_TOKEN = ""; + setGlobalDispatcher(kMockAgent); + }); + + after(() => { + delete process.env.GRAFANA_API_TOKEN; + setGlobalDispatcher(kDefaultDispatcher); + }); + + it("should return labels", async() => { + const expectedLabels = ["app", "env"]; + + agentPoolInterceptor + .intercept({ + path: (path) => path.includes("loki/api/v1/labels") + }) + .reply(200, mockLabelResponse(expectedLabels), { + headers: { "Content-Type": "application/json" } + }); + + const sdk = new GrafanaLoki({ remoteApiURL: kDummyURL }); + + const result = await sdk.labels(); + assert.deepEqual( + result, + expectedLabels + ); + }); + + it("should return label values", async() => { + const expectedLabelValues = ["prod", "preprod"]; + agentPoolInterceptor + .intercept({ + path: (path) => path.includes("loki/api/v1/label/env/values") + }) + .reply(200, mockLabelResponse(expectedLabelValues), { + headers: { "Content-Type": "application/json" } + }); + + const sdk = new GrafanaLoki({ remoteApiURL: kDummyURL }); + + const result = await sdk.labelValues("env"); + assert.deepEqual( + result, + expectedLabelValues + ); + }); + }); }); type DeepPartial = T extends object ? { @@ -137,6 +190,13 @@ function mockStreamResponse(logs: string[]): DeepPartial { }; } +function mockLabelResponse(response: string[]): LabelResponse { + return { + status: "success", + data: response + }; +} + function getNanoSecTime() { const hrTime = process.hrtime();