Skip to content

Commit

Permalink
feat: export rest metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
dhhyi authored and shauke committed Sep 7, 2023
1 parent 0127488 commit d49262d
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 2 deletions.
23 changes: 21 additions & 2 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ const requestDuration = new client.Histogram({
labelNames: ['status_code', 'base_href', 'path'],
});

const restRequestDuration = new client.Summary({
name: 'pwa_rest_request_duration_seconds',
help: 'duration histogram of ICM rest responses',
percentiles: [0.5, 0.9, 0.95, 0.99],
labelNames: ['endpoint'],
});

const PM2 = process.env.pm_id && process.env.name ? `${process.env.pm_id} ${process.env.name}` : undefined;

if (PM2) {
Expand Down Expand Up @@ -163,12 +170,17 @@ export function app() {
// Express server
const server = express();

const prometheusRest: { endpoint: string; duration: number }[] = [];

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine(
'html',
ngExpressEngine({
bootstrap: AppServerModule,
providers: [{ provide: 'SSR_HYBRID', useValue: !!process.env.SSR_HYBRID }],
providers: [
{ provide: 'SSR_HYBRID', useValue: !!process.env.SSR_HYBRID },
{ provide: 'PROMETHEUS_REST', useValue: prometheusRest },
],
inlineCriticalCss: false,
})
);
Expand Down Expand Up @@ -450,12 +462,19 @@ export function app() {
onFinished(res, () => {
const duration = Date.now() - start;
const matched = /;baseHref=([^;?]*)/.exec(req.originalUrl);
const base_href = matched?.[1] ? `${decodeURIComponent(decodeURIComponent(matched[1]))}/` : '/';
let base_href = matched?.[1] ? `${decodeURIComponent(decodeURIComponent(matched[1]))}` : '/';
if (!base_href.endsWith('/')) {
base_href += '/';
}
const cleanUrl = req.originalUrl.replace(/[;?].*/g, '');
const path = cleanUrl.replace(base_href, '');

requestCounts.inc({ method: req.method, status_code: res.statusCode, base_href, path });
requestDuration.labels({ status_code: res.statusCode, base_href, path }).observe(duration / 1000);
prometheusRest.forEach(({ endpoint, duration }) => {
restRequestDuration.labels({ endpoint }).observe(duration / 1000);
});
prometheusRest.length = 0;
});
next();
});
Expand Down
2 changes: 2 additions & 0 deletions src/app/app.server.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { COOKIE_CONSENT_VERSION, DISPLAY_VERSION } from 'ish-core/configurations
import { UniversalCacheInterceptor } from 'ish-core/interceptors/universal-cache.interceptor';
import { UniversalLogInterceptor } from 'ish-core/interceptors/universal-log.interceptor';
import { UniversalMockInterceptor } from 'ish-core/interceptors/universal-mock.interceptor';
import { UniversalPrometheusInterceptor } from 'ish-core/interceptors/universal-prometheus.interceptor';

import { environment } from '../environments/environment';

Expand Down Expand Up @@ -40,6 +41,7 @@ export class UniversalErrorHandler implements ErrorHandler {
{ provide: HTTP_INTERCEPTORS, useClass: UniversalMockInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: UniversalCacheInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: UniversalLogInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: UniversalPrometheusInterceptor, multi: true },
{ provide: ErrorHandler, useClass: UniversalErrorHandler },
{ provide: META_REDUCERS, useValue: configurationMeta, multi: true },
// disable data retention for SSR
Expand Down
80 changes: 80 additions & 0 deletions src/app/core/interceptors/universal-prometheus.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpResponse,
} from '@angular/common/http';
import { Inject, Injectable, Optional } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { map, tap, withLatestFrom } from 'rxjs/operators';

import { getRestEndpoint } from 'ish-core/store/core/configuration';
import { whenTruthy } from 'ish-core/utils/operators';

@Injectable()
export class UniversalPrometheusInterceptor implements HttpInterceptor {
constructor(
private store: Store,
@Optional()
@Inject('PROMETHEUS_REST')
private restCalls: { endpoint: string; duration: number }[]
) {}

private endpointCategory(path: string): string {
const pathSegments = path
// clear leading slash and before (usually ...;loc=...;cur=...)
.replace(/^[^\/]*\//, '')
// clear trailing slash
.replace(/\/$/, '')
.split('/');

const endpoint = pathSegments[0];
if (endpoint === 'products' && pathSegments.length > 2) {
// product sub calls like /products/SKU/links
return `${endpoint}/${pathSegments[2]}`;
} else if (endpoint === 'categories' && pathSegments.length === 1) {
// category tree
return `${endpoint}/tree`;
} else if (endpoint === 'categories' && pathSegments[pathSegments.length - 1] === 'products') {
// category product list
return `${endpoint}/products`;
} else if (endpoint === 'cms' && pathSegments.length >= 2) {
// cms sub calls
return `${endpoint}/${pathSegments[1]}`;
}
return endpoint;
}

intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
if (!SSR && !/on|1|true|yes/.test(process.env.PROMETHEUS?.toLowerCase())) {
return next.handle(req);
}

// eslint-disable-next-line @typescript-eslint/no-var-requires
const { performance } = require('perf_hooks');

const start = performance.now();

const tracker = (args: [HttpEvent<unknown>, string]) => {
if (args && args.length === 2) {
const [res, restEndPoint] = args;

if ((res instanceof HttpResponse || res instanceof HttpErrorResponse) && req.url.startsWith(restEndPoint)) {
const duration = performance.now() - start;
const url = req.url.replace(restEndPoint, '');
const endpoint = this.endpointCategory(url);
this.restCalls.push({ endpoint, duration });
}
}
};

return next.handle(req).pipe(
withLatestFrom(this.store.pipe(select(getRestEndpoint), whenTruthy())),
tap({ next: tracker, error: tracker }),
map(([res]) => res)
);
}
}

0 comments on commit d49262d

Please sign in to comment.