Skip to content

Commit

Permalink
fix: connected grafana api with response-time-interceptor
Browse files Browse the repository at this point in the history
  • Loading branch information
DarrenDsouza7273 committed Jul 4, 2024
1 parent 74b2aa2 commit 1ce4457
Show file tree
Hide file tree
Showing 12 changed files with 219 additions and 869 deletions.
3 changes: 2 additions & 1 deletion packages/common/src/controllers/prometheus.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Controller, Get, Res } from '@nestjs/common';
import { FastifyReply } from 'fastify';
import { register } from 'prom-client';

@Controller()
export class PrometheusController {
@Get('metrics')
async metrics(@Res() response: FastifyReply) {
response.headers({ 'Content-Type': register.contentType });
response.header('Content-Type', register.contentType);
response.send(await register.metrics());
}
}
110 changes: 79 additions & 31 deletions packages/common/src/interceptors/response-time.interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,65 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { Histogram, exponentialBuckets } from 'prom-client';
import * as fs from 'fs';
import { generateBaseJSON, generateRow } from './utils';
import axios from 'axios';

import {
generateBaseJSON,
generateRow,
getDashboardByUID,
getDashboardJSON,
} from './utils';

@Injectable()
export class ResponseTimeInterceptor implements NestInterceptor {
private histogram: Histogram;
private logger: Logger;
private dashboardUid: string;
private grafanaBaseURL: string;
private apiToken: string;

constructor(histogramTitle: string, jsonPath: string) {
constructor(
histogramTitle: string,
grafanaBaseURL: string,
apiToken: string,
) {
const name = histogramTitle + '_response_time';
this.logger = new Logger(name + '_interceptor');
this.dashboardUid = null;
this.grafanaBaseURL = grafanaBaseURL;
this.apiToken = apiToken;
this.init(histogramTitle, grafanaBaseURL);
}

async init(histogramTitle: string, grafanaBaseURL: string) {
const name = histogramTitle + '_response_time';
this.histogram = new Histogram({
name: name,
help: 'Response time of APIs',
buckets: exponentialBuckets(1, 1.5, 30),
labelNames: ['statusCode', 'endpoint'],
});

// updating the grafana JSON with the row for this panel
console.log("apiToken", this.apiToken);
console.log("grafanaBaseURL", grafanaBaseURL);
try {
// check if the path exists or not?
if (!fs.existsSync(jsonPath)) {
fs.writeFileSync(jsonPath, JSON.stringify(generateBaseJSON()), 'utf8'); // create file if not exists
}
const dashboardJSONSearchResp = await getDashboardJSON(
this.apiToken,
histogramTitle,
grafanaBaseURL,
);
this.dashboardUid =
dashboardJSONSearchResp.length > 0
? dashboardJSONSearchResp[0]['uid']
: undefined;
let parsedContent: any;

const parsedContent = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
if (this.dashboardUid === null || !this.dashboardUid) {
parsedContent = generateBaseJSON(histogramTitle);

let isPresent = false;

// skip creating the row if it already exists -- prevents multiple panels/rows on app restarts
let isPresent = false;
parsedContent.panels.forEach((panel: any) => {
// TODO: Make this verbose and add types -- convert the grafana JSON to TS Types/interface
parsedContent.dashboard.panels.forEach((panel: any) => {
if (
panel.title.trim() ===
name
Expand All @@ -52,13 +72,43 @@ export class ResponseTimeInterceptor implements NestInterceptor {
}
});
if (isPresent) return;
parsedContent.dashboard.panels.push(generateRow(name));
} else {
parsedContent = await getDashboardByUID(this.dashboardUid, grafanaBaseURL, this.apiToken);


let isPresent = false;
parsedContent.panels.forEach((panel: any) => {
if (
panel.title.trim() ===
name
.split('_')
.map((str) => str.charAt(0).toUpperCase() + str.slice(1))
.join(' ')
.trim()
) {
isPresent = true;
}
});
if (isPresent) return;

parsedContent.panels.push(generateRow(name));
}


parsedContent.panels.push(generateRow(name));
// write back to file
fs.writeFileSync(jsonPath, JSON.stringify(parsedContent));

const FINAL_JSON = parsedContent;
await axios.post(`${grafanaBaseURL}/api/dashboards/db`, FINAL_JSON, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiToken}`
}
});

this.logger.log('Successfully added histogram to dashboard!');
} catch (err) {
this.logger.error('Error updating grafana JSON!', err);
console.error('Error updating grafana JSON!', err);
}
}

Expand All @@ -70,26 +120,24 @@ export class ResponseTimeInterceptor implements NestInterceptor {
const startTime = performance.now();
return next.handle().pipe(
tap(() => {
// handles when there is no error propagating from the services to the controller
const endTime = performance.now();
const responseTime = endTime - startTime;
const statusCode = response.statusCode;
const endpoint = request.url;
this.histogram.labels({ statusCode, endpoint }).observe(responseTime);
}),
catchError((err) => {
// handles when an exception is to be returned to the client
this.logger.error('error: ', err);
const endTime = performance.now();
const responseTime = endTime - startTime;
const endpoint = request.url;
this.histogram
.labels({ statusCode: err.status, endpoint })
.observe(responseTime);
this.histogram.labels({ statusCode: err.status, endpoint }).observe(responseTime);
return throwError(() => {
throw err;
});
}),
);
}
}



165 changes: 122 additions & 43 deletions packages/common/src/interceptors/utils.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,90 @@
export const generateBaseJSON = () => {
import axios from 'axios';

export const generateBaseJSON = (histogramTitle) => {
return {
annotations: {
list: [
{
builtIn: 1,
datasource: {
type: 'grafana',
uid: '-- Grafana --',
},
enable: true,
hide: true,
iconColor: 'rgba(0, 211, 255, 1)',
name: 'Annotations & Alerts',
type: 'dashboard',
meta: {
type: "db",
canSave: true,
canEdit: true,
canAdmin: true,
canStar: true,
canDelete: true,
slug: histogramTitle,
url: `/d/ec1b884b/${histogramTitle}`,
expires: "0001-01-01T00:00:00Z",
created: "2024-07-03T04:01:29Z",
updated: "2024-07-03T04:01:29Z",
updatedBy: "admin",
createdBy: "admin",
version: 1,
hasAcl: false,
isFolder: false,
folderId: 0,
folderUid: "",
folderTitle: "General",
folderUrl: "",
provisioned: false,
provisionedExternalId: "",
annotationsPermissions: {
dashboard: {
canAdd: true,
canEdit: true,
canDelete: true,
},
],
},
description: 'Dashboard for API response time visualisations\n',
editable: true,
fiscalYearStartMonth: 0,
graphTooltip: 0,
id: 5,
links: [],
liveNow: false,
panels: [],
refresh: '',
schemaVersion: 38,
tags: [],
templating: {
list: [],
organization: {
canAdd: true,
canEdit: true,
canDelete: true,
},
},
},
time: {
from: 'now-6h',
to: 'now',
dashboard: {
annotations: {
list: [
{
builtIn: 1,
datasource: {
type: 'grafana',
uid: '-- Grafana --',
},
enable: true,
hide: true,
iconColor: 'rgba(0, 211, 255, 1)',
name: 'Annotations & Alerts',
type: 'dashboard',
},
],
},
description: 'Dashboard for API response time visualisations\n',
editable: true,
fiscalYearStartMonth: 0,
graphTooltip: 0,
id: null,
links: [],
liveNow: false,
panels: [],
refresh: '',
schemaVersion: 38,
tags: [],
templating: {
list: [],
},
time: {
from: 'now-6h',
to: 'now',
},
timepicker: {},
timezone: '',
title: histogramTitle,
uid: null,
version: 1,
weekStart: '',
},
timepicker: {},
timezone: '',
title: 'Response Times',
uid: 'ec1b884b-753e-405f-8f4b-e0a5d97cf167',
version: 6,
weekStart: '',
};
};


export const generateRow = (name: string) => {
const textPanel = generateTextPanel(
name,
// `# ${name} Response Time Information\n`,
name,
);
const gauagePanel = generateGaugePanel(name);
const heatmap = generateHeatmap(name);
const successfulUnsuccessful = generateSuccessfulUnSuccessfulPanel(name);
Expand Down Expand Up @@ -570,3 +606,46 @@ export const generateAverageResponseTimePanel = (label: string) => {
type: 'timeseries',
};
};





export const getDashboardByUID = async (
uid: string,
grafanaBaseURL: string,
apiToken : string
) => {
try {
const resp = await axios.get(grafanaBaseURL + `/api/dashboards/uid/${uid}`, {
headers: {
'Authorization': `Bearer ${apiToken}`
}
});
return resp.data.dashboard;
} catch (err) {
console.error('error getting dashboard by uid: ', err);
}
return ;
};

export const getDashboardJSON = async (
token: string,
dashboardTitle: string,
grafanaBaseURL: string,
) => {
try {
const resp: any = await axios.get(grafanaBaseURL + '/api/search', {
headers: {
Authorization: `Bearer ${token}`,
},
});
return (resp.data as any[]).filter((item) => {
return item.title && item.title === dashboardTitle;
});
} catch (err) {
console.error('Error searching dashboards: ', err);
}
return;
};

This file was deleted.

1 change: 0 additions & 1 deletion packages/nestjs-monitor/monitor/response_times_base.json

This file was deleted.

3 changes: 3 additions & 0 deletions sample/02-monitoring/env-example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

GRAFANA_API_TOKEN="" #your grafana api token
GRAFANA_BASE_URL="http://localhost:7889"
1 change: 0 additions & 1 deletion sample/02-monitoring/monitor/response_times_base.json

This file was deleted.

6 changes: 1 addition & 5 deletions sample/02-monitoring/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { ResponseTimeInterceptor } from './response.interceptor';


@Controller()
@UseInterceptors(
new ResponseTimeInterceptor(
'app_controller_interceptor',
'http://localhost:7889',
),
)
export class AppController {
constructor(private readonly appService: AppService) {}
Expand Down
3 changes: 2 additions & 1 deletion sample/02-monitoring/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrometheusController, MonitoringModule } from '@samagra-x/stencil';
import { ConfigModule } from '@nestjs/config';

@Module({
imports: [MonitoringModule],
imports: [MonitoringModule,ConfigModule.forRoot()],
controllers: [AppController, PrometheusController],
providers: [AppService],
})
Expand Down
Loading

0 comments on commit 1ce4457

Please sign in to comment.