From cf83043d8a4b76567523c1c1ed15816afc97555b Mon Sep 17 00:00:00 2001 From: David Leek Date: Thu, 15 Aug 2024 13:25:42 +0200 Subject: [PATCH] feat: resolve useragent source and add as source label to metrics (#7883) --- src/lib/metric-events.ts | 1 + src/lib/metrics.ts | 6 ++-- .../middleware/integration-headers.test.ts | 29 ++++++++++++++++ src/lib/middleware/integration-headers.ts | 34 +++++++++++++++++++ src/lib/middleware/origin-middleware.test.ts | 2 ++ src/lib/middleware/origin-middleware.ts | 10 ++++++ 6 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 src/lib/middleware/integration-headers.test.ts create mode 100644 src/lib/middleware/integration-headers.ts diff --git a/src/lib/metric-events.ts b/src/lib/metric-events.ts index b6b4976b008d..f989e5980abe 100644 --- a/src/lib/metric-events.ts +++ b/src/lib/metric-events.ts @@ -30,6 +30,7 @@ type MetricEvent = type RequestOriginEventPayload = { type: 'UI' | 'API'; method: Request['method']; + source?: string; }; type MetricEventPayloads = { diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index 2c6dd2cb0de7..d958d071ef18 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -350,7 +350,7 @@ export default class MetricsMonitor { const requestOriginCounter = createCounter({ name: 'request_origin_counter', help: 'Number of authenticated requests, including origin information.', - labelNames: ['type', 'method'], + labelNames: ['type', 'method', 'source'], }); const resourceLimit = createGauge({ @@ -715,9 +715,9 @@ export default class MetricsMonitor { events.onMetricEvent( eventBus, events.REQUEST_ORIGIN, - ({ type, method }) => { + ({ type, method, source }) => { if (flagResolver.isEnabled('originMiddleware')) { - requestOriginCounter.increment({ type, method }); + requestOriginCounter.increment({ type, method, source }); } }, ); diff --git a/src/lib/middleware/integration-headers.test.ts b/src/lib/middleware/integration-headers.test.ts new file mode 100644 index 000000000000..f93b524d2f6d --- /dev/null +++ b/src/lib/middleware/integration-headers.test.ts @@ -0,0 +1,29 @@ +import { determineIntegrationSource } from './integration-headers'; + +test('resolves known user agents to source labels', () => { + expect(determineIntegrationSource('axios/0.27.2')).toBe('Axios'); + expect(determineIntegrationSource('axios/1.4.0')).toBe('Axios'); + expect(determineIntegrationSource('curl/8.6.0')).toBe('Curl'); + expect(determineIntegrationSource('node-fetch/1.0.0')).toBe('Node'); + expect(determineIntegrationSource('node')).toBe('Node'); + expect(determineIntegrationSource('python-requests/2.31.0')).toBe('Python'); + expect(determineIntegrationSource('Terraform-Provider-Unleash/1.1.1')).toBe( + 'TerraformUnleash', + ); + expect(determineIntegrationSource('Jira-Cloud-Unleash')).toBe( + 'JiraCloudUnleash', + ); + expect(determineIntegrationSource('OpenAPI-Generator/1.0.0/go')).toBe( + 'OpenAPIGO', + ); + expect( + determineIntegrationSource('Apache-HttpClient/4.5.13 (Java/11.0.22)'), + ).toBe('Java'); + expect(determineIntegrationSource('Go-http-client/1.1')).toBe('Go'); + expect( + determineIntegrationSource( + 'rest-client/2.0.2 (linux-gnu x86_64) ruby/2.1.7p400', + ), + ).toBe('RestClientRuby'); + expect(determineIntegrationSource('No-http-client')).toBe('Other'); +}); diff --git a/src/lib/middleware/integration-headers.ts b/src/lib/middleware/integration-headers.ts new file mode 100644 index 000000000000..10714c265b1d --- /dev/null +++ b/src/lib/middleware/integration-headers.ts @@ -0,0 +1,34 @@ +import type { Request } from 'express'; + +const ORIGIN = 'origin'; +const httpMatcher = /^https?:\/\//; +const userAgentMatches = [ + { label: 'Axios', matcher: /^axios/ }, + { label: 'Curl', matcher: /^curl/ }, + { label: 'Go', matcher: /^Go-http-client/ }, + { label: 'Python', matcher: /^python-requests/ }, + { label: 'Node', matcher: /^node/ }, + { label: 'Java', matcher: /^Apache-HttpClient.*Java/ }, + { label: 'JiraCloudUnleash', matcher: /^Jira-Cloud-Unleash/ }, + { label: 'TerraformUnleash', matcher: /^Terraform-Provider-Unleash/ }, + { label: 'OpenAPIGO', matcher: /^OpenAPI-Generator\/.*\/go/ }, + { label: 'RestClientRuby', matcher: /^rest-client\/.*ruby/ }, +]; + +export const getFilteredOrigin = (request: Request): string | undefined => { + const origin = request.headers[ORIGIN]; + if (origin && httpMatcher.test(origin)) { + return origin; + } + + return undefined; +}; + +export const determineIntegrationSource = ( + userAgent: string, +): string | undefined => { + return ( + userAgentMatches.find((candidate) => candidate.matcher.test(userAgent)) + ?.label ?? 'Other' + ); +}; diff --git a/src/lib/middleware/origin-middleware.test.ts b/src/lib/middleware/origin-middleware.test.ts index 0adef86f3263..280a13a39d8b 100644 --- a/src/lib/middleware/origin-middleware.test.ts +++ b/src/lib/middleware/origin-middleware.test.ts @@ -69,6 +69,7 @@ describe('originMiddleware', () => { expect(eventBus.emit).toHaveBeenCalledWith(REQUEST_ORIGIN, { type: 'API', method: req.method, + source: 'Other', }); }); @@ -83,6 +84,7 @@ describe('originMiddleware', () => { expect(loggerMock.info).toHaveBeenCalledWith('API request', { method: req.method, userAgent: TEST_USER_AGENT, + origin: undefined, }); }); }); diff --git a/src/lib/middleware/origin-middleware.ts b/src/lib/middleware/origin-middleware.ts index 7a1b07a3aad7..8277effcdb1a 100644 --- a/src/lib/middleware/origin-middleware.ts +++ b/src/lib/middleware/origin-middleware.ts @@ -1,6 +1,10 @@ import type { Request, Response, NextFunction } from 'express'; import type { IUnleashConfig } from '../types'; import { REQUEST_ORIGIN, emitMetricEvent } from '../metric-events'; +import { + determineIntegrationSource, + getFilteredOrigin, +} from './integration-headers'; export const originMiddleware = ({ getLogger, @@ -23,13 +27,19 @@ export const originMiddleware = ({ method: req.method, }); } else { + const userAgent = req.headers['user-agent']; + const uaLabel = userAgent + ? determineIntegrationSource(userAgent) + : 'Other'; logger.info('API request', { method: req.method, userAgent: req.headers['user-agent'], + origin: getFilteredOrigin(req), }); emitMetricEvent(eventBus, REQUEST_ORIGIN, { type: 'API', method: req.method, + source: uaLabel, }); }