Skip to content

Commit 4d5c57a

Browse files
authored
feat: improve OTLP configurability and ignore health and metric spans by default (#33)
1 parent ccf8b46 commit 4d5c57a

File tree

5 files changed

+1212
-1021
lines changed

5 files changed

+1212
-1021
lines changed

package.json

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -70,66 +70,67 @@
7070
"dependencies": {
7171
"@godaddy/terminus": "^4.12.1",
7272
"@opentelemetry/api": "^1.9.0",
73-
"@opentelemetry/auto-instrumentations-node": "^0.56.1",
74-
"@opentelemetry/exporter-prometheus": "^0.57.2",
75-
"@opentelemetry/instrumentation-dns": "^0.43.1",
76-
"@opentelemetry/instrumentation-express": "^0.47.1",
77-
"@opentelemetry/instrumentation-generic-pool": "^0.43.1",
78-
"@opentelemetry/instrumentation-graphql": "^0.47.1",
79-
"@opentelemetry/instrumentation-http": "^0.57.2",
80-
"@opentelemetry/instrumentation-ioredis": "^0.47.1",
81-
"@opentelemetry/instrumentation-net": "^0.43.1",
82-
"@opentelemetry/instrumentation-pg": "^0.51.1",
83-
"@opentelemetry/instrumentation-pino": "^0.46.1",
84-
"@opentelemetry/instrumentation-undici": "^0.10.1",
85-
"@opentelemetry/resource-detector-container": "^0.6.1",
86-
"@opentelemetry/resource-detector-gcp": "^0.33.1",
87-
"@opentelemetry/sdk-node": "^0.57.2",
88-
"@opentelemetry/semantic-conventions": "^1.30.0",
73+
"@opentelemetry/auto-instrumentations-node": "^0.59.0",
74+
"@opentelemetry/exporter-prometheus": "^0.201.1",
75+
"@opentelemetry/instrumentation-dns": "^0.45.0",
76+
"@opentelemetry/instrumentation-express": "^0.50.0",
77+
"@opentelemetry/instrumentation-generic-pool": "^0.45.0",
78+
"@opentelemetry/instrumentation-graphql": "^0.49.0",
79+
"@opentelemetry/instrumentation-http": "^0.201.1",
80+
"@opentelemetry/instrumentation-ioredis": "^0.49.0",
81+
"@opentelemetry/instrumentation-net": "^0.45.0",
82+
"@opentelemetry/instrumentation-pg": "^0.53.0",
83+
"@opentelemetry/instrumentation-pino": "^0.48.0",
84+
"@opentelemetry/instrumentation-undici": "^0.12.0",
85+
"@opentelemetry/resource-detector-container": "^0.7.1",
86+
"@opentelemetry/resource-detector-gcp": "^0.35.0",
87+
"@opentelemetry/sdk-node": "^0.201.1",
88+
"@opentelemetry/semantic-conventions": "^1.33.0",
8989
"@sesamecare-oss/confit": "^2.2.1",
9090
"@sesamecare-oss/opentelemetry-node-metrics": "^1.1.0",
9191
"ajv": "^8.17.1",
9292
"clean-stack": "^5.2.0",
9393
"cookie-parser": "^1.4.7",
94-
"dotenv": "^16.4.7",
94+
"dotenv": "^16.5.0",
9595
"express": "^5.1.0",
96-
"express-openapi-validator": "^5.4.7",
97-
"glob": "^11.0.1",
98-
"import-in-the-middle": "^1.13.1",
96+
"express-openapi-validator": "^5.5.2",
97+
"glob": "^11.0.2",
98+
"import-in-the-middle": "^1.13.2",
9999
"minimist": "^1.2.8",
100100
"moderndash": "^4.0.0",
101-
"pino": "^9.6.0",
101+
"opentelemetry-resource-detector-sync-api": "^0.30.0",
102+
"pino": "^9.7.0",
102103
"read-package-up": "^11.0.0",
103104
"request-ip": "^3.3.0"
104105
},
105106
"devDependencies": {
106-
"@commitlint/cli": "^19.8.0",
107-
"@commitlint/config-conventional": "^19.8.0",
108-
"@openapi-typescript-infra/coconfig": "^4.6.0",
107+
"@commitlint/cli": "^19.8.1",
108+
"@commitlint/config-conventional": "^19.8.1",
109+
"@openapi-typescript-infra/coconfig": "^4.7.1",
109110
"@semantic-release/commit-analyzer": "^13.0.1",
110-
"@semantic-release/exec": "^7.0.3",
111-
"@semantic-release/github": "^11.0.1",
111+
"@semantic-release/exec": "^7.1.0",
112+
"@semantic-release/github": "^11.0.2",
112113
"@semantic-release/release-notes-generator": "^14.0.3",
113114
"@types/cookie-parser": "^1.4.8",
114-
"@types/express": "^5.0.1",
115+
"@types/express": "^5.0.2",
115116
"@types/minimist": "^1.2.5",
116-
"@types/node": "^22.13.17",
117+
"@types/node": "^22.15.20",
117118
"@types/request-ip": "^0.0.41",
118119
"@types/supertest": "^6.0.3",
119120
"@typescript-eslint/eslint-plugin": "^7.18.0",
120121
"@typescript-eslint/parser": "^7.18.0",
121-
"coconfig": "^1.6.1",
122+
"coconfig": "^1.6.2",
122123
"eslint": "^8.57.1",
123124
"eslint-config-prettier": "^9.1.0",
124-
"eslint-import-resolver-typescript": "^4.3.1",
125+
"eslint-import-resolver-typescript": "^4.3.5",
125126
"eslint-plugin-import": "^2.31.0",
126127
"pino-pretty": "^13.0.0",
127128
"pinst": "^3.0.0",
128-
"supertest": "^7.1.0",
129+
"supertest": "^7.1.1",
129130
"tsconfig-paths": "^4.2.0",
130-
"tsx": "^4.19.3",
131-
"typescript": "^5.8.2",
132-
"vitest": "^3.1.1"
131+
"tsx": "^4.19.4",
132+
"typescript": "^5.8.3",
133+
"vitest": "^3.1.4"
133134
},
134135
"resolutions": {
135136
"qs": "^6.11.0"

src/express-app/app.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ export async function startApp<
232232
}
233233

234234
if (routing?.freezeQuery) {
235-
app.use((req, res, next) => {
235+
app.use(function freezeQuery(req, res, next) {
236236
// Express 5 re-parses the query string every time. This causes problems with
237237
// various libraries, namely the express OpenAPI parser. So we "freeze it" in place
238238
// here, which runs right before the routing validation logic does. Note that this
@@ -259,7 +259,8 @@ export async function startApp<
259259
);
260260
}
261261
if (routing?.openapi) {
262-
app.use(await openApi(app, rootDirectory, codepath, codePattern, options.openApiOptions));
262+
const openApiMiddleware = await openApi(app, rootDirectory, codepath, codePattern, options.openApiOptions);
263+
app.use(openApiMiddleware);
263264
}
264265

265266
// Putting this here allows more flexible middleware insertion

src/telemetry/index.ts

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-proto';
22
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
33
import {
4-
Detector,
5-
DetectorSync,
6-
envDetectorSync,
7-
hostDetectorSync,
8-
IResource,
9-
osDetectorSync,
10-
processDetectorSync,
11-
ResourceDetectionConfig,
4+
detectResources,
5+
envDetector,
6+
hostDetector,
7+
osDetector,
8+
processDetector,
129
} from '@opentelemetry/resources';
1310
import { containerDetector } from '@opentelemetry/resource-detector-container';
1411
import { gcpDetector } from '@opentelemetry/resource-detector-gcp';
@@ -67,17 +64,6 @@ function getLogExporter() {
6764
let prometheusExporter: PrometheusExporter | undefined;
6865
let telemetrySdk: opentelemetry.NodeSDK | undefined;
6966

70-
function awaitAttributes(detector: DetectorSync): Detector {
71-
return {
72-
async detect(config?: ResourceDetectionConfig): Promise<IResource> {
73-
const resource = detector.detect(config)
74-
await resource.waitForAsyncAttributes?.()
75-
76-
return resource
77-
},
78-
}
79-
}
80-
8167
/**
8268
* OpenTelemetry is not friendly to the idea of stopping
8369
* and starting itself, it seems. So we can only keep a global
@@ -90,33 +76,40 @@ export async function startGlobalTelemetry(serviceName: string) {
9076
if (!prometheusExporter) {
9177
const { metrics, logs, NodeSDK } = opentelemetry;
9278

79+
const resource = await detectResources({
80+
detectors: [
81+
envDetector,
82+
hostDetector,
83+
osDetector,
84+
processDetector,
85+
containerDetector,
86+
gcpDetector,
87+
],
88+
});
89+
9390
prometheusExporter = new PrometheusExporter({ preventServerStart: true });
9491
const instrumentations = getAutoInstrumentations();
9592
const logExporter = getLogExporter();
9693
telemetrySdk = new NodeSDK({
9794
serviceName,
9895
autoDetectResources: false,
99-
resourceDetectors: [
100-
awaitAttributes(envDetectorSync),
101-
awaitAttributes(hostDetectorSync),
102-
awaitAttributes(osDetectorSync),
103-
awaitAttributes(processDetectorSync),
104-
awaitAttributes(containerDetector),
105-
awaitAttributes(gcpDetector),
106-
],
96+
resource,
10797
traceExporter: getSpanExporter(),
10898
metricReader: prometheusExporter,
10999
instrumentations,
110100
logRecordProcessors: logExporter ? [new logs.BatchLogRecordProcessor(logExporter)] : [],
111101
views: [
112-
new metrics.View({
102+
{
113103
instrumentName: 'http_request_duration_seconds',
114104
instrumentType: metrics.InstrumentType.HISTOGRAM,
115-
aggregation: new metrics.ExplicitBucketHistogramAggregation(
116-
[0.003, 0.03, 0.1, 0.3, 1.5, 10],
117-
true,
118-
),
119-
}),
105+
aggregation: {
106+
type: metrics.AggregationType.EXPLICIT_BUCKET_HISTOGRAM,
107+
options: {
108+
boundaries: [0.003, 0.03, 0.1, 0.3, 1.5, 10],
109+
recordMinMax: true,
110+
},
111+
},
112+
},
120113
],
121114
});
122115
telemetrySdk.start();
@@ -160,3 +153,5 @@ export async function startWithTelemetry<
160153
});
161154
return { app, codepath: options.codepath, server };
162155
}
156+
157+
export { setTelemetryHooks } from './instrumentations.js';

src/telemetry/instrumentations.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { Instrumentation } from '@opentelemetry/instrumentation';
22
import { DnsInstrumentation } from '@opentelemetry/instrumentation-dns';
3-
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
3+
import { ExpressInstrumentation, SpanNameHook } from '@opentelemetry/instrumentation-express';
44
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';
55
import { GenericPoolInstrumentation } from '@opentelemetry/instrumentation-generic-pool';
6-
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
6+
import { HttpInstrumentation, IgnoreIncomingRequestFunction } from '@opentelemetry/instrumentation-http';
77
import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
88
import { NetInstrumentation } from '@opentelemetry/instrumentation-net';
99
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
@@ -27,8 +27,45 @@ export type InstrumentationConfigMap = {
2727
[Name in keyof typeof InstrumentationMap]?: ConfigArg<(typeof InstrumentationMap)[Name]>;
2828
};
2929

30+
let ignoreIncomingRequestHook: IgnoreIncomingRequestFunction | undefined = (req) => {
31+
return req.url === '/health' || req.url === '/metrics';
32+
};
33+
34+
let spanNameHook: SpanNameHook | undefined;
35+
36+
export function setTelemetryHooks(hooks: {
37+
ignoreIncomingRequestHook?: IgnoreIncomingRequestFunction;
38+
spanNameHook?: SpanNameHook;
39+
}) {
40+
if ('ignoreIncomingRequestHook' in hooks) {
41+
ignoreIncomingRequestHook = hooks.ignoreIncomingRequestHook;
42+
}
43+
if ('spanNameHook' in hooks) {
44+
spanNameHook = hooks.spanNameHook;
45+
}
46+
}
47+
48+
const defaultConfigs: InstrumentationConfigMap = {
49+
'@opentelemetry/instrumentation-http': {
50+
ignoreIncomingRequestHook(req) {
51+
if (ignoreIncomingRequestHook) {
52+
return ignoreIncomingRequestHook(req);
53+
}
54+
return false;
55+
},
56+
},
57+
'@opentelemetry/instrumentation-express': {
58+
spanNameHook(info, defaultName) {
59+
if (spanNameHook) {
60+
return spanNameHook(info, defaultName);
61+
}
62+
return defaultName;
63+
},
64+
},
65+
};
66+
3067
export function getAutoInstrumentations(
31-
inputConfigs: InstrumentationConfigMap = {},
68+
inputConfigs: InstrumentationConfigMap = defaultConfigs,
3269
): Instrumentation[] {
3370
const keys = Object.keys(InstrumentationMap) as Array<keyof typeof InstrumentationMap>;
3471
return keys

0 commit comments

Comments
 (0)