diff --git a/README.md b/README.md index 59cefb5..bceeb96 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,18 @@ const app = new ExpressKit(nodekit, { app.run(); ``` +## Self telemetry + +By default, self telemetry sends the original request URL. Applications with large or +high-cardinality query strings can strip query parameters before sending stats: + +```typescript +const config: Partial = { + appTelemetryChEnableSelfStats: true, + appTelemetryChSelfStatsStripQueryParams: true, +}; +``` + ## CSP `config.ts` diff --git a/src/router.ts b/src/router.ts index 030bff6..0fe4db1 100644 --- a/src/router.ts +++ b/src/router.ts @@ -14,6 +14,12 @@ import { } from './types'; import {prepareCSRFMiddleware} from './csrf'; +function stripQueryString(url: string) { + const queryIndex = url.indexOf('?'); + + return queryIndex === -1 ? url : url.slice(0, queryIndex); +} + // Methods are lowercased to use it in `expressApp[method]` function isAllowedMethod(method: string): method is Lowercase | 'mount' { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -132,6 +138,11 @@ export function setupRoutes(ctx: AppContext, expressApp: Express, routes: AppRou const disableSelfStats = Boolean(req.routeInfo.disableSelfStats); if (!disableSelfStats) { + // Stripping removes all query parameter values, so redaction is unnecessary in this mode. + const requestUrl = ctx.config.appTelemetryChSelfStatsStripQueryParams + ? stripQueryString(req.originalUrl) + : ctx.utils.redactSensitiveQueryParams(req.originalUrl); + req.originalContext.stats({ service: 'self', action: req.routeInfo.handlerName || UNNAMED_CONTROLLER, @@ -140,7 +151,7 @@ export function setupRoutes(ctx: AppContext, expressApp: Express, routes: AppRou requestId: req.originalContext.get(REQUEST_ID_PARAM_NAME) || '', requestTime: req.originalContext.getTime(), // We have to use req.originalContext here to get full time requestMethod: req.method, - requestUrl: ctx.utils.redactSensitiveQueryParams(req.originalUrl), + requestUrl, traceId: req.originalContext.getTraceId() || '', userId: req.originalContext.get(USER_ID_PARAM_NAME) || '', }); diff --git a/src/tests/self-stats.test.ts b/src/tests/self-stats.test.ts index 9ab95d4..8144041 100644 --- a/src/tests/self-stats.test.ts +++ b/src/tests/self-stats.test.ts @@ -55,7 +55,10 @@ describe('self stats telemetry', () => { const agent = request.agent(app.express); const requestId = Math.random().toString(); - await agent.get('/ping-self-stats').set(DEFAULT_REQUEST_ID_HEADER, requestId).expect(200); + await agent + .get('/ping-self-stats?filter=value') + .set(DEFAULT_REQUEST_ID_HEADER, requestId) + .expect(200); // last self stats data const stat = stats.mock.calls?.pop() || {}; @@ -67,12 +70,32 @@ describe('self stats telemetry', () => { responseStatus: 200, requestId, requestMethod: 'GET', - requestUrl: '/ping-self-stats', + requestUrl: '/ping-self-stats?filter=value', traceId: '', }, ]); }); + it('self stats telemetry strips query string when enabled', async () => { + const {app, stats} = setupApp({ + config: { + appTelemetryChSelfStatsStripQueryParams: true, + }, + }); + + const agent = request.agent(app.express); + + await agent.get('/ping-self-stats?filter=value&filter=other').expect(200); + + const stat = stats.mock.calls?.pop() || {}; + + expect(stat).toMatchObject([ + { + requestUrl: '/ping-self-stats', + }, + ]); + }); + it('self stats telemetry skipped', async () => { const {app, stats} = setupApp(); diff --git a/src/types.ts b/src/types.ts index 9dd37f0..57a88fc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,6 +49,7 @@ declare module '@gravity-ui/nodekit' { appAfterAuthMiddleware?: AppMiddleware[]; appTelemetryChEnableSelfStats?: boolean; + appTelemetryChSelfStatsStripQueryParams?: boolean; appLoggingOmitIdInMessages?: boolean;