Skip to content

Commit eef7a0d

Browse files
authored
Feat/grafana tools (#21)
This pull request introduces new clients for interacting with Grafana and Loki, and includes corresponding test files for each client. The changes add functionality for fetching dashboards and logs, as well as querying specific data from these services.
1 parent bce2273 commit eef7a0d

File tree

4 files changed

+194
-0
lines changed

4 files changed

+194
-0
lines changed

src/grafana/grafanaclient.spec.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { GrafanaClient } from './grafanaclient';
2+
import 'dotenv/config';
3+
4+
const grafanaApiKey = process.env.GRAFANA_API_KEY!;
5+
const grafanaUrl = process.env.GRAFANA_INSTANCE_URL!;
6+
const isGithubActions = process.env.GITHUB_ACTIONS === 'true';
7+
const maybe = !isGithubActions ? describe : describe.skip;
8+
9+
maybe('GrafanaClient', () => {
10+
let grafanaClient: GrafanaClient;
11+
12+
beforeAll(() => {
13+
grafanaClient = new GrafanaClient(grafanaUrl, grafanaApiKey);
14+
});
15+
16+
it('should get dashboards', async () => {
17+
const dashboards = await grafanaClient.getDashboards();
18+
expect(dashboards).toBeDefined();
19+
20+
expect(Array.isArray(dashboards)).toBe(true);
21+
22+
const db = 'Runners Overview'
23+
const dashboard = await grafanaClient.getDashboardUrlByName(db);
24+
expect(dashboard).toBeDefined();
25+
});
26+
27+
});

src/grafana/grafanaclient.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export class GrafanaClient {
2+
private readonly grafanaUrl: string;
3+
private readonly grafanaApiKey: string;
4+
5+
constructor(grafanaUrl: string, grafanaApiKey: string) {
6+
this.grafanaUrl = grafanaUrl;
7+
this.grafanaApiKey = grafanaApiKey;
8+
}
9+
10+
async getDashboardUrlByName(dashboardName: string): Promise<string> {
11+
const dashboards = await this.getDashboards();
12+
const runners = dashboards.filter(d=>d.type==='dash-db' ).filter(d=>d.title.toLowerCase().includes(dashboardName.toLowerCase()))[0]
13+
return runners.url
14+
}
15+
16+
async getDashboards(): Promise<any[]> {
17+
const url = `${this.grafanaUrl}/api/search`;
18+
const response = await fetch(url, {
19+
method: 'GET',
20+
headers: {
21+
'Content-Type': 'application/json',
22+
'Authorization': `Bearer ${this.grafanaApiKey}`
23+
}
24+
});
25+
26+
if (!response.ok) {
27+
throw new Error(`Error fetching dashboards: ${response.statusText}`);
28+
}
29+
30+
return response.json();
31+
}
32+
}

src/grafana/lokiclient.spec.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { LokiClient } from './lokiclient';
2+
import 'dotenv/config';
3+
const lokiApiKey = process.env.LOKI_API_KEY!;
4+
const user = process.env.LOKI_USER!;
5+
const lokiUrl = process.env.LOKI_URL!;
6+
const isGithubActions = process.env.GITHUB_ACTIONS === 'true';
7+
const maybe = !isGithubActions ? describe : describe.skip;
8+
jest.setTimeout(30000);
9+
maybe('LokiClient', () => {
10+
let lokiClient: LokiClient;
11+
12+
beforeAll(() => {
13+
lokiClient = new LokiClient(lokiUrl, lokiApiKey, user, "staging");
14+
});
15+
16+
it ('can count logs by level for a service', async () => {
17+
const service = "checkly-api";
18+
const rangeMinutes = 60*12;
19+
const data = await lokiClient.getLogCountByLevel(service, rangeMinutes);
20+
expect(data).toBeDefined();
21+
console.log(JSON.stringify(data.data.result));
22+
expect(data).toHaveProperty('data');
23+
//console.log(JSON.stringify(data.data.result[0].values));
24+
})
25+
26+
it('should get available services', async () => {
27+
const services = await lokiClient.getAllValuesForLabel("app");
28+
expect(services).toBeDefined();
29+
expect(services.length).toBeGreaterThan(0);
30+
//console.log(services);
31+
});
32+
33+
it('should run a query and return results', async () => {
34+
const services = lokiClient.getAllValuesForLabel("app");
35+
const data = await lokiClient.getErrorsForService(services[1], 10);
36+
expect(data).toBeDefined();
37+
expect(data).toHaveProperty('data');
38+
//console.log(JSON.stringify(data.data.result[0].values));
39+
});
40+
});
41+

src/grafana/lokiclient.ts

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
export class LokiClient {
2+
private readonly lokiUrl: string;
3+
private readonly lokiApiKey: string;
4+
private readonly environment: string;
5+
user: string;
6+
7+
constructor(lokiUrl: string, lokiApiKey: string, user: string, environment: string) {
8+
this.lokiUrl = lokiUrl;
9+
this.lokiApiKey = lokiApiKey;
10+
this.environment = environment;
11+
this.user = user;
12+
}
13+
14+
queryError(service: string): string {
15+
return `{app="${service}", env="${this.environment}"} |= "error"`;
16+
}
17+
18+
async getLogCountByLevel(app: string, rangeMinutes: number): Promise<any> {
19+
const query = `sum by (detected_level) (count_over_time({app="${app}", env="${this.environment}"}[5m]))`;
20+
const end = new Date();
21+
const start = new Date(end.getTime() - rangeMinutes * 60 * 1000);
22+
const data = await this.queryLoki(query, start.toISOString(), end.toISOString());
23+
return data;
24+
}
25+
26+
async getAllEnvironments(): Promise<string[]> {
27+
return this.getAllValuesForLabel('env');
28+
}
29+
30+
31+
async getAllApps(): Promise<string[]> {
32+
return this.getAllValuesForLabel('app');
33+
}
34+
35+
/**
36+
* This function gets all available values for a label in Loki.
37+
* @returns
38+
*/
39+
async getAllValuesForLabel(label: string): Promise<string[]> {
40+
const url = new URL(`${this.lokiUrl}/loki/api/v1/label/${label}/values`);
41+
const authHeader = 'Basic ' + btoa(`${this.user}:${this.lokiApiKey}`);
42+
43+
const response = await fetch(url.toString(), {
44+
method: 'GET',
45+
headers: {
46+
'Content-Type': 'application/json',
47+
'Authorization': authHeader
48+
}
49+
});
50+
51+
if (!response.ok) {
52+
throw new Error(`Error fetching available services: ${response.statusText}`);
53+
}
54+
55+
const data = await response.json();
56+
return data.data; // Assuming the response structure is { "status": "success", "data": ["app1", "app2", ...] }
57+
}
58+
59+
async getErrorsForService(service: string, rangeMinutes: number) {
60+
// Get the current time and subtract "rangeMinutes" minutes
61+
const end = new Date();
62+
const start = new Date(end.getTime() - rangeMinutes * 60 * 1000);
63+
64+
// Convert to ISO string format
65+
const startISOString = start.toISOString();
66+
const endISOString = end.toISOString();
67+
const query = this.queryError(service);
68+
return this.queryLoki(query, startISOString, endISOString);
69+
}
70+
async queryLoki(query: string, start: string, end: string): Promise<any> {
71+
const url = new URL(`${this.lokiUrl}/loki/api/v1/query_range`);
72+
url.searchParams.append('query', query);
73+
url.searchParams.append('start', start);
74+
url.searchParams.append('end', end);
75+
const authHeader = 'Basic ' + btoa(`${this.user}:${this.lokiApiKey}`);
76+
77+
const response = await fetch(url.toString(), {
78+
method: 'GET',
79+
headers: {
80+
'Content-Type': 'application/json',
81+
Authorization: authHeader,
82+
},
83+
});
84+
85+
if (!response.ok) {
86+
const errorText = await response.text();
87+
throw new Error(
88+
`Error querying Loki: ${response.status} ${response.statusText} - ${errorText}`,
89+
);
90+
}
91+
//https://grafana.com/docs/loki/latest/reference/loki-http-api/#query-logs-within-a-range-of-time
92+
return response.json();
93+
}
94+
}

0 commit comments

Comments
 (0)