From f2996d95f532deed6ce13478791b6c2004493e24 Mon Sep 17 00:00:00 2001 From: Daniel Paulus Date: Fri, 22 Nov 2024 15:04:36 +0100 Subject: [PATCH 1/2] lokiclient --- src/grafana/lokiclient.ts | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/grafana/lokiclient.ts diff --git a/src/grafana/lokiclient.ts b/src/grafana/lokiclient.ts new file mode 100644 index 0000000..85585e3 --- /dev/null +++ b/src/grafana/lokiclient.ts @@ -0,0 +1,56 @@ +export class LokiClient { + private readonly lokiUrl: string; + private readonly lokiApiKey: string; + private readonly environment: string; + user: string; + + //'sum by (level) (count_over_time({app=""}[1m]))'; + constructor(lokiUrl: string, lokiApiKey: string, user: string, environment: string) { + this.lokiUrl = lokiUrl; + this.lokiApiKey = lokiApiKey; + this.environment = environment; + this.user = user; + } + + queryError(service: string): string { + return `{app="${service}", env="${this.environment}"} |= "error"`; + } + getAvailableServices(): string[] { + return ['']; + } + async getErrorsForService(service: string,env: string, rangeMinutes: number) { + // Get the current time and subtract "rangeMinutes" minutes + const end = new Date(); + const start = new Date(end.getTime() - rangeMinutes * 60 * 1000); + + // Convert to ISO string format + const startISOString = start.toISOString(); + const endISOString = end.toISOString(); + const query = this.queryError(service); + return this.queryLoki(query, startISOString, endISOString); + } + async queryLoki(query: string, start: string, end: string): Promise { + const url = new URL(`${this.lokiUrl}/loki/api/v1/query_range`); + url.searchParams.append('query', query); + url.searchParams.append('start', start); + url.searchParams.append('end', end); + const authHeader = 'Basic ' + btoa(`${this.user}:${this.lokiApiKey}`); + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: authHeader, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Error querying Loki: ${response.status} ${response.statusText} - ${errorText}`, + ); + } + //https://grafana.com/docs/loki/latest/reference/loki-http-api/#query-logs-within-a-range-of-time + return response.json(); + } + } \ No newline at end of file From 4d11f98dafd6c31303726049446ae12dbecd5ba3 Mon Sep 17 00:00:00 2001 From: Daniel Paulus Date: Fri, 22 Nov 2024 16:05:46 +0100 Subject: [PATCH 2/2] basic loki client --- src/grafana/lokiclient.spec.ts | 41 ++++++++++++++++++++++++++++++ src/grafana/lokiclient.ts | 46 +++++++++++++++++++++++++++++++--- 2 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 src/grafana/lokiclient.spec.ts diff --git a/src/grafana/lokiclient.spec.ts b/src/grafana/lokiclient.spec.ts new file mode 100644 index 0000000..aad3fa4 --- /dev/null +++ b/src/grafana/lokiclient.spec.ts @@ -0,0 +1,41 @@ +import { LokiClient } from './lokiclient'; +import 'dotenv/config'; +const lokiApiKey = process.env.LOKI_API_KEY!; +const user = process.env.LOKI_USER!; +const lokiUrl = process.env.LOKI_URL!; +const isGithubActions = process.env.GITHUB_ACTIONS === 'true'; +const maybe = !isGithubActions ? describe : describe.skip; +jest.setTimeout(30000); +maybe('LokiClient', () => { + let lokiClient: LokiClient; + + beforeAll(() => { + lokiClient = new LokiClient(lokiUrl, lokiApiKey, user, "staging"); + }); + + it ('can count logs by level for a service', async () => { + const service = "checkly-api"; + const rangeMinutes = 60*12; + const data = await lokiClient.getLogCountByLevel(service, rangeMinutes); + expect(data).toBeDefined(); + console.log(JSON.stringify(data.data.result)); + expect(data).toHaveProperty('data'); + //console.log(JSON.stringify(data.data.result[0].values)); + }) + + it('should get available services', async () => { + const services = await lokiClient.getAllValuesForLabel("app"); + expect(services).toBeDefined(); + expect(services.length).toBeGreaterThan(0); + //console.log(services); + }); + + it('should run a query and return results', async () => { + const services = lokiClient.getAllValuesForLabel("app"); + const data = await lokiClient.getErrorsForService(services[1], 10); + expect(data).toBeDefined(); + expect(data).toHaveProperty('data'); + //console.log(JSON.stringify(data.data.result[0].values)); + }); +}); + \ No newline at end of file diff --git a/src/grafana/lokiclient.ts b/src/grafana/lokiclient.ts index 85585e3..a627077 100644 --- a/src/grafana/lokiclient.ts +++ b/src/grafana/lokiclient.ts @@ -4,7 +4,6 @@ export class LokiClient { private readonly environment: string; user: string; - //'sum by (level) (count_over_time({app=""}[1m]))'; constructor(lokiUrl: string, lokiApiKey: string, user: string, environment: string) { this.lokiUrl = lokiUrl; this.lokiApiKey = lokiApiKey; @@ -15,10 +14,49 @@ export class LokiClient { queryError(service: string): string { return `{app="${service}", env="${this.environment}"} |= "error"`; } - getAvailableServices(): string[] { - return ['']; + + async getLogCountByLevel(app: string, rangeMinutes: number): Promise { + const query = `sum by (detected_level) (count_over_time({app="${app}", env="${this.environment}"}[5m]))`; + const end = new Date(); + const start = new Date(end.getTime() - rangeMinutes * 60 * 1000); + const data = await this.queryLoki(query, start.toISOString(), end.toISOString()); + return data; } - async getErrorsForService(service: string,env: string, rangeMinutes: number) { + + async getAllEnvironments(): Promise { + return this.getAllValuesForLabel('env'); + } + + + async getAllApps(): Promise { + return this.getAllValuesForLabel('app'); + } + + /** + * This function gets all available values for a label in Loki. + * @returns + */ + async getAllValuesForLabel(label: string): Promise { + const url = new URL(`${this.lokiUrl}/loki/api/v1/label/${label}/values`); + const authHeader = 'Basic ' + btoa(`${this.user}:${this.lokiApiKey}`); + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': authHeader + } + }); + + if (!response.ok) { + throw new Error(`Error fetching available services: ${response.statusText}`); + } + + const data = await response.json(); + return data.data; // Assuming the response structure is { "status": "success", "data": ["app1", "app2", ...] } + } + + async getErrorsForService(service: string, rangeMinutes: number) { // Get the current time and subtract "rangeMinutes" minutes const end = new Date(); const start = new Date(end.getTime() - rangeMinutes * 60 * 1000);