@davidilie/telemetry-node 0.0.0-bootstrap.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,436 @@
1
+ import { SpanKind, SpanStatusCode, context, metrics, trace } from "@opentelemetry/api";
2
+ import { SeverityNumber, logs } from "@opentelemetry/api-logs";
3
+ import { TelemetryClient, redactText, sanitizeAttributes, sanitizeResource, sanitizeSignal } from "@davidilie/telemetry-core";
4
+ import { SamplingDecision } from "@opentelemetry/sdk-trace-base";
5
+ //#region src/attributes.ts
6
+ function toOtelAttributeValue(value) {
7
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
8
+ if (value.every((entry) => typeof entry === "string")) return [...value];
9
+ if (value.every((entry) => typeof entry === "number")) return [...value];
10
+ if (value.every((entry) => typeof entry === "boolean")) return [...value];
11
+ return value.map(String);
12
+ }
13
+ function toOtelAttributes(attributes = {}) {
14
+ const result = {};
15
+ for (const [key, value] of Object.entries(attributes)) {
16
+ if (value == null) continue;
17
+ result[key] = toOtelAttributeValue(value);
18
+ }
19
+ return result;
20
+ }
21
+ function telemetryResourceAttributes(resource) {
22
+ return {
23
+ ...toOtelAttributes(resource.attributes),
24
+ "service.name": resource.serviceName,
25
+ ...resource.serviceVersion ? { "service.version": resource.serviceVersion } : {},
26
+ ...resource.environment ? { "deployment.environment.name": resource.environment } : {},
27
+ ...resource.namespace ? { "service.namespace": resource.namespace } : {},
28
+ ...resource.repositoryUrl ? {
29
+ "app.repository.url": resource.repositoryUrl,
30
+ "vcs.repository.url.full": resource.repositoryUrl
31
+ } : {},
32
+ ...resource.commitSha ? { "vcs.ref.head.revision": resource.commitSha } : {},
33
+ ...resource.platform ? {
34
+ "app.platform": resource.platform,
35
+ "deployment.platform": resource.platform
36
+ } : {}
37
+ };
38
+ }
39
+ //#endregion
40
+ //#region src/adapter.ts
41
+ const severityNumbers = {
42
+ debug: SeverityNumber.DEBUG,
43
+ info: SeverityNumber.INFO,
44
+ warn: SeverityNumber.WARN,
45
+ error: SeverityNumber.ERROR
46
+ };
47
+ function exceptionForOtel(error) {
48
+ if (error instanceof Error) {
49
+ const safe = new Error(redactText(error.message), { ...error.cause !== void 0 ? { cause: redactText(String(error.cause)) } : {} });
50
+ safe.name = redactText(error.name, 256);
51
+ if (error.stack) safe.stack = redactText(error.stack, 16384);
52
+ return safe;
53
+ }
54
+ return redactText(typeof error === "string" ? error : String(error));
55
+ }
56
+ function signalAttributes(signal) {
57
+ return {
58
+ ...toOtelAttributes(signal.attributes),
59
+ ...telemetryResourceAttributes(signal.resource),
60
+ "telemetry.signal.id": signal.id,
61
+ "telemetry.signal.type": signal.type
62
+ };
63
+ }
64
+ const spanKinds = {
65
+ internal: SpanKind.INTERNAL,
66
+ server: SpanKind.SERVER,
67
+ client: SpanKind.CLIENT,
68
+ producer: SpanKind.PRODUCER,
69
+ consumer: SpanKind.CONSUMER
70
+ };
71
+ const spanStatuses = {
72
+ unset: SpanStatusCode.UNSET,
73
+ ok: SpanStatusCode.OK,
74
+ error: SpanStatusCode.ERROR
75
+ };
76
+ const VALID_INSTRUMENT_NAME = /^[A-Za-z][A-Za-z0-9_.\-/]{0,254}$/;
77
+ var OpenTelemetrySpan = class {
78
+ #span;
79
+ constructor(span) {
80
+ this.#span = span;
81
+ }
82
+ setAttribute(name, value) {
83
+ const safe = sanitizeAttributes({ [name]: value });
84
+ if (safe[name] !== void 0) this.#span.setAttribute(name, toOtelAttributeValue(safe[name]));
85
+ return this;
86
+ }
87
+ recordException(error, attributes = {}) {
88
+ this.#span.setAttributes(toOtelAttributes(sanitizeAttributes(attributes)));
89
+ this.#span.recordException(exceptionForOtel(error));
90
+ this.#span.setStatus({ code: SpanStatusCode.ERROR });
91
+ }
92
+ setStatus(status, message) {
93
+ this.#span.setStatus({
94
+ code: spanStatuses[status],
95
+ ...message ? { message: redactText(message) } : {}
96
+ });
97
+ return this;
98
+ }
99
+ end() {
100
+ this.#span.end();
101
+ }
102
+ };
103
+ var OpenTelemetryAdapter = class {
104
+ #instrumentationName;
105
+ #instrumentationVersion;
106
+ #resource;
107
+ #structuredConsole;
108
+ #measurementMode;
109
+ #metricAttributeAllowlist;
110
+ #maxMetricInstruments;
111
+ #logSampleRate;
112
+ #shutdown;
113
+ #histograms = /* @__PURE__ */ new Map();
114
+ constructor(config) {
115
+ this.#resource = sanitizeResource(config.resource);
116
+ this.#instrumentationName = config.instrumentationName ?? this.#resource.serviceName;
117
+ this.#instrumentationVersion = config.instrumentationVersion;
118
+ this.#structuredConsole = config.structuredConsole ?? true;
119
+ this.#measurementMode = config.measurementMode ?? "spans";
120
+ this.#metricAttributeAllowlist = new Set(config.metricAttributeAllowlist ?? []);
121
+ this.#maxMetricInstruments = Math.max(0, Math.floor(Number.isFinite(config.maxMetricInstruments) ? config.maxMetricInstruments ?? 64 : 64));
122
+ this.#logSampleRate = Number.isFinite(config.logSampleRate) ? Math.min(1, Math.max(0, config.logSampleRate ?? 1)) : 1;
123
+ this.#shutdown = config.shutdown;
124
+ }
125
+ send(signal) {
126
+ signal = sanitizeSignal(signal);
127
+ const attributes = signalAttributes(signal);
128
+ switch (signal.type) {
129
+ case "event": {
130
+ const activeSpan = trace.getActiveSpan();
131
+ if (activeSpan) {
132
+ activeSpan.addEvent(signal.name, attributes, new Date(signal.timestamp));
133
+ return;
134
+ }
135
+ const span = this.#tracer().startSpan(signal.name, {
136
+ kind: SpanKind.INTERNAL,
137
+ attributes,
138
+ startTime: new Date(signal.timestamp)
139
+ });
140
+ span.addEvent(signal.name, attributes, new Date(signal.timestamp));
141
+ span.end(new Date(signal.timestamp));
142
+ return;
143
+ }
144
+ case "exception": {
145
+ const activeSpan = trace.getActiveSpan();
146
+ const span = activeSpan ?? this.#tracer().startSpan("exception", {
147
+ kind: SpanKind.INTERNAL,
148
+ attributes,
149
+ startTime: new Date(signal.timestamp)
150
+ });
151
+ span.setAttributes(attributes);
152
+ span.recordException({
153
+ name: signal.exception.name,
154
+ message: signal.exception.message,
155
+ ...signal.exception.stack ? { stack: signal.exception.stack } : {}
156
+ });
157
+ span.setStatus({
158
+ code: SpanStatusCode.ERROR,
159
+ message: signal.exception.message
160
+ });
161
+ if (!activeSpan) span.end(new Date(signal.timestamp));
162
+ return;
163
+ }
164
+ case "log":
165
+ if (Math.random() >= this.#logSampleRate) return;
166
+ logs.getLogger(this.#instrumentationName, this.#instrumentationVersion).emit({
167
+ body: signal.message,
168
+ severityText: signal.level.toUpperCase(),
169
+ severityNumber: severityNumbers[signal.level],
170
+ attributes,
171
+ timestamp: new Date(signal.timestamp)
172
+ });
173
+ if (this.#structuredConsole) {
174
+ const activeTrace = currentTraceContext();
175
+ const record = JSON.stringify({
176
+ ...attributes,
177
+ timestamp: signal.timestamp,
178
+ level: signal.level,
179
+ message: signal.message,
180
+ service_name: signal.resource.serviceName,
181
+ ...signal.resource.serviceVersion ? { service_version: signal.resource.serviceVersion } : {},
182
+ ...activeTrace ? {
183
+ trace_id: activeTrace.traceId,
184
+ span_id: activeTrace.spanId
185
+ } : {}
186
+ });
187
+ console[signal.level](record);
188
+ }
189
+ return;
190
+ case "measurement": {
191
+ if (this.#measurementMode === "spans" || this.#measurementMode === "both") this.#tracer().startSpan(`measurement:${signal.name}`, {
192
+ kind: SpanKind.INTERNAL,
193
+ attributes: {
194
+ ...attributes,
195
+ "telemetry.measurement.value": signal.value,
196
+ ...signal.unit ? { "telemetry.measurement.unit": signal.unit } : {}
197
+ },
198
+ startTime: new Date(signal.timestamp)
199
+ }).end(new Date(signal.timestamp));
200
+ if (this.#measurementMode === "spans") return;
201
+ if (!VALID_INSTRUMENT_NAME.test(signal.name)) return;
202
+ const key = `${signal.name}\u0000${signal.unit ?? ""}`;
203
+ let histogram = this.#histograms.get(key);
204
+ if (!histogram) {
205
+ if (this.#histograms.size >= this.#maxMetricInstruments) return;
206
+ histogram = metrics.getMeter(this.#instrumentationName, this.#instrumentationVersion).createHistogram(signal.name, { ...signal.unit ? { unit: signal.unit } : {} });
207
+ this.#histograms.set(key, histogram);
208
+ }
209
+ const metricAttributes = Object.fromEntries(Object.entries(signal.attributes).filter(([name]) => this.#metricAttributeAllowlist.has(name)));
210
+ histogram.record(signal.value, toOtelAttributes(metricAttributes));
211
+ }
212
+ }
213
+ }
214
+ startSpan(name, attributes = {}, options = {}) {
215
+ return new OpenTelemetrySpan(this.#tracer().startSpan(redactText(name, 256), {
216
+ kind: spanKinds[options.kind ?? "internal"],
217
+ attributes: {
218
+ ...toOtelAttributes(sanitizeAttributes(attributes)),
219
+ ...telemetryResourceAttributes(this.#resource)
220
+ }
221
+ }));
222
+ }
223
+ currentTraceContext() {
224
+ return currentTraceContext();
225
+ }
226
+ async withActiveSpan(name, operation, attributes = {}, options = {}) {
227
+ let operationRan = false;
228
+ let operationSucceeded = false;
229
+ let operationResult;
230
+ let operationError;
231
+ try {
232
+ await this.#tracer().startActiveSpan(redactText(name, 256), {
233
+ kind: spanKinds[options.kind ?? "internal"],
234
+ attributes: {
235
+ ...toOtelAttributes(sanitizeAttributes(attributes)),
236
+ ...telemetryResourceAttributes(this.#resource)
237
+ }
238
+ }, async (span) => {
239
+ operationRan = true;
240
+ try {
241
+ operationResult = await operation();
242
+ operationSucceeded = true;
243
+ } catch (error) {
244
+ operationError = error;
245
+ try {
246
+ span.recordException(exceptionForOtel(error));
247
+ span.setStatus({
248
+ code: SpanStatusCode.ERROR,
249
+ ...error instanceof Error ? { message: redactText(error.message) } : {}
250
+ });
251
+ } catch {}
252
+ } finally {
253
+ try {
254
+ span.end();
255
+ } catch {}
256
+ }
257
+ });
258
+ } catch (telemetryError) {
259
+ if (!operationRan) throw telemetryError;
260
+ }
261
+ if (operationSucceeded) return operationResult;
262
+ if (operationRan) throw operationError;
263
+ return operation();
264
+ }
265
+ async shutdown() {
266
+ await this.#shutdown?.();
267
+ }
268
+ #tracer() {
269
+ return trace.getTracer(this.#instrumentationName, this.#instrumentationVersion);
270
+ }
271
+ };
272
+ function createOpenTelemetryAdapter(config) {
273
+ return new OpenTelemetryAdapter(config);
274
+ }
275
+ function currentTraceContext() {
276
+ const spanContext = trace.getSpan(context.active())?.spanContext();
277
+ if (!spanContext) return void 0;
278
+ return {
279
+ traceId: spanContext.traceId,
280
+ spanId: spanContext.spanId,
281
+ traceFlags: spanContext.traceFlags
282
+ };
283
+ }
284
+ function startSpan(name, attributes = {}, options = {}) {
285
+ return new OpenTelemetrySpan(trace.getTracer("@davidilie/telemetry-node").startSpan(redactText(name, 256), {
286
+ kind: spanKinds[options.kind ?? "internal"],
287
+ attributes: toOtelAttributes(sanitizeAttributes(attributes))
288
+ }));
289
+ }
290
+ async function withSpan(name, operation, attributes = {}, options = {}) {
291
+ return trace.getTracer("@davidilie/telemetry-node").startActiveSpan(redactText(name, 256), {
292
+ kind: spanKinds[options.kind ?? "internal"],
293
+ attributes: toOtelAttributes(sanitizeAttributes(attributes))
294
+ }, async (span) => {
295
+ try {
296
+ return await operation();
297
+ } catch (error) {
298
+ try {
299
+ span.recordException(exceptionForOtel(error));
300
+ span.setStatus({
301
+ code: SpanStatusCode.ERROR,
302
+ ...error instanceof Error ? { message: redactText(error.message) } : {}
303
+ });
304
+ } catch {}
305
+ throw error;
306
+ } finally {
307
+ try {
308
+ span.end();
309
+ } catch {}
310
+ }
311
+ });
312
+ }
313
+ function recordException(error, attributes = {}) {
314
+ const activeSpan = trace.getActiveSpan();
315
+ const span = activeSpan ?? trace.getTracer("@davidilie/telemetry-node").startSpan("exception");
316
+ span.setAttributes(toOtelAttributes(sanitizeAttributes(attributes)));
317
+ span.recordException(exceptionForOtel(error));
318
+ span.setStatus({
319
+ code: SpanStatusCode.ERROR,
320
+ ...error instanceof Error ? { message: redactText(error.message) } : {}
321
+ });
322
+ if (!activeSpan) span.end();
323
+ }
324
+ //#endregion
325
+ //#region src/client.ts
326
+ var NodeTelemetryClient = class extends TelemetryClient {
327
+ #adapter;
328
+ #sampleRate;
329
+ #onError;
330
+ #onCollectionStateChange;
331
+ #enabled;
332
+ #consent;
333
+ constructor(config) {
334
+ const adapter = new OpenTelemetryAdapter({
335
+ resource: config.resource,
336
+ ...config.instrumentationName ? { instrumentationName: config.instrumentationName } : {},
337
+ ...config.instrumentationVersion ? { instrumentationVersion: config.instrumentationVersion } : {},
338
+ ...config.structuredConsole !== void 0 ? { structuredConsole: config.structuredConsole } : {},
339
+ ...config.measurementMode ? { measurementMode: config.measurementMode } : {},
340
+ ...config.metricAttributeAllowlist ? { metricAttributeAllowlist: config.metricAttributeAllowlist } : {},
341
+ ...config.maxMetricInstruments !== void 0 ? { maxMetricInstruments: config.maxMetricInstruments } : {},
342
+ ...config.providerManagedSampling ? { logSampleRate: config.sampleRate } : {},
343
+ ...config.shutdown ? { shutdown: config.shutdown } : {}
344
+ });
345
+ super({
346
+ adapter,
347
+ resource: config.resource,
348
+ ...config.beforeSend ? { beforeSend: config.beforeSend } : {},
349
+ ...config.enabled !== void 0 ? { enabled: config.enabled } : {},
350
+ ...config.sampleRate !== void 0 ? { sampleRate: config.providerManagedSampling ? 1 : config.sampleRate } : {},
351
+ ...config.consent ? { consent: config.consent } : {},
352
+ ...config.debug !== void 0 ? { debug: config.debug } : {},
353
+ ...config.onError ? { onError: config.onError } : {}
354
+ });
355
+ this.#adapter = adapter;
356
+ this.#sampleRate = config.providerManagedSampling ? 1 : Number.isFinite(config.sampleRate) ? Math.min(1, Math.max(0, config.sampleRate ?? 1)) : 1;
357
+ this.#onError = config.onError;
358
+ this.#onCollectionStateChange = config.onCollectionStateChange;
359
+ this.#enabled = config.enabled ?? true;
360
+ this.#consent = config.consent ?? "granted";
361
+ }
362
+ setEnabled(enabled) {
363
+ this.#enabled = enabled;
364
+ super.setEnabled(enabled);
365
+ this.#onCollectionStateChange?.({
366
+ enabled: this.#enabled,
367
+ consent: this.#consent
368
+ });
369
+ }
370
+ setConsent(consent) {
371
+ this.#consent = consent;
372
+ super.setConsent(consent);
373
+ this.#onCollectionStateChange?.({
374
+ enabled: this.#enabled,
375
+ consent: this.#consent
376
+ });
377
+ }
378
+ async withSpan(name, operation, attributes = {}, options = {}) {
379
+ if (!this.#enabled || this.#consent !== "granted" || Math.random() >= this.#sampleRate) return operation();
380
+ let operationStarted = false;
381
+ try {
382
+ return await this.#adapter.withActiveSpan(redactText(name, 256), () => {
383
+ operationStarted = true;
384
+ return operation();
385
+ }, sanitizeAttributes(attributes), options);
386
+ } catch (error) {
387
+ if (operationStarted) throw error;
388
+ this.#reportAdapterError(error, { operation: "startSpan" });
389
+ return operation();
390
+ }
391
+ }
392
+ #reportAdapterError(error, context) {
393
+ if (!this.#onError) return;
394
+ try {
395
+ Promise.resolve(this.#onError(error, context)).catch(() => void 0);
396
+ } catch {}
397
+ }
398
+ };
399
+ function createNodeTelemetry(config) {
400
+ return new NodeTelemetryClient(config);
401
+ }
402
+ //#endregion
403
+ //#region src/sampler.ts
404
+ /**
405
+ * A mutable gate around an ordinary OpenTelemetry sampler.
406
+ *
407
+ * OpenTelemetry providers are registered once, but consent and operational
408
+ * switches can change later. This wrapper drops every span—including children
409
+ * of sampled remote parents—while collection is disabled, then delegates to
410
+ * the configured sampler immediately after collection is re-enabled.
411
+ */
412
+ var DynamicTelemetrySampler = class {
413
+ delegate;
414
+ #enabled;
415
+ #consent;
416
+ constructor(delegate, state) {
417
+ this.delegate = delegate;
418
+ this.#enabled = state.enabled;
419
+ this.#consent = state.consent;
420
+ }
421
+ setEnabled(enabled) {
422
+ this.#enabled = enabled;
423
+ }
424
+ setConsent(consent) {
425
+ this.#consent = consent;
426
+ }
427
+ shouldSample(parentContext, traceId, spanName, spanKind, attributes, links) {
428
+ if (!this.#enabled || this.#consent !== "granted") return { decision: SamplingDecision.NOT_RECORD };
429
+ return this.delegate.shouldSample(parentContext, traceId, spanName, spanKind, attributes, links);
430
+ }
431
+ toString() {
432
+ return `DynamicTelemetrySampler{${this.delegate.toString()}}`;
433
+ }
434
+ };
435
+ //#endregion
436
+ export { OpenTelemetrySpan as a, recordException as c, telemetryResourceAttributes as d, toOtelAttributeValue as f, OpenTelemetryAdapter as i, startSpan as l, NodeTelemetryClient as n, createOpenTelemetryAdapter as o, toOtelAttributes as p, createNodeTelemetry as r, currentTraceContext as s, DynamicTelemetrySampler as t, withSpan as u };
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@davidilie/telemetry-node",
3
+ "version": "0.0.0-bootstrap.0",
4
+ "description": "OpenTelemetry-backed telemetry for Node.js services.",
5
+ "keywords": [
6
+ "analytics",
7
+ "node",
8
+ "observability",
9
+ "opentelemetry",
10
+ "telemetry"
11
+ ],
12
+ "author": "David Ilie <david@davidilie.com> (https://davidilie.com)",
13
+ "license": "Apache-2.0",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/DavidIlie/davidapps-telemetry.git",
17
+ "directory": "packages/node"
18
+ },
19
+ "homepage": "https://github.com/DavidIlie/davidapps-telemetry#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/DavidIlie/davidapps-telemetry/issues"
22
+ },
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE",
32
+ "THIRD_PARTY_NOTICES.md",
33
+ "UPSTREAM.yml"
34
+ ],
35
+ "main": "./dist/index.cjs",
36
+ "module": "./dist/index.mjs",
37
+ "types": "./dist/index.d.mts",
38
+ "exports": {
39
+ ".": {
40
+ "import": {
41
+ "types": "./dist/index.d.mts",
42
+ "default": "./dist/index.mjs"
43
+ },
44
+ "require": {
45
+ "types": "./dist/index.d.cts",
46
+ "default": "./dist/index.cjs"
47
+ }
48
+ },
49
+ "./register": {
50
+ "import": {
51
+ "types": "./dist/register.d.mts",
52
+ "default": "./dist/register.mjs"
53
+ },
54
+ "require": {
55
+ "types": "./dist/register.d.cts",
56
+ "default": "./dist/register.cjs"
57
+ }
58
+ }
59
+ },
60
+ "scripts": {
61
+ "build": "tsdown src/index.ts src/register.ts --format esm,cjs --dts --tsconfig tsconfig.build.json",
62
+ "check": "tsc -p tsconfig.json",
63
+ "test": "vitest run",
64
+ "lint": "tsc -p tsconfig.json",
65
+ "clean": "rm -rf dist coverage"
66
+ },
67
+ "dependencies": {
68
+ "@davidilie/telemetry-core": "0.0.0-bootstrap.0",
69
+ "@opentelemetry/api-logs": "0.221.0",
70
+ "@opentelemetry/resources": "2.10.0",
71
+ "@opentelemetry/sdk-node": "0.221.0",
72
+ "@opentelemetry/sdk-trace-base": "2.10.0"
73
+ },
74
+ "peerDependencies": {
75
+ "@opentelemetry/api": ">=1.9.0 <2.0.0"
76
+ },
77
+ "devDependencies": {
78
+ "@opentelemetry/api": "1.9.0"
79
+ },
80
+ "publishConfig": {
81
+ "access": "public",
82
+ "registry": "https://registry.npmjs.org/",
83
+ "provenance": true
84
+ }
85
+ }