Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[93] fix: Connected Grafana Api to the response-time-interceptor #150

Open
wants to merge 24 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f4ec971
feat(wip): move response time interceptor to use grafana api
techsavvyash Mar 10, 2024
125e67d
feat(wip): interceptor changes w api
techsavvyash Mar 15, 2024
3b0e08e
fix: connected grafana api with response-time-interceptor
DarrenDsouza7273 Jul 4, 2024
3fa0a9c
Added Monitoring
DarrenDsouza7273 Jul 6, 2024
9154a6d
fix: fixed dashboard generation to row level generation
Savio629 Jul 25, 2024
eae1619
Merge branch 'dev' of https://github.com/DarrenDsouza7273/stencil int…
Savio629 Jul 25, 2024
f3bde9c
fix: removed extra repo
Savio629 Jul 25, 2024
03b87d2
fix: updated test
Savio629 Jul 27, 2024
9d0d4a1
fix: added grafana level test
Savio629 Jul 28, 2024
04dbe76
fix: fixed the failing ci
Savio629 Aug 10, 2024
b4560f4
fix: updated the naming convection
Savio629 Aug 18, 2024
10660ff
fix: updated test
Savio629 Aug 20, 2024
9913ebe
fix: updated test and increased the coverage
Savio629 Sep 22, 2024
e0f6699
Merge branch 'Response-time-grafana' of https://github.com/DarrenDsou…
Savio629 Sep 22, 2024
db9a51d
fix: updated suggested changes
Savio629 Sep 22, 2024
4b9e67a
fix: updated test
Savio629 Sep 24, 2024
e528e29
fix: updated ci test
Savio629 Sep 24, 2024
6e4dc55
fix: fixed ci test
Savio629 Sep 24, 2024
0a6a022
fix: updating the failing ci
Savio629 Sep 24, 2024
a51767e
fix: updated ci test
Savio629 Sep 24, 2024
f472c86
fix: updated test
Savio629 Sep 24, 2024
683deb5
fix: updated test
Savio629 Sep 24, 2024
0cb6522
fix: failing test
Savio629 Sep 28, 2024
abe17ae
fix: updated yarn.lock file
Savio629 Oct 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19,319 changes: 19,319 additions & 0 deletions packages/common/package-lock.json

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,17 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/axios": "^3.0.3",
"@nestjs/cache-manager": "^2.1.0",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.2",
"@nestjs/common": "^10.4.4",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-fastify": "^10.3.0",
"@temporalio/client": "^1.8.6",
"@temporalio/worker": "^1.8.6",
"@types/multer": "^1.4.11",
"axios": "^1.7.7",
"cache-manager": "^5.2.4",
"cache-manager-redis-store": "2",
"fastify": "^4.27.0",
Expand All @@ -50,7 +52,7 @@
"minio": "^7.1.3",
"multer": "^1.4.5-lts.1",
"nestjs-temporal": "^2.0.1",
"prom-client": "^15.1.2",
"prom-client": "^15.1.3",
"redis": "^4.6.10",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
Expand Down
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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static Analysis: Decorators are not valid here.

Decorators are only valid on class declarations, class expressions, and class methods. You can enable parameter decorators by setting the unsafeParameterDecoratorsEnabled option to true in your configuration file.

-  async metrics(@Res() response: FastifyReply) {
+  async metrics(response: FastifyReply) {
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async metrics(@Res() response: FastifyReply) {
async metrics(response: FastifyReply) {
Tools
Biome

[error] 8-8: Decorators are not valid here.

Decorators are only valid on class declarations, class expressions, and class methods.
You can enable parameter decorators by setting the unsafeParameterDecoratorsEnabled option to true in your configuration file.

(parse)

response.headers({ 'Content-Type': register.contentType });
response.header('Content-Type', register.contentType);
response.send(await register.metrics());
}
}
14 changes: 14 additions & 0 deletions packages/common/src/interceptors/mutex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export class Mutex {
private mutex: Promise<void> = Promise.resolve();

lock(): PromiseLike<() => void> {
let begin: (unlock: () => void) => void = (unlock) => {};

this.mutex = this.mutex.then(() => new Promise(begin));

return new Promise((res) => {
begin = res;
});
}
}

157 changes: 116 additions & 41 deletions packages/common/src/interceptors/response-time.interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,141 @@
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 { Mutex } from './mutex';

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

const mutex = new Mutex();
@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);
}

async init(histogramTitle: 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
const unlock = await mutex.lock();
let parsedContent: any;
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,
'Response_Times',
this.grafanaBaseURL,
);
if (dashboardJSONSearchResp && dashboardJSONSearchResp.length > 0) {
this.dashboardUid = dashboardJSONSearchResp[0]['uid'];
} else {
this.dashboardUid = undefined;
}


if (this.dashboardUid === undefined || !this.dashboardUid) {
parsedContent = generateBaseJSON();
if (parsedContent && parsedContent.dashboard) {

const parsedContent = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));

// 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
if (
panel.title.trim() ===
name
.split('_')
.map((str) => str.charAt(0).toUpperCase() + str.slice(1))
.join(' ')
.trim()
) {
isPresent = true;
if (!parsedContent.dashboard.panels) {
parsedContent.dashboard.panels = [];
}
});
if (isPresent) return;

parsedContent.panels.push(generateRow(name));
// write back to file
fs.writeFileSync(jsonPath, JSON.stringify(parsedContent));
if (!this.isPanelPresent(parsedContent.dashboard.panels, name)) {
parsedContent.dashboard.panels.push(generateRow(name));
}
}
const FINAL_JSON = parsedContent;
await axios.post(`${this.grafanaBaseURL}/api/dashboards/db`, FINAL_JSON, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiToken}`
}
});
} else {
parsedContent = await getDashboardByUID(this.dashboardUid, this.grafanaBaseURL, this.apiToken);

if (parsedContent && parsedContent.dashboard) {
if (!parsedContent.dashboard.panels) {
parsedContent.dashboard.panels = [];
}

if (!this.isPanelPresent(parsedContent.dashboard.panels, name)) {
parsedContent.dashboard.panels.push(generateRow(name));
}

await this.updateDashboard(parsedContent);
} else {
throw new Error('Invalid dashboard response from Grafana API');
}
}
this.logger.log('Successfully added histogram to dashboard!');
} catch (err) {
this.logger.error('Error updating grafana JSON!', err);
const updatedDashboard = parsedContent;
if (err.response && err.response.data && err.response.data.status === 'version-mismatch') {
this.logger.log('Dashboard version mismatch, retrying with latest version...');
updatedDashboard.dashboard.version++;
await axios.post(`${this.grafanaBaseURL}/api/dashboards/db`, updatedDashboard, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiToken}`
}
});
} else {
console.error('Error updating Grafana JSON!', err);
}
} finally {
unlock();
}
}
isPanelPresent(panels: any[], name: string): boolean {
const formattedName = name
.split('_')
.map((str) => str.charAt(0).toUpperCase() + str.slice(1))
.join(' ')
.trim();

return panels.some((panel) => panel && panel.title && panel.title.trim() === formattedName);
}

async updateDashboard(FINAL_JSON: any) {
try {
await axios.post(`${this.grafanaBaseURL}/api/dashboards/db`, FINAL_JSON, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiToken}`
}
});
} catch (err) {
console.error('Error updating Grafana JSON!', err);
}
}

Expand All @@ -70,26 +147,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;
});
}),
);
}
}



Loading
Loading