autotel 2.1.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.
- package/LICENSE +21 -0
- package/README.md +1946 -0
- package/dist/chunk-2LNRY4QK.js +273 -0
- package/dist/chunk-2LNRY4QK.js.map +1 -0
- package/dist/chunk-3HENGDW2.js +587 -0
- package/dist/chunk-3HENGDW2.js.map +1 -0
- package/dist/chunk-4OAT42CA.cjs +73 -0
- package/dist/chunk-4OAT42CA.cjs.map +1 -0
- package/dist/chunk-5GWX5LFW.js +70 -0
- package/dist/chunk-5GWX5LFW.js.map +1 -0
- package/dist/chunk-5R2M36QB.js +195 -0
- package/dist/chunk-5R2M36QB.js.map +1 -0
- package/dist/chunk-5ZN622AO.js +73 -0
- package/dist/chunk-5ZN622AO.js.map +1 -0
- package/dist/chunk-77MSMAUQ.cjs +498 -0
- package/dist/chunk-77MSMAUQ.cjs.map +1 -0
- package/dist/chunk-ABPEQ6RK.cjs +596 -0
- package/dist/chunk-ABPEQ6RK.cjs.map +1 -0
- package/dist/chunk-BWYGJKRB.js +95 -0
- package/dist/chunk-BWYGJKRB.js.map +1 -0
- package/dist/chunk-BZHG5IZ4.js +73 -0
- package/dist/chunk-BZHG5IZ4.js.map +1 -0
- package/dist/chunk-G7VZBCD6.cjs +35 -0
- package/dist/chunk-G7VZBCD6.cjs.map +1 -0
- package/dist/chunk-GVLK7YUU.cjs +30 -0
- package/dist/chunk-GVLK7YUU.cjs.map +1 -0
- package/dist/chunk-HCCXC7XG.js +205 -0
- package/dist/chunk-HCCXC7XG.js.map +1 -0
- package/dist/chunk-HE6T6FIX.cjs +203 -0
- package/dist/chunk-HE6T6FIX.cjs.map +1 -0
- package/dist/chunk-KIXWPOCO.cjs +100 -0
- package/dist/chunk-KIXWPOCO.cjs.map +1 -0
- package/dist/chunk-KVGNW3FC.js +87 -0
- package/dist/chunk-KVGNW3FC.js.map +1 -0
- package/dist/chunk-LITNXTTT.js +3 -0
- package/dist/chunk-LITNXTTT.js.map +1 -0
- package/dist/chunk-M4ANN7RL.js +114 -0
- package/dist/chunk-M4ANN7RL.js.map +1 -0
- package/dist/chunk-NC52UBR2.cjs +32 -0
- package/dist/chunk-NC52UBR2.cjs.map +1 -0
- package/dist/chunk-NHCNRQD3.cjs +212 -0
- package/dist/chunk-NHCNRQD3.cjs.map +1 -0
- package/dist/chunk-NZ72VDNY.cjs +4 -0
- package/dist/chunk-NZ72VDNY.cjs.map +1 -0
- package/dist/chunk-P6JUDYNO.js +57 -0
- package/dist/chunk-P6JUDYNO.js.map +1 -0
- package/dist/chunk-RJYY7BWX.js +1349 -0
- package/dist/chunk-RJYY7BWX.js.map +1 -0
- package/dist/chunk-TRI4V5BF.cjs +126 -0
- package/dist/chunk-TRI4V5BF.cjs.map +1 -0
- package/dist/chunk-UL33I6IS.js +139 -0
- package/dist/chunk-UL33I6IS.js.map +1 -0
- package/dist/chunk-URRW6M2C.cjs +61 -0
- package/dist/chunk-URRW6M2C.cjs.map +1 -0
- package/dist/chunk-UY3UYPBZ.cjs +77 -0
- package/dist/chunk-UY3UYPBZ.cjs.map +1 -0
- package/dist/chunk-W3253FGB.cjs +277 -0
- package/dist/chunk-W3253FGB.cjs.map +1 -0
- package/dist/chunk-W7LHZVQF.js +26 -0
- package/dist/chunk-W7LHZVQF.js.map +1 -0
- package/dist/chunk-WBWNM6LB.cjs +1360 -0
- package/dist/chunk-WBWNM6LB.cjs.map +1 -0
- package/dist/chunk-WFJ7L2RV.js +494 -0
- package/dist/chunk-WFJ7L2RV.js.map +1 -0
- package/dist/chunk-X4RMFFMR.js +28 -0
- package/dist/chunk-X4RMFFMR.js.map +1 -0
- package/dist/chunk-Y4Y2S7BM.cjs +92 -0
- package/dist/chunk-Y4Y2S7BM.cjs.map +1 -0
- package/dist/chunk-YLPNXZFI.cjs +143 -0
- package/dist/chunk-YLPNXZFI.cjs.map +1 -0
- package/dist/chunk-YTXEZ4SD.cjs +77 -0
- package/dist/chunk-YTXEZ4SD.cjs.map +1 -0
- package/dist/chunk-Z6ZWNWWR.js +30 -0
- package/dist/chunk-Z6ZWNWWR.js.map +1 -0
- package/dist/config.cjs +26 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +75 -0
- package/dist/config.d.ts +75 -0
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -0
- package/dist/db.cjs +233 -0
- package/dist/db.cjs.map +1 -0
- package/dist/db.d.cts +123 -0
- package/dist/db.d.ts +123 -0
- package/dist/db.js +228 -0
- package/dist/db.js.map +1 -0
- package/dist/decorators.cjs +67 -0
- package/dist/decorators.cjs.map +1 -0
- package/dist/decorators.d.cts +91 -0
- package/dist/decorators.d.ts +91 -0
- package/dist/decorators.js +65 -0
- package/dist/decorators.js.map +1 -0
- package/dist/event-subscriber.cjs +6 -0
- package/dist/event-subscriber.cjs.map +1 -0
- package/dist/event-subscriber.d.cts +116 -0
- package/dist/event-subscriber.d.ts +116 -0
- package/dist/event-subscriber.js +3 -0
- package/dist/event-subscriber.js.map +1 -0
- package/dist/event-testing.cjs +21 -0
- package/dist/event-testing.cjs.map +1 -0
- package/dist/event-testing.d.cts +110 -0
- package/dist/event-testing.d.ts +110 -0
- package/dist/event-testing.js +4 -0
- package/dist/event-testing.js.map +1 -0
- package/dist/event.cjs +30 -0
- package/dist/event.cjs.map +1 -0
- package/dist/event.d.cts +282 -0
- package/dist/event.d.ts +282 -0
- package/dist/event.js +13 -0
- package/dist/event.js.map +1 -0
- package/dist/exporters.cjs +17 -0
- package/dist/exporters.cjs.map +1 -0
- package/dist/exporters.d.cts +1 -0
- package/dist/exporters.d.ts +1 -0
- package/dist/exporters.js +4 -0
- package/dist/exporters.js.map +1 -0
- package/dist/functional.cjs +46 -0
- package/dist/functional.cjs.map +1 -0
- package/dist/functional.d.cts +478 -0
- package/dist/functional.d.ts +478 -0
- package/dist/functional.js +13 -0
- package/dist/functional.js.map +1 -0
- package/dist/http.cjs +189 -0
- package/dist/http.cjs.map +1 -0
- package/dist/http.d.cts +169 -0
- package/dist/http.d.ts +169 -0
- package/dist/http.js +184 -0
- package/dist/http.js.map +1 -0
- package/dist/index.cjs +333 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +758 -0
- package/dist/index.d.ts +758 -0
- package/dist/index.js +143 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation.cjs +182 -0
- package/dist/instrumentation.cjs.map +1 -0
- package/dist/instrumentation.d.cts +49 -0
- package/dist/instrumentation.d.ts +49 -0
- package/dist/instrumentation.js +179 -0
- package/dist/instrumentation.js.map +1 -0
- package/dist/logger.cjs +19 -0
- package/dist/logger.cjs.map +1 -0
- package/dist/logger.d.cts +146 -0
- package/dist/logger.d.ts +146 -0
- package/dist/logger.js +6 -0
- package/dist/logger.js.map +1 -0
- package/dist/metric-helpers.cjs +31 -0
- package/dist/metric-helpers.cjs.map +1 -0
- package/dist/metric-helpers.d.cts +13 -0
- package/dist/metric-helpers.d.ts +13 -0
- package/dist/metric-helpers.js +6 -0
- package/dist/metric-helpers.js.map +1 -0
- package/dist/metric-testing.cjs +21 -0
- package/dist/metric-testing.cjs.map +1 -0
- package/dist/metric-testing.d.cts +110 -0
- package/dist/metric-testing.d.ts +110 -0
- package/dist/metric-testing.js +4 -0
- package/dist/metric-testing.js.map +1 -0
- package/dist/metric.cjs +26 -0
- package/dist/metric.cjs.map +1 -0
- package/dist/metric.d.cts +240 -0
- package/dist/metric.d.ts +240 -0
- package/dist/metric.js +9 -0
- package/dist/metric.js.map +1 -0
- package/dist/processors.cjs +17 -0
- package/dist/processors.cjs.map +1 -0
- package/dist/processors.d.cts +1 -0
- package/dist/processors.d.ts +1 -0
- package/dist/processors.js +4 -0
- package/dist/processors.js.map +1 -0
- package/dist/sampling.cjs +40 -0
- package/dist/sampling.cjs.map +1 -0
- package/dist/sampling.d.cts +260 -0
- package/dist/sampling.d.ts +260 -0
- package/dist/sampling.js +7 -0
- package/dist/sampling.js.map +1 -0
- package/dist/semantic-helpers.cjs +35 -0
- package/dist/semantic-helpers.cjs.map +1 -0
- package/dist/semantic-helpers.d.cts +442 -0
- package/dist/semantic-helpers.d.ts +442 -0
- package/dist/semantic-helpers.js +14 -0
- package/dist/semantic-helpers.js.map +1 -0
- package/dist/tail-sampling-processor.cjs +13 -0
- package/dist/tail-sampling-processor.cjs.map +1 -0
- package/dist/tail-sampling-processor.d.cts +27 -0
- package/dist/tail-sampling-processor.d.ts +27 -0
- package/dist/tail-sampling-processor.js +4 -0
- package/dist/tail-sampling-processor.js.map +1 -0
- package/dist/testing.cjs +286 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +291 -0
- package/dist/testing.d.ts +291 -0
- package/dist/testing.js +263 -0
- package/dist/testing.js.map +1 -0
- package/dist/trace-context-DRZdUvVY.d.cts +181 -0
- package/dist/trace-context-DRZdUvVY.d.ts +181 -0
- package/dist/trace-helpers.cjs +54 -0
- package/dist/trace-helpers.cjs.map +1 -0
- package/dist/trace-helpers.d.cts +524 -0
- package/dist/trace-helpers.d.ts +524 -0
- package/dist/trace-helpers.js +5 -0
- package/dist/trace-helpers.js.map +1 -0
- package/dist/tracer-provider.cjs +21 -0
- package/dist/tracer-provider.cjs.map +1 -0
- package/dist/tracer-provider.d.cts +169 -0
- package/dist/tracer-provider.d.ts +169 -0
- package/dist/tracer-provider.js +4 -0
- package/dist/tracer-provider.js.map +1 -0
- package/package.json +280 -0
- package/src/baggage-span-processor.test.ts +202 -0
- package/src/baggage-span-processor.ts +98 -0
- package/src/circuit-breaker.test.ts +341 -0
- package/src/circuit-breaker.ts +184 -0
- package/src/config.test.ts +94 -0
- package/src/config.ts +169 -0
- package/src/db.test.ts +252 -0
- package/src/db.ts +447 -0
- package/src/decorators.test.ts +203 -0
- package/src/decorators.ts +188 -0
- package/src/env-config.test.ts +246 -0
- package/src/env-config.ts +158 -0
- package/src/event-queue.test.ts +222 -0
- package/src/event-queue.ts +203 -0
- package/src/event-subscriber.ts +136 -0
- package/src/event-testing.ts +197 -0
- package/src/event.test.ts +718 -0
- package/src/event.ts +556 -0
- package/src/exporters.ts +96 -0
- package/src/functional.test.ts +1059 -0
- package/src/functional.ts +2295 -0
- package/src/http.test.ts +487 -0
- package/src/http.ts +424 -0
- package/src/index.ts +158 -0
- package/src/init.customization.test.ts +210 -0
- package/src/init.integrations.test.ts +366 -0
- package/src/init.openllmetry.test.ts +282 -0
- package/src/init.protocol.test.ts +215 -0
- package/src/init.ts +1426 -0
- package/src/instrumentation.test.ts +108 -0
- package/src/instrumentation.ts +308 -0
- package/src/logger.test.ts +117 -0
- package/src/logger.ts +246 -0
- package/src/metric-helpers.ts +47 -0
- package/src/metric-testing.ts +197 -0
- package/src/metric.ts +434 -0
- package/src/metrics.test.ts +205 -0
- package/src/operation-context.ts +93 -0
- package/src/processors.ts +106 -0
- package/src/rate-limiter.test.ts +199 -0
- package/src/rate-limiter.ts +98 -0
- package/src/sampling.test.ts +513 -0
- package/src/sampling.ts +428 -0
- package/src/semantic-helpers.test.ts +311 -0
- package/src/semantic-helpers.ts +584 -0
- package/src/shutdown.test.ts +311 -0
- package/src/shutdown.ts +222 -0
- package/src/stub.integration.test.ts +361 -0
- package/src/tail-sampling-processor.test.ts +226 -0
- package/src/tail-sampling-processor.ts +51 -0
- package/src/testing.ts +670 -0
- package/src/trace-context.ts +470 -0
- package/src/trace-helpers.new.test.ts +278 -0
- package/src/trace-helpers.test.ts +242 -0
- package/src/trace-helpers.ts +690 -0
- package/src/tracer-provider.test.ts +183 -0
- package/src/tracer-provider.ts +266 -0
- package/src/track.test.ts +153 -0
- package/src/track.ts +120 -0
- package/src/validation.test.ts +306 -0
- package/src/validation.ts +239 -0
- package/src/variable-name-inference.test.ts +178 -0
- package/src/variable-name-inference.ts +242 -0
package/src/init.ts
ADDED
|
@@ -0,0 +1,1426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified initialization for autotel
|
|
3
|
+
*
|
|
4
|
+
* Single init() function with sensible defaults.
|
|
5
|
+
* Replaces initInstrumentation() and separate events config.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
9
|
+
import type { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
|
|
10
|
+
import {
|
|
11
|
+
BatchSpanProcessor,
|
|
12
|
+
type SpanProcessor,
|
|
13
|
+
SimpleSpanProcessor,
|
|
14
|
+
ConsoleSpanExporter,
|
|
15
|
+
} from '@opentelemetry/sdk-trace-base';
|
|
16
|
+
import type { SpanExporter } from '@opentelemetry/sdk-trace-base';
|
|
17
|
+
import {
|
|
18
|
+
resourceFromAttributes,
|
|
19
|
+
type Resource,
|
|
20
|
+
} from '@opentelemetry/resources';
|
|
21
|
+
import {
|
|
22
|
+
ATTR_SERVICE_NAME,
|
|
23
|
+
ATTR_SERVICE_VERSION,
|
|
24
|
+
} from '@opentelemetry/semantic-conventions';
|
|
25
|
+
import type { Sampler } from './sampling';
|
|
26
|
+
import { AdaptiveSampler } from './sampling';
|
|
27
|
+
import type { EventSubscriber } from './event-subscriber';
|
|
28
|
+
import type { Logger } from './logger';
|
|
29
|
+
import type { Attributes } from '@opentelemetry/api';
|
|
30
|
+
import type { ValidationConfig } from './validation';
|
|
31
|
+
import {
|
|
32
|
+
PeriodicExportingMetricReader,
|
|
33
|
+
type MetricReader,
|
|
34
|
+
} from '@opentelemetry/sdk-metrics';
|
|
35
|
+
import { OTLPMetricExporter as OTLPMetricExporterHTTP } from '@opentelemetry/exporter-metrics-otlp-http';
|
|
36
|
+
import { OTLPTraceExporter as OTLPTraceExporterHTTP } from '@opentelemetry/exporter-trace-otlp-http';
|
|
37
|
+
import type { PushMetricExporter } from '@opentelemetry/sdk-metrics';
|
|
38
|
+
import type { LogRecordProcessor } from '@opentelemetry/sdk-logs';
|
|
39
|
+
import { TailSamplingSpanProcessor } from './tail-sampling-processor';
|
|
40
|
+
import { BaggageSpanProcessor } from './baggage-span-processor';
|
|
41
|
+
import { resolveConfigFromEnv } from './env-config';
|
|
42
|
+
import { PinoInstrumentation } from '@opentelemetry/instrumentation-pino';
|
|
43
|
+
import { WinstonInstrumentation } from '@opentelemetry/instrumentation-winston';
|
|
44
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
|
|
45
|
+
|
|
46
|
+
// Type imports for exporters
|
|
47
|
+
type OTLPExporterConfig = {
|
|
48
|
+
url?: string;
|
|
49
|
+
headers?: Record<string, string>;
|
|
50
|
+
timeoutMillis?: number;
|
|
51
|
+
concurrencyLimit?: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Lazy-load gRPC exporters (optional peer dependencies)
|
|
55
|
+
let OTLPTraceExporterGRPC:
|
|
56
|
+
| (new (config: OTLPExporterConfig) => SpanExporter)
|
|
57
|
+
| undefined;
|
|
58
|
+
let OTLPMetricExporterGRPC:
|
|
59
|
+
| (new (config: OTLPExporterConfig) => PushMetricExporter)
|
|
60
|
+
| undefined;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Helper: Lazy-load gRPC trace exporter
|
|
64
|
+
*/
|
|
65
|
+
function loadGRPCTraceExporter(): new (
|
|
66
|
+
config: OTLPExporterConfig,
|
|
67
|
+
) => SpanExporter {
|
|
68
|
+
if (OTLPTraceExporterGRPC) return OTLPTraceExporterGRPC;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
// Dynamic import for optional peer dependency
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
73
|
+
const grpcModule = require('@opentelemetry/exporter-trace-otlp-grpc');
|
|
74
|
+
OTLPTraceExporterGRPC = grpcModule.OTLPTraceExporter as new (
|
|
75
|
+
config: OTLPExporterConfig,
|
|
76
|
+
) => SpanExporter;
|
|
77
|
+
return OTLPTraceExporterGRPC;
|
|
78
|
+
} catch {
|
|
79
|
+
throw new Error(
|
|
80
|
+
'gRPC trace exporter not found. Install with: pnpm add @opentelemetry/exporter-trace-otlp-grpc',
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Helper: Lazy-load gRPC metric exporter
|
|
87
|
+
*/
|
|
88
|
+
function loadGRPCMetricExporter(): new (
|
|
89
|
+
config: OTLPExporterConfig,
|
|
90
|
+
) => PushMetricExporter {
|
|
91
|
+
if (OTLPMetricExporterGRPC) return OTLPMetricExporterGRPC;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Dynamic import for optional peer dependency
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
96
|
+
const grpcModule = require('@opentelemetry/exporter-metrics-otlp-grpc');
|
|
97
|
+
OTLPMetricExporterGRPC = grpcModule.OTLPMetricExporter as new (
|
|
98
|
+
config: OTLPExporterConfig,
|
|
99
|
+
) => PushMetricExporter;
|
|
100
|
+
return OTLPMetricExporterGRPC;
|
|
101
|
+
} catch {
|
|
102
|
+
throw new Error(
|
|
103
|
+
'gRPC metric exporter not found. Install with: pnpm add @opentelemetry/exporter-metrics-otlp-grpc',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Helper: Create trace exporter based on protocol
|
|
110
|
+
*/
|
|
111
|
+
function createTraceExporter(
|
|
112
|
+
protocol: 'http' | 'grpc',
|
|
113
|
+
config: OTLPExporterConfig,
|
|
114
|
+
): SpanExporter {
|
|
115
|
+
if (protocol === 'grpc') {
|
|
116
|
+
const Exporter = loadGRPCTraceExporter();
|
|
117
|
+
return new Exporter(config);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Default: HTTP
|
|
121
|
+
return new OTLPTraceExporterHTTP(config);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Helper: Create metric exporter based on protocol
|
|
126
|
+
*/
|
|
127
|
+
function createMetricExporter(
|
|
128
|
+
protocol: 'http' | 'grpc',
|
|
129
|
+
config: OTLPExporterConfig,
|
|
130
|
+
): PushMetricExporter {
|
|
131
|
+
if (protocol === 'grpc') {
|
|
132
|
+
const Exporter = loadGRPCMetricExporter();
|
|
133
|
+
return new Exporter(config);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Default: HTTP
|
|
137
|
+
return new OTLPMetricExporterHTTP(config);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Helper: Resolve protocol from config and environment
|
|
142
|
+
*/
|
|
143
|
+
function resolveProtocol(configProtocol?: 'http' | 'grpc'): 'http' | 'grpc' {
|
|
144
|
+
// 1. Check config parameter (highest priority)
|
|
145
|
+
if (configProtocol === 'grpc' || configProtocol === 'http') {
|
|
146
|
+
return configProtocol;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 2. Check OTEL_EXPORTER_OTLP_PROTOCOL env var
|
|
150
|
+
const envProtocol = process.env.OTEL_EXPORTER_OTLP_PROTOCOL;
|
|
151
|
+
if (envProtocol === 'grpc') return 'grpc';
|
|
152
|
+
if (envProtocol === 'http/protobuf' || envProtocol === 'http') return 'http';
|
|
153
|
+
|
|
154
|
+
// 3. Default to HTTP
|
|
155
|
+
return 'http';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Helper: Adjust endpoint URL for protocol
|
|
160
|
+
* gRPC exporters don't need the /v1/traces or /v1/metrics path
|
|
161
|
+
* HTTP exporters need the full path
|
|
162
|
+
*/
|
|
163
|
+
function formatEndpointUrl(
|
|
164
|
+
endpoint: string,
|
|
165
|
+
signal: 'traces' | 'metrics',
|
|
166
|
+
protocol: 'http' | 'grpc',
|
|
167
|
+
): string {
|
|
168
|
+
if (protocol === 'grpc') {
|
|
169
|
+
// gRPC: strip any paths, return base endpoint
|
|
170
|
+
return endpoint.replace(/\/(v1\/)?(traces|metrics|logs)$/, '');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// HTTP: append signal path if not present
|
|
174
|
+
if (!endpoint.endsWith(`/v1/${signal}`)) {
|
|
175
|
+
return `${endpoint}/v1/${signal}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return endpoint;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Default silent logger (no-op) when user doesn't provide one
|
|
183
|
+
*/
|
|
184
|
+
const silentLogger: Logger = {
|
|
185
|
+
info: () => {},
|
|
186
|
+
warn: () => {},
|
|
187
|
+
error: () => {},
|
|
188
|
+
debug: () => {},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export interface AutotelConfig {
|
|
192
|
+
/** Service name (required) */
|
|
193
|
+
service: string;
|
|
194
|
+
|
|
195
|
+
/** Event subscribers - bring your own (PostHog, Mixpanel, etc.) */
|
|
196
|
+
subscribers?: EventSubscriber[];
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Additional OpenTelemetry instrumentations to register.
|
|
200
|
+
* Useful when you want HTTP/Prisma/etc auto instrumentation alongside
|
|
201
|
+
* the functional helpers.
|
|
202
|
+
*
|
|
203
|
+
* **Important:** If you need custom instrumentation configs (like `requireParentSpan: false`),
|
|
204
|
+
* use EITHER manual instrumentations OR integrations, not both for the same library.
|
|
205
|
+
* Manual instrumentations always take precedence over auto-instrumentations.
|
|
206
|
+
*
|
|
207
|
+
* @example Manual instrumentations with custom config
|
|
208
|
+
* ```typescript
|
|
209
|
+
* import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'
|
|
210
|
+
*
|
|
211
|
+
* init({
|
|
212
|
+
* service: 'my-app',
|
|
213
|
+
* integrations: false, // Disable auto-instrumentations
|
|
214
|
+
* instrumentations: [
|
|
215
|
+
* new MongoDBInstrumentation({
|
|
216
|
+
* requireParentSpan: false // Custom config
|
|
217
|
+
* })
|
|
218
|
+
* ]
|
|
219
|
+
* })
|
|
220
|
+
* ```
|
|
221
|
+
*
|
|
222
|
+
* @example Mix auto + manual (auto for most, manual for specific configs)
|
|
223
|
+
* ```typescript
|
|
224
|
+
* import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'
|
|
225
|
+
*
|
|
226
|
+
* init({
|
|
227
|
+
* service: 'my-app',
|
|
228
|
+
* integrations: ['http', 'express'], // Auto for these
|
|
229
|
+
* instrumentations: [
|
|
230
|
+
* new MongoDBInstrumentation({
|
|
231
|
+
* requireParentSpan: false // Manual config for MongoDB
|
|
232
|
+
* })
|
|
233
|
+
* ]
|
|
234
|
+
* })
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
instrumentations?: NodeSDKConfiguration['instrumentations'];
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Simple integration names for auto-instrumentation.
|
|
241
|
+
* Uses @opentelemetry/auto-instrumentations-node (peer dependency).
|
|
242
|
+
*
|
|
243
|
+
* **Important:** If you provide manual instrumentations for the same library,
|
|
244
|
+
* the manual config takes precedence and auto-instrumentation for that library is disabled.
|
|
245
|
+
*
|
|
246
|
+
* @example Enable all integrations (simple approach)
|
|
247
|
+
* ```typescript
|
|
248
|
+
* init({
|
|
249
|
+
* service: 'my-app',
|
|
250
|
+
* integrations: true // Enable all with defaults
|
|
251
|
+
* })
|
|
252
|
+
* ```
|
|
253
|
+
*
|
|
254
|
+
* @example Enable specific integrations
|
|
255
|
+
* ```typescript
|
|
256
|
+
* init({
|
|
257
|
+
* service: 'my-app',
|
|
258
|
+
* integrations: ['express', 'pino', 'http']
|
|
259
|
+
* })
|
|
260
|
+
* ```
|
|
261
|
+
*
|
|
262
|
+
* @example Configure specific integrations
|
|
263
|
+
* ```typescript
|
|
264
|
+
* init({
|
|
265
|
+
* service: 'my-app',
|
|
266
|
+
* integrations: {
|
|
267
|
+
* express: { enabled: true },
|
|
268
|
+
* pino: { enabled: true },
|
|
269
|
+
* http: { enabled: false }
|
|
270
|
+
* }
|
|
271
|
+
* })
|
|
272
|
+
* ```
|
|
273
|
+
*
|
|
274
|
+
* @example Manual config when you need custom settings
|
|
275
|
+
* ```typescript
|
|
276
|
+
* import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'
|
|
277
|
+
*
|
|
278
|
+
* init({
|
|
279
|
+
* service: 'my-app',
|
|
280
|
+
* integrations: false, // Use manual control
|
|
281
|
+
* instrumentations: [
|
|
282
|
+
* new MongoDBInstrumentation({
|
|
283
|
+
* requireParentSpan: false // Custom config not available with auto
|
|
284
|
+
* })
|
|
285
|
+
* ]
|
|
286
|
+
* })
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
integrations?: string[] | boolean | Record<string, { enabled?: boolean }>;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* OTLP endpoint for traces/metrics/logs
|
|
293
|
+
* Only used if you don't provide custom exporters/processors
|
|
294
|
+
* @default process.env.OTLP_ENDPOINT || 'http://localhost:4318'
|
|
295
|
+
*/
|
|
296
|
+
endpoint?: string;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Custom span processors for traces (supports multiple processors)
|
|
300
|
+
* Allows you to use any backend: Jaeger, Zipkin, Datadog, New Relic, etc.
|
|
301
|
+
* If not provided, defaults to OTLP with tail sampling
|
|
302
|
+
*
|
|
303
|
+
* @example Multiple processors
|
|
304
|
+
* ```typescript
|
|
305
|
+
* import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
|
|
306
|
+
* import { BatchSpanProcessor, SimpleSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base'
|
|
307
|
+
*
|
|
308
|
+
* init({
|
|
309
|
+
* service: 'my-app',
|
|
310
|
+
* spanProcessors: [
|
|
311
|
+
* new BatchSpanProcessor(new JaegerExporter()),
|
|
312
|
+
* new SimpleSpanProcessor(new ConsoleSpanExporter()) // Debug alongside production
|
|
313
|
+
* ]
|
|
314
|
+
* })
|
|
315
|
+
* ```
|
|
316
|
+
*
|
|
317
|
+
* @example Single processor
|
|
318
|
+
* ```typescript
|
|
319
|
+
* import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
320
|
+
*
|
|
321
|
+
* init({
|
|
322
|
+
* service: 'my-app',
|
|
323
|
+
* spanProcessors: [new SimpleSpanProcessor(new ConsoleSpanExporter())]
|
|
324
|
+
* })
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
spanProcessors?: SpanProcessor[];
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Custom span exporters for traces (alternative to spanProcessors, supports multiple exporters)
|
|
331
|
+
* Provide either spanProcessors OR spanExporters, not both
|
|
332
|
+
* Each exporter will be wrapped in TailSamplingSpanProcessor + BatchSpanProcessor
|
|
333
|
+
*
|
|
334
|
+
* @example Multiple exporters
|
|
335
|
+
* ```typescript
|
|
336
|
+
* import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
|
|
337
|
+
* import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
|
|
338
|
+
*
|
|
339
|
+
* init({
|
|
340
|
+
* service: 'my-app',
|
|
341
|
+
* spanExporters: [
|
|
342
|
+
* new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' }),
|
|
343
|
+
* new JaegerExporter() // Send to multiple backends simultaneously
|
|
344
|
+
* ]
|
|
345
|
+
* })
|
|
346
|
+
* ```
|
|
347
|
+
*
|
|
348
|
+
* @example Single exporter
|
|
349
|
+
* ```typescript
|
|
350
|
+
* import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
|
|
351
|
+
*
|
|
352
|
+
* init({
|
|
353
|
+
* service: 'my-app',
|
|
354
|
+
* spanExporters: [new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' })]
|
|
355
|
+
* })
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
spanExporters?: SpanExporter[];
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Custom metric readers (supports multiple readers)
|
|
362
|
+
* Allows sending metrics to multiple backends: OTLP, Prometheus, custom readers
|
|
363
|
+
* Defaults to OTLP metrics exporter when metrics are enabled.
|
|
364
|
+
*
|
|
365
|
+
* @example Multiple metric readers
|
|
366
|
+
* ```typescript
|
|
367
|
+
* import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
|
|
368
|
+
* import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
|
|
369
|
+
* import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'
|
|
370
|
+
*
|
|
371
|
+
* init({
|
|
372
|
+
* service: 'my-app',
|
|
373
|
+
* metricReaders: [
|
|
374
|
+
* new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter() }),
|
|
375
|
+
* new PrometheusExporter() // Export to multiple backends
|
|
376
|
+
* ]
|
|
377
|
+
* })
|
|
378
|
+
* ```
|
|
379
|
+
*/
|
|
380
|
+
metricReaders?: MetricReader[];
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Custom log record processors. When omitted, logs are not configured.
|
|
384
|
+
*/
|
|
385
|
+
logRecordProcessors?: LogRecordProcessor[];
|
|
386
|
+
|
|
387
|
+
/** Additional resource attributes to merge with defaults. */
|
|
388
|
+
resourceAttributes?: Attributes;
|
|
389
|
+
|
|
390
|
+
/** Provide a fully custom Resource to merge (advanced use case). */
|
|
391
|
+
resource?: Resource;
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Headers for default OTLP exporters. Accepts either an object map or
|
|
395
|
+
* a "key=value" comma separated string.
|
|
396
|
+
*/
|
|
397
|
+
otlpHeaders?: Record<string, string> | string;
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* OTLP protocol to use for traces, metrics, and logs
|
|
401
|
+
* - 'http': HTTP/protobuf (default, uses port 4318)
|
|
402
|
+
* - 'grpc': gRPC (uses port 4317)
|
|
403
|
+
*
|
|
404
|
+
* Can be overridden with OTEL_EXPORTER_OTLP_PROTOCOL env var.
|
|
405
|
+
*
|
|
406
|
+
* Note: gRPC exporters are optional peer dependencies. Install them with:
|
|
407
|
+
* ```bash
|
|
408
|
+
* pnpm add @opentelemetry/exporter-trace-otlp-grpc @opentelemetry/exporter-metrics-otlp-grpc
|
|
409
|
+
* ```
|
|
410
|
+
*
|
|
411
|
+
* @example HTTP (default)
|
|
412
|
+
* ```typescript
|
|
413
|
+
* init({
|
|
414
|
+
* service: 'my-app',
|
|
415
|
+
* protocol: 'http', // or omit (defaults to http)
|
|
416
|
+
* endpoint: 'http://localhost:4318'
|
|
417
|
+
* })
|
|
418
|
+
* ```
|
|
419
|
+
*
|
|
420
|
+
* @example gRPC
|
|
421
|
+
* ```typescript
|
|
422
|
+
* init({
|
|
423
|
+
* service: 'my-app',
|
|
424
|
+
* protocol: 'grpc',
|
|
425
|
+
* endpoint: 'grpc://localhost:4317'
|
|
426
|
+
* })
|
|
427
|
+
* ```
|
|
428
|
+
*
|
|
429
|
+
* @default 'http'
|
|
430
|
+
*/
|
|
431
|
+
protocol?: 'http' | 'grpc';
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Optional factory to build a customised NodeSDK instance from our defaults.
|
|
435
|
+
*/
|
|
436
|
+
sdkFactory?: (defaults: Partial<NodeSDKConfiguration>) => NodeSDK;
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Infrastructure metrics configuration
|
|
440
|
+
* - true: always enabled (default)
|
|
441
|
+
* - false: always disabled
|
|
442
|
+
* - 'auto': always enabled (same as true)
|
|
443
|
+
*
|
|
444
|
+
* Can be overridden with AUTOTELEMETRY_METRICS=on|off env var
|
|
445
|
+
*/
|
|
446
|
+
metrics?: boolean | 'auto';
|
|
447
|
+
|
|
448
|
+
/** Sampling strategy (default: AdaptiveSampler with 10% baseline) */
|
|
449
|
+
sampler?: Sampler;
|
|
450
|
+
|
|
451
|
+
/** Service version (default: auto-detect from package.json or '1.0.0') */
|
|
452
|
+
version?: string;
|
|
453
|
+
|
|
454
|
+
/** Environment (default: process.env.NODE_ENV || 'development') */
|
|
455
|
+
environment?: string;
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Logger instance for structured logging with automatic trace correlation
|
|
459
|
+
*
|
|
460
|
+
* **Recommended:** Bring your own Pino or Winston instance
|
|
461
|
+
*
|
|
462
|
+
* Autotel automatically instruments Pino and Winston loggers to:
|
|
463
|
+
* - Inject trace context (traceId, spanId) into every log record
|
|
464
|
+
* - Record errors in the active OpenTelemetry span
|
|
465
|
+
* - Bridge logs to the OpenTelemetry Logs API for OTLP export to Grafana, Datadog, etc.
|
|
466
|
+
*
|
|
467
|
+
* Supports any logger with 4 methods: info/warn/error/debug
|
|
468
|
+
* Default: silent logger (no-op)
|
|
469
|
+
*
|
|
470
|
+
* @example Using Pino (recommended)
|
|
471
|
+
* ```typescript
|
|
472
|
+
* import pino from 'pino' // npm install pino
|
|
473
|
+
* import { init } from 'autotel'
|
|
474
|
+
*
|
|
475
|
+
* const logger = pino({ level: 'info' })
|
|
476
|
+
* init({ service: 'my-app', logger })
|
|
477
|
+
*
|
|
478
|
+
* // Logs automatically include traceId/spanId and export via OTLP!
|
|
479
|
+
* logger.info('User created', { userId: '123' })
|
|
480
|
+
* ```
|
|
481
|
+
*
|
|
482
|
+
* @example Using Winston
|
|
483
|
+
* ```typescript
|
|
484
|
+
* import winston from 'winston' // npm install winston
|
|
485
|
+
* import { init } from 'autotel'
|
|
486
|
+
*
|
|
487
|
+
* const logger = winston.createLogger({
|
|
488
|
+
* level: 'info',
|
|
489
|
+
* format: winston.format.json()
|
|
490
|
+
* })
|
|
491
|
+
* init({ service: 'my-app', logger })
|
|
492
|
+
* ```
|
|
493
|
+
*
|
|
494
|
+
* @example Custom logger (any logger with 4 methods)
|
|
495
|
+
* ```typescript
|
|
496
|
+
* const logger = {
|
|
497
|
+
* info: (msg, extra) => console.log(msg, extra),
|
|
498
|
+
* warn: (msg, extra) => console.warn(msg, extra),
|
|
499
|
+
* error: (msg, err, extra) => console.error(msg, err, extra),
|
|
500
|
+
* debug: (msg, extra) => console.debug(msg, extra),
|
|
501
|
+
* }
|
|
502
|
+
* init({ service: 'my-app', logger })
|
|
503
|
+
* ```
|
|
504
|
+
*/
|
|
505
|
+
logger?: Logger;
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Automatically flush events queue when root spans end
|
|
509
|
+
* - true: Auto-flush on root span completion (default)
|
|
510
|
+
* - false: Use batching (events flush every 10 seconds automatically)
|
|
511
|
+
*
|
|
512
|
+
* Only flushes on root spans to avoid excessive network calls.
|
|
513
|
+
* Default is true for serverless/short-lived processes. Set to false
|
|
514
|
+
* for long-running services where batching is more efficient.
|
|
515
|
+
*/
|
|
516
|
+
autoFlushEvents?: boolean;
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Include OpenTelemetry span flushing in auto-flush (default: false)
|
|
520
|
+
*
|
|
521
|
+
* When enabled, spans are force-flushed along with events events on root
|
|
522
|
+
* span completion. This is useful for serverless/short-lived processes where
|
|
523
|
+
* spans may not export before the process ends.
|
|
524
|
+
*
|
|
525
|
+
* - true: Force-flush spans on root span completion (~50-200ms latency)
|
|
526
|
+
* - false: Spans export via normal batch processor (default behavior)
|
|
527
|
+
*
|
|
528
|
+
* Only applies when autoFlushEvents is also enabled.
|
|
529
|
+
*
|
|
530
|
+
* Note: For edge runtimes (Cloudflare Workers, Vercel Edge), use the
|
|
531
|
+
* 'autotel-edge' package instead, which handles this automatically.
|
|
532
|
+
*
|
|
533
|
+
* @example Serverless with auto-flush
|
|
534
|
+
* ```typescript
|
|
535
|
+
* init({
|
|
536
|
+
* service: 'my-lambda',
|
|
537
|
+
* autoFlushEvents: true,
|
|
538
|
+
* autoFlush: true, // Force-flush spans
|
|
539
|
+
* });
|
|
540
|
+
* ```
|
|
541
|
+
*/
|
|
542
|
+
autoFlush?: boolean;
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Automatically copy baggage entries to span attributes
|
|
546
|
+
*
|
|
547
|
+
* When enabled, all baggage entries are automatically added as span attributes,
|
|
548
|
+
* making them visible in trace UIs (Jaeger, Grafana, DataDog, etc.) without
|
|
549
|
+
* manually calling ctx.setAttribute() for each entry.
|
|
550
|
+
*
|
|
551
|
+
* - `true`: adds baggage with 'baggage.' prefix (e.g. baggage.tenant.id)
|
|
552
|
+
* - `string`: uses custom prefix (e.g. 'ctx' → ctx.tenant.id, '' → tenant.id)
|
|
553
|
+
* - `false` or omit: disabled (default)
|
|
554
|
+
*
|
|
555
|
+
* @default false
|
|
556
|
+
*
|
|
557
|
+
* @example Enable with default prefix
|
|
558
|
+
* ```typescript
|
|
559
|
+
* init({
|
|
560
|
+
* service: 'my-app',
|
|
561
|
+
* baggage: true
|
|
562
|
+
* });
|
|
563
|
+
*
|
|
564
|
+
* // Now baggage automatically appears as span attributes
|
|
565
|
+
* await withBaggage({
|
|
566
|
+
* baggage: { 'tenant.id': 't1', 'user.id': 'u1' },
|
|
567
|
+
* fn: async () => {
|
|
568
|
+
* // Span has baggage.tenant.id and baggage.user.id attributes!
|
|
569
|
+
* }
|
|
570
|
+
* });
|
|
571
|
+
* ```
|
|
572
|
+
*
|
|
573
|
+
* @example Custom prefix
|
|
574
|
+
* ```typescript
|
|
575
|
+
* init({
|
|
576
|
+
* service: 'my-app',
|
|
577
|
+
* baggage: 'ctx' // Uses 'ctx.' prefix
|
|
578
|
+
* });
|
|
579
|
+
* // Creates attributes: ctx.tenant.id, ctx.user.id
|
|
580
|
+
* ```
|
|
581
|
+
*
|
|
582
|
+
* @example No prefix
|
|
583
|
+
* ```typescript
|
|
584
|
+
* init({
|
|
585
|
+
* service: 'my-app',
|
|
586
|
+
* baggage: '' // No prefix
|
|
587
|
+
* });
|
|
588
|
+
* // Creates attributes: tenant.id, user.id
|
|
589
|
+
* ```
|
|
590
|
+
*/
|
|
591
|
+
baggage?: boolean | string;
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Validation configuration for events events
|
|
595
|
+
* - Override default sensitive field patterns for redaction
|
|
596
|
+
* - Customize max lengths, nesting depth, etc.
|
|
597
|
+
*
|
|
598
|
+
* @example Disable redaction for development
|
|
599
|
+
* ```typescript
|
|
600
|
+
* init({
|
|
601
|
+
* service: 'my-app',
|
|
602
|
+
* validation: {
|
|
603
|
+
* sensitivePatterns: [] // Disable all redaction
|
|
604
|
+
* }
|
|
605
|
+
* })
|
|
606
|
+
* ```
|
|
607
|
+
*
|
|
608
|
+
* @example Add custom patterns
|
|
609
|
+
* ```typescript
|
|
610
|
+
* init({
|
|
611
|
+
* service: 'my-app',
|
|
612
|
+
* validation: {
|
|
613
|
+
* sensitivePatterns: [
|
|
614
|
+
* /password/i,
|
|
615
|
+
* /apiKey/i,
|
|
616
|
+
* /customSecret/i // Your custom pattern
|
|
617
|
+
* ]
|
|
618
|
+
* }
|
|
619
|
+
* })
|
|
620
|
+
* ```
|
|
621
|
+
*/
|
|
622
|
+
validation?: Partial<ValidationConfig>;
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Debug mode for local span inspection.
|
|
626
|
+
* Enables console output to help you see spans as they're created.
|
|
627
|
+
*
|
|
628
|
+
* When true: Outputs spans to console AND sends to backend (if endpoint/exporter configured)
|
|
629
|
+
* When false/undefined: Sends to backend only (default behavior)
|
|
630
|
+
*
|
|
631
|
+
* Perfect for progressive development:
|
|
632
|
+
* - Start with debug: true (no endpoint) → console-only, see traces immediately
|
|
633
|
+
* - Add endpoint later → console + backend, verify before choosing provider
|
|
634
|
+
* - Remove debug in production → backend only, clean production config
|
|
635
|
+
*
|
|
636
|
+
* Can be overridden with AUTOLEMETRY_DEBUG environment variable.
|
|
637
|
+
*
|
|
638
|
+
* @example Getting started - see spans immediately
|
|
639
|
+
* ```typescript
|
|
640
|
+
* init({
|
|
641
|
+
* service: 'my-app',
|
|
642
|
+
* debug: true // No endpoint yet - console only!
|
|
643
|
+
* })
|
|
644
|
+
* ```
|
|
645
|
+
*
|
|
646
|
+
* @example Testing with local collector
|
|
647
|
+
* ```typescript
|
|
648
|
+
* init({
|
|
649
|
+
* service: 'my-app',
|
|
650
|
+
* debug: true,
|
|
651
|
+
* endpoint: 'http://localhost:4318' // Console + OTLP
|
|
652
|
+
* })
|
|
653
|
+
* ```
|
|
654
|
+
*
|
|
655
|
+
* @example Production debugging
|
|
656
|
+
* ```typescript
|
|
657
|
+
* init({
|
|
658
|
+
* service: 'my-app',
|
|
659
|
+
* debug: true, // See what's being sent
|
|
660
|
+
* endpoint: 'https://api.honeycomb.io'
|
|
661
|
+
* })
|
|
662
|
+
* ```
|
|
663
|
+
*
|
|
664
|
+
* @example Environment variable
|
|
665
|
+
* ```bash
|
|
666
|
+
* AUTOLEMETRY_DEBUG=true node server.js
|
|
667
|
+
* ```
|
|
668
|
+
*/
|
|
669
|
+
debug?: boolean;
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* OpenLLMetry integration for LLM observability.
|
|
673
|
+
* Requires @traceloop/node-server-sdk as an optional peer dependency.
|
|
674
|
+
*
|
|
675
|
+
* @example Enable OpenLLMetry with default settings
|
|
676
|
+
* ```typescript
|
|
677
|
+
* init({
|
|
678
|
+
* service: 'my-app',
|
|
679
|
+
* openllmetry: { enabled: true }
|
|
680
|
+
* })
|
|
681
|
+
* ```
|
|
682
|
+
*
|
|
683
|
+
* @example Enable with custom options
|
|
684
|
+
* ```typescript
|
|
685
|
+
* init({
|
|
686
|
+
* service: 'my-app',
|
|
687
|
+
* openllmetry: {
|
|
688
|
+
* enabled: true,
|
|
689
|
+
* options: {
|
|
690
|
+
* disableBatch: process.env.NODE_ENV !== 'production',
|
|
691
|
+
* apiKey: process.env.TRACELOOP_API_KEY
|
|
692
|
+
* }
|
|
693
|
+
* }
|
|
694
|
+
* })
|
|
695
|
+
* ```
|
|
696
|
+
*/
|
|
697
|
+
openllmetry?: {
|
|
698
|
+
enabled: boolean;
|
|
699
|
+
options?: Record<string, unknown>;
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Internal state
|
|
704
|
+
let initialized = false;
|
|
705
|
+
let config: AutotelConfig | null = null;
|
|
706
|
+
let sdk: NodeSDK | null = null;
|
|
707
|
+
let warnedOnce = false;
|
|
708
|
+
let logger: Logger = silentLogger;
|
|
709
|
+
let validationConfig: Partial<ValidationConfig> | null = null;
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Resolve metrics flag with env var override support
|
|
713
|
+
*/
|
|
714
|
+
export function resolveMetricsFlag(
|
|
715
|
+
configFlag: boolean | 'auto' = 'auto',
|
|
716
|
+
): boolean {
|
|
717
|
+
// 1. Check env var override (highest priority)
|
|
718
|
+
const envFlag = process.env.AUTOTELEMETRY_METRICS;
|
|
719
|
+
if (envFlag === 'on' || envFlag === 'true') return true;
|
|
720
|
+
if (envFlag === 'off' || envFlag === 'false') return false;
|
|
721
|
+
|
|
722
|
+
// 2. Check config flag
|
|
723
|
+
if (configFlag === true) return true;
|
|
724
|
+
if (configFlag === false) return false;
|
|
725
|
+
|
|
726
|
+
// 3. Default: enabled in all environments (simpler)
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Resolve debug flag with env var override support
|
|
732
|
+
*/
|
|
733
|
+
export function resolveDebugFlag(configFlag?: boolean): boolean {
|
|
734
|
+
// 1. Check env var override (highest priority)
|
|
735
|
+
const envFlag = process.env.AUTOLEMETRY_DEBUG;
|
|
736
|
+
if (envFlag === 'true' || envFlag === '1') return true;
|
|
737
|
+
if (envFlag === 'false' || envFlag === '0') return false;
|
|
738
|
+
|
|
739
|
+
// 2. Return config flag (defaults to false)
|
|
740
|
+
return configFlag ?? false;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function normalizeOtlpHeaders(
|
|
744
|
+
headers?: Record<string, string> | string,
|
|
745
|
+
): Record<string, string> | undefined {
|
|
746
|
+
if (!headers) return undefined;
|
|
747
|
+
if (typeof headers !== 'string') return headers;
|
|
748
|
+
|
|
749
|
+
const parsed: Record<string, string> = {};
|
|
750
|
+
for (const pair of headers.split(',')) {
|
|
751
|
+
const [key, ...valueParts] = pair.split('=');
|
|
752
|
+
if (!key || valueParts.length === 0) continue;
|
|
753
|
+
parsed[key.trim()] = valueParts.join('=').trim();
|
|
754
|
+
}
|
|
755
|
+
return parsed;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Initialize autotel - Write Once, Observe Everywhere
|
|
760
|
+
*
|
|
761
|
+
* Follows OpenTelemetry standards: opinionated defaults with full flexibility
|
|
762
|
+
* Idempotent: multiple calls are safe, last one wins
|
|
763
|
+
*
|
|
764
|
+
* @example Minimal setup (OTLP default)
|
|
765
|
+
* ```typescript
|
|
766
|
+
* init({ service: 'my-app' })
|
|
767
|
+
* ```
|
|
768
|
+
*
|
|
769
|
+
* @example With events (observe in PostHog, Mixpanel, etc.)
|
|
770
|
+
* ```typescript
|
|
771
|
+
* import { PostHogSubscriber } from 'autotel-subscribers/posthog';
|
|
772
|
+
*
|
|
773
|
+
* init({
|
|
774
|
+
* service: 'my-app',
|
|
775
|
+
* subscribers: [new PostHogSubscriber({ apiKey: '...' })]
|
|
776
|
+
* })
|
|
777
|
+
* ```
|
|
778
|
+
*
|
|
779
|
+
* @example Observe in Jaeger
|
|
780
|
+
* ```typescript
|
|
781
|
+
* import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
|
|
782
|
+
*
|
|
783
|
+
* init({
|
|
784
|
+
* service: 'my-app',
|
|
785
|
+
* spanExporter: new JaegerExporter({ endpoint: 'http://localhost:14268/api/traces' })
|
|
786
|
+
* })
|
|
787
|
+
* ```
|
|
788
|
+
*
|
|
789
|
+
* @example Observe in Zipkin
|
|
790
|
+
* ```typescript
|
|
791
|
+
* import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
|
|
792
|
+
*
|
|
793
|
+
* init({
|
|
794
|
+
* service: 'my-app',
|
|
795
|
+
* spanExporter: new ZipkinExporter({ url: 'http://localhost:9411/api/v2/spans' })
|
|
796
|
+
* })
|
|
797
|
+
* ```
|
|
798
|
+
*
|
|
799
|
+
* @example Observe in Datadog
|
|
800
|
+
* ```typescript
|
|
801
|
+
* import { DatadogSpanProcessor } from '@opentelemetry/exporter-datadog'
|
|
802
|
+
*
|
|
803
|
+
* init({
|
|
804
|
+
* service: 'my-app',
|
|
805
|
+
* spanProcessor: new DatadogSpanProcessor({ ... })
|
|
806
|
+
* })
|
|
807
|
+
* ```
|
|
808
|
+
*
|
|
809
|
+
* @example Console output (dev)
|
|
810
|
+
* ```typescript
|
|
811
|
+
* import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
812
|
+
*
|
|
813
|
+
* init({
|
|
814
|
+
* service: 'my-app',
|
|
815
|
+
* spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter())
|
|
816
|
+
* })
|
|
817
|
+
* ```
|
|
818
|
+
*/
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Auto-detect logger type and return appropriate instrumentation
|
|
822
|
+
* Detects Pino and Winston loggers based on their unique properties
|
|
823
|
+
*/
|
|
824
|
+
function createLoggerInstrumentation(
|
|
825
|
+
logger: Logger,
|
|
826
|
+
): PinoInstrumentation | WinstonInstrumentation | null {
|
|
827
|
+
// Type guard: check for Pino-specific properties
|
|
828
|
+
// Pino has 'child' and 'bindings' methods
|
|
829
|
+
if (
|
|
830
|
+
'child' in logger &&
|
|
831
|
+
'bindings' in logger &&
|
|
832
|
+
typeof logger.child === 'function'
|
|
833
|
+
) {
|
|
834
|
+
return new PinoInstrumentation();
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Type guard: check for Winston-specific properties
|
|
838
|
+
// Winston has 'transports' array or 'defaultMeta' property
|
|
839
|
+
if ('transports' in logger || 'defaultMeta' in logger) {
|
|
840
|
+
return new WinstonInstrumentation();
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Unknown logger type - no instrumentation
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
export function init(cfg: AutotelConfig): void {
|
|
848
|
+
// Resolve environment variables (standard OTEL env vars)
|
|
849
|
+
const envConfig = resolveConfigFromEnv();
|
|
850
|
+
|
|
851
|
+
// Merge configs: explicit config > env vars > defaults
|
|
852
|
+
// Note: We merge envConfig first, then cfg overrides it
|
|
853
|
+
const mergedConfig: AutotelConfig = {
|
|
854
|
+
...envConfig,
|
|
855
|
+
...cfg,
|
|
856
|
+
// Deep merge for resourceAttributes
|
|
857
|
+
resourceAttributes: {
|
|
858
|
+
...envConfig.resourceAttributes,
|
|
859
|
+
...cfg.resourceAttributes,
|
|
860
|
+
},
|
|
861
|
+
// Handle otlpHeaders merge (can be string or object)
|
|
862
|
+
otlpHeaders:
|
|
863
|
+
cfg.otlpHeaders === undefined
|
|
864
|
+
? envConfig.otlpHeaders === undefined
|
|
865
|
+
? undefined
|
|
866
|
+
: envConfig.otlpHeaders
|
|
867
|
+
: cfg.otlpHeaders,
|
|
868
|
+
} as AutotelConfig;
|
|
869
|
+
|
|
870
|
+
// Set logger (use provided or default to silent)
|
|
871
|
+
logger = mergedConfig.logger || silentLogger;
|
|
872
|
+
|
|
873
|
+
// Warn if re-initializing (same behavior in all environments)
|
|
874
|
+
if (initialized) {
|
|
875
|
+
logger.warn(
|
|
876
|
+
'[autotel] init() called again - last config wins. This may cause unexpected behavior.',
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
config = mergedConfig;
|
|
881
|
+
validationConfig = mergedConfig.validation || null;
|
|
882
|
+
|
|
883
|
+
// Initialize OpenTelemetry
|
|
884
|
+
// Only use endpoint if explicitly configured (no default fallback)
|
|
885
|
+
const endpoint = mergedConfig.endpoint;
|
|
886
|
+
const otlpHeaders = normalizeOtlpHeaders(mergedConfig.otlpHeaders);
|
|
887
|
+
const version = mergedConfig.version || detectVersion();
|
|
888
|
+
const environment =
|
|
889
|
+
mergedConfig.environment || process.env.NODE_ENV || 'development';
|
|
890
|
+
const metricsEnabled = resolveMetricsFlag(mergedConfig.metrics);
|
|
891
|
+
|
|
892
|
+
// Detect hostname for proper Datadog correlation and Service Catalog discovery
|
|
893
|
+
const hostname = detectHostname();
|
|
894
|
+
|
|
895
|
+
let resource = resourceFromAttributes({
|
|
896
|
+
[ATTR_SERVICE_NAME]: cfg.service,
|
|
897
|
+
[ATTR_SERVICE_VERSION]: version,
|
|
898
|
+
// Support both old and new OpenTelemetry semantic conventions for environment
|
|
899
|
+
'deployment.environment': environment, // Deprecated but widely supported
|
|
900
|
+
'deployment.environment.name': environment, // OTel v1.27.0+ standard
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// Add hostname attributes for Datadog Service Catalog and infrastructure correlation
|
|
904
|
+
if (hostname) {
|
|
905
|
+
resource = resource.merge(
|
|
906
|
+
resourceFromAttributes({
|
|
907
|
+
'host.name': hostname, // OpenTelemetry standard
|
|
908
|
+
'datadog.host.name': hostname, // Datadog-specific, highest priority for Datadog
|
|
909
|
+
}),
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (cfg.resource) {
|
|
914
|
+
resource = resource.merge(cfg.resource);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (cfg.resourceAttributes) {
|
|
918
|
+
resource = resource.merge(resourceFromAttributes(cfg.resourceAttributes));
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Resolve OTLP protocol (http or grpc)
|
|
922
|
+
const protocol = resolveProtocol(cfg.protocol);
|
|
923
|
+
|
|
924
|
+
// Build array of span processors (supports multiple)
|
|
925
|
+
const spanProcessors: SpanProcessor[] = [];
|
|
926
|
+
|
|
927
|
+
if (cfg.spanProcessors && cfg.spanProcessors.length > 0) {
|
|
928
|
+
// User provided custom processors (full control)
|
|
929
|
+
spanProcessors.push(...cfg.spanProcessors);
|
|
930
|
+
} else if (cfg.spanExporters && cfg.spanExporters.length > 0) {
|
|
931
|
+
// User provided custom exporters (wrap each with tail sampling)
|
|
932
|
+
for (const exporter of cfg.spanExporters) {
|
|
933
|
+
spanProcessors.push(
|
|
934
|
+
new TailSamplingSpanProcessor(new BatchSpanProcessor(exporter)),
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
} else if (endpoint) {
|
|
938
|
+
// Default: OTLP with tail sampling (only if endpoint is configured)
|
|
939
|
+
const traceExporter = createTraceExporter(protocol, {
|
|
940
|
+
url: formatEndpointUrl(endpoint, 'traces', protocol),
|
|
941
|
+
headers: otlpHeaders,
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
spanProcessors.push(
|
|
945
|
+
new TailSamplingSpanProcessor(new BatchSpanProcessor(traceExporter)),
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
// If no endpoint and no custom processors/exporters, array remains empty
|
|
949
|
+
// SDK will still work but won't export traces
|
|
950
|
+
|
|
951
|
+
// Add baggage span processor if enabled
|
|
952
|
+
if (cfg.baggage) {
|
|
953
|
+
const prefix =
|
|
954
|
+
typeof cfg.baggage === 'string'
|
|
955
|
+
? cfg.baggage
|
|
956
|
+
? `${cfg.baggage}.`
|
|
957
|
+
: ''
|
|
958
|
+
: 'baggage.';
|
|
959
|
+
spanProcessors.push(new BaggageSpanProcessor({ prefix }));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Apply debug mode configuration
|
|
963
|
+
const debugMode = resolveDebugFlag(cfg.debug);
|
|
964
|
+
|
|
965
|
+
if (debugMode) {
|
|
966
|
+
// Debug enabled: add console processor
|
|
967
|
+
spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter()));
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Build array of metric readers (supports multiple)
|
|
971
|
+
const metricReaders: MetricReader[] = [];
|
|
972
|
+
|
|
973
|
+
if (cfg.metricReaders && cfg.metricReaders.length > 0) {
|
|
974
|
+
// User provided custom metric readers
|
|
975
|
+
metricReaders.push(...cfg.metricReaders);
|
|
976
|
+
} else if (metricsEnabled && endpoint) {
|
|
977
|
+
// Default: OTLP metrics exporter (only if endpoint is configured)
|
|
978
|
+
const metricExporter = createMetricExporter(protocol, {
|
|
979
|
+
url: formatEndpointUrl(endpoint, 'metrics', protocol),
|
|
980
|
+
headers: otlpHeaders,
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
metricReaders.push(
|
|
984
|
+
new PeriodicExportingMetricReader({
|
|
985
|
+
exporter: metricExporter,
|
|
986
|
+
}),
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
let logRecordProcessors: LogRecordProcessor[] | undefined;
|
|
991
|
+
if (cfg.logRecordProcessors && cfg.logRecordProcessors.length > 0) {
|
|
992
|
+
logRecordProcessors = [...cfg.logRecordProcessors];
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Handle instrumentations: merge manual instrumentations with auto-integrations
|
|
996
|
+
let finalInstrumentations: NodeSDKConfiguration['instrumentations'] =
|
|
997
|
+
cfg.instrumentations ? [...cfg.instrumentations] : [];
|
|
998
|
+
|
|
999
|
+
// Auto-enable logger instrumentation if a logger is provided
|
|
1000
|
+
if (cfg.logger) {
|
|
1001
|
+
const loggerInstrumentation = createLoggerInstrumentation(cfg.logger);
|
|
1002
|
+
if (loggerInstrumentation) {
|
|
1003
|
+
finalInstrumentations = [...finalInstrumentations, loggerInstrumentation];
|
|
1004
|
+
logger.debug(
|
|
1005
|
+
`[autotel] Auto-enabled ${loggerInstrumentation.constructor.name} for logger`,
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (cfg.integrations !== undefined) {
|
|
1011
|
+
try {
|
|
1012
|
+
// Detect manual instrumentations to avoid conflicts
|
|
1013
|
+
const manualInstrumentationNames = getInstrumentationNames(
|
|
1014
|
+
cfg.instrumentations ?? [],
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
// Warn if both integrations and manual instrumentations are provided
|
|
1018
|
+
if (
|
|
1019
|
+
manualInstrumentationNames.size > 0 &&
|
|
1020
|
+
cfg.integrations !== false &&
|
|
1021
|
+
cfg.integrations !== undefined
|
|
1022
|
+
) {
|
|
1023
|
+
const manualNames = [...manualInstrumentationNames].join(', ');
|
|
1024
|
+
logger.info(
|
|
1025
|
+
`[autotel] Detected manual instrumentations (${manualNames}). ` +
|
|
1026
|
+
'These will take precedence over auto-instrumentations. ' +
|
|
1027
|
+
'Tip: Set integrations:false if you want full manual control, or remove manual configs to use auto-instrumentations.',
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const autoInstrumentations = getAutoInstrumentations(
|
|
1032
|
+
cfg.integrations,
|
|
1033
|
+
manualInstrumentationNames,
|
|
1034
|
+
);
|
|
1035
|
+
if (autoInstrumentations && autoInstrumentations.length > 0) {
|
|
1036
|
+
// Cast to proper type - getNodeAutoInstrumentations returns the correct type
|
|
1037
|
+
finalInstrumentations = [
|
|
1038
|
+
...finalInstrumentations,
|
|
1039
|
+
...(autoInstrumentations as NodeSDKConfiguration['instrumentations']),
|
|
1040
|
+
];
|
|
1041
|
+
}
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
logger.warn(
|
|
1044
|
+
`[autotel] Failed to configure auto-instrumentations: ${error instanceof Error ? error.message : String(error)}`,
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const sdkOptions: Partial<NodeSDKConfiguration> = {
|
|
1050
|
+
resource,
|
|
1051
|
+
instrumentations: finalInstrumentations,
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
if (spanProcessors.length > 0) {
|
|
1055
|
+
sdkOptions.spanProcessors = spanProcessors;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (metricReaders.length > 0) {
|
|
1059
|
+
sdkOptions.metricReaders = metricReaders;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (logRecordProcessors && logRecordProcessors.length > 0) {
|
|
1063
|
+
sdkOptions.logRecordProcessors = logRecordProcessors;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
sdk = cfg.sdkFactory ? cfg.sdkFactory(sdkOptions) : new NodeSDK(sdkOptions);
|
|
1067
|
+
|
|
1068
|
+
if (!sdk) {
|
|
1069
|
+
throw new Error('[autotel] sdkFactory must return a NodeSDK instance');
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
sdk.start();
|
|
1073
|
+
|
|
1074
|
+
// Initialize OpenLLMetry if enabled (after SDK starts to reuse tracer provider)
|
|
1075
|
+
if (cfg.openllmetry?.enabled) {
|
|
1076
|
+
// Try synchronous initialization first (for require-based modules)
|
|
1077
|
+
let initializedSync = false;
|
|
1078
|
+
try {
|
|
1079
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1080
|
+
const traceloop = require('@traceloop/node-server-sdk');
|
|
1081
|
+
const initOptions: Record<string, unknown> = {
|
|
1082
|
+
...cfg.openllmetry.options,
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
// Reuse autotel's tracer provider
|
|
1086
|
+
try {
|
|
1087
|
+
// Type assertion needed as getTracerProvider is not in the public NodeSDK interface
|
|
1088
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1089
|
+
const tracerProvider = (sdk as any).getTracerProvider();
|
|
1090
|
+
initOptions.tracerProvider = tracerProvider;
|
|
1091
|
+
} catch {
|
|
1092
|
+
// Ignore if tracer provider not available
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if (typeof traceloop.initialize === 'function') {
|
|
1096
|
+
traceloop.initialize(initOptions);
|
|
1097
|
+
logger.info('[autotel] OpenLLMetry initialized successfully');
|
|
1098
|
+
initializedSync = true;
|
|
1099
|
+
}
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
// If require fails, try async import (for ESM modules or when module not found)
|
|
1102
|
+
if (
|
|
1103
|
+
error instanceof Error &&
|
|
1104
|
+
(error.message.includes('Cannot find module') ||
|
|
1105
|
+
error.message.includes('Module not found') ||
|
|
1106
|
+
error.message.includes('Cannot resolve module') ||
|
|
1107
|
+
error.message.includes('Dynamic require'))
|
|
1108
|
+
) {
|
|
1109
|
+
// Try async import as fallback - this will work with ESM/tsx and mocks in tests
|
|
1110
|
+
initializeOpenLLMetry(
|
|
1111
|
+
cfg.openllmetry.options,
|
|
1112
|
+
sdk,
|
|
1113
|
+
cfg.spanExporters?.[0], // Pass first exporter if available
|
|
1114
|
+
).catch((error_) => {
|
|
1115
|
+
logger.warn(
|
|
1116
|
+
`[autotel] OpenLLMetry initialization error: ${error_ instanceof Error ? error_.message : String(error_)}`,
|
|
1117
|
+
);
|
|
1118
|
+
});
|
|
1119
|
+
} else if (!initializedSync) {
|
|
1120
|
+
logger.warn(
|
|
1121
|
+
`[autotel] Failed to initialize OpenLLMetry: ${error instanceof Error ? error.message : String(error)}`,
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
initialized = true;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Initialize OpenLLMetry integration
|
|
1132
|
+
* Dynamically imports @traceloop/node-server-sdk and initializes it
|
|
1133
|
+
* Returns a promise but can be called without awaiting (fire-and-forget)
|
|
1134
|
+
*/
|
|
1135
|
+
async function initializeOpenLLMetry(
|
|
1136
|
+
options?: Record<string, unknown>,
|
|
1137
|
+
sdkInstance?: NodeSDK,
|
|
1138
|
+
spanExporter?: SpanExporter,
|
|
1139
|
+
): Promise<void> {
|
|
1140
|
+
try {
|
|
1141
|
+
// Try synchronous require first (for testing/mocking), then fall back to dynamic import
|
|
1142
|
+
let traceloop: {
|
|
1143
|
+
initialize?: (options?: Record<string, unknown>) => void;
|
|
1144
|
+
instrumentations?: unknown[];
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
try {
|
|
1148
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1149
|
+
traceloop = require('@traceloop/node-server-sdk');
|
|
1150
|
+
} catch {
|
|
1151
|
+
// Fall back to dynamic import if require fails (ESM modules)
|
|
1152
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1153
|
+
// @ts-ignore - optional peer dependency
|
|
1154
|
+
traceloop = await import('@traceloop/node-server-sdk');
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Prepare initialization options
|
|
1158
|
+
const initOptions: Record<string, unknown> = {
|
|
1159
|
+
...options,
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
// Pass span exporter to OpenLLMetry if provided
|
|
1163
|
+
// This ensures OpenLLMetry uses the same exporter as autotel
|
|
1164
|
+
if (spanExporter) {
|
|
1165
|
+
initOptions.exporter = spanExporter;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Reuse autotel's tracer provider if SDK is available
|
|
1169
|
+
if (sdkInstance) {
|
|
1170
|
+
try {
|
|
1171
|
+
// Type assertion needed as getTracerProvider is not in the public NodeSDK interface
|
|
1172
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1173
|
+
const tracerProvider = (sdkInstance as any).getTracerProvider();
|
|
1174
|
+
initOptions.tracerProvider = tracerProvider;
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
logger.debug(
|
|
1177
|
+
`[autotel] Could not get tracer provider for OpenLLMetry: ${error instanceof Error ? error.message : String(error)}`,
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Initialize OpenLLMetry
|
|
1183
|
+
if (typeof traceloop.initialize === 'function') {
|
|
1184
|
+
traceloop.initialize(initOptions);
|
|
1185
|
+
logger.info('[autotel] OpenLLMetry initialized successfully');
|
|
1186
|
+
} else {
|
|
1187
|
+
logger.warn(
|
|
1188
|
+
'[autotel] OpenLLMetry initialize function not found. Check @traceloop/node-server-sdk version.',
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
// Gracefully handle missing dependency
|
|
1193
|
+
if (
|
|
1194
|
+
error instanceof Error &&
|
|
1195
|
+
(error.message.includes('Cannot find module') ||
|
|
1196
|
+
error.message.includes('Module not found') ||
|
|
1197
|
+
error.message.includes('Cannot resolve module'))
|
|
1198
|
+
) {
|
|
1199
|
+
logger.warn(
|
|
1200
|
+
'[autotel] OpenLLMetry enabled but @traceloop/node-server-sdk is not installed. ' +
|
|
1201
|
+
'Install it as a peer dependency to use OpenLLMetry integration.',
|
|
1202
|
+
);
|
|
1203
|
+
} else {
|
|
1204
|
+
logger.warn(
|
|
1205
|
+
`[autotel] Failed to initialize OpenLLMetry: ${error instanceof Error ? error.message : String(error)}`,
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Extract instrumentation class names from instrumentation instances
|
|
1213
|
+
* Used to detect duplicates between manual and auto instrumentations
|
|
1214
|
+
*/
|
|
1215
|
+
function getInstrumentationNames(
|
|
1216
|
+
instrumentations: NodeSDKConfiguration['instrumentations'],
|
|
1217
|
+
): Set<string> {
|
|
1218
|
+
const names = new Set<string>();
|
|
1219
|
+
|
|
1220
|
+
if (!instrumentations) return names;
|
|
1221
|
+
|
|
1222
|
+
for (const instrumentation of instrumentations) {
|
|
1223
|
+
if (instrumentation && typeof instrumentation === 'object') {
|
|
1224
|
+
names.add(instrumentation.constructor.name);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
return names;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Map common instrumentation class names to their package names
|
|
1233
|
+
* Used to disable auto-instrumentations when user provides manual configs
|
|
1234
|
+
*/
|
|
1235
|
+
const INSTRUMENTATION_CLASS_TO_PACKAGE: Record<string, string> = {
|
|
1236
|
+
HttpInstrumentation: '@opentelemetry/instrumentation-http',
|
|
1237
|
+
HttpsInstrumentation: '@opentelemetry/instrumentation-http',
|
|
1238
|
+
ExpressInstrumentation: '@opentelemetry/instrumentation-express',
|
|
1239
|
+
FastifyInstrumentation: '@opentelemetry/instrumentation-fastify',
|
|
1240
|
+
MongoDBInstrumentation: '@opentelemetry/instrumentation-mongodb',
|
|
1241
|
+
MongooseInstrumentation: '@opentelemetry/instrumentation-mongoose',
|
|
1242
|
+
PrismaInstrumentation: '@opentelemetry/instrumentation-prisma',
|
|
1243
|
+
PinoInstrumentation: '@opentelemetry/instrumentation-pino',
|
|
1244
|
+
WinstonInstrumentation: '@opentelemetry/instrumentation-winston',
|
|
1245
|
+
RedisInstrumentation: '@opentelemetry/instrumentation-redis',
|
|
1246
|
+
GraphQLInstrumentation: '@opentelemetry/instrumentation-graphql',
|
|
1247
|
+
GrpcInstrumentation: '@opentelemetry/instrumentation-grpc',
|
|
1248
|
+
IORedisInstrumentation: '@opentelemetry/instrumentation-ioredis',
|
|
1249
|
+
KnexInstrumentation: '@opentelemetry/instrumentation-knex',
|
|
1250
|
+
NestJsInstrumentation: '@opentelemetry/instrumentation-nestjs-core',
|
|
1251
|
+
PgInstrumentation: '@opentelemetry/instrumentation-pg',
|
|
1252
|
+
MySQLInstrumentation: '@opentelemetry/instrumentation-mysql',
|
|
1253
|
+
MySQL2Instrumentation: '@opentelemetry/instrumentation-mysql2',
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Get auto-instrumentations based on simple integration names
|
|
1258
|
+
* Excludes instrumentations that are manually provided to avoid conflicts
|
|
1259
|
+
*/
|
|
1260
|
+
function getAutoInstrumentations(
|
|
1261
|
+
integrations: string[] | boolean | Record<string, { enabled?: boolean }>,
|
|
1262
|
+
manualInstrumentationNames: Set<string> = new Set(),
|
|
1263
|
+
): unknown[] {
|
|
1264
|
+
if (integrations === false) {
|
|
1265
|
+
return [];
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Build exclusion config for manual instrumentations
|
|
1269
|
+
const exclusionConfig: Record<string, { enabled: boolean }> = {};
|
|
1270
|
+
for (const className of manualInstrumentationNames) {
|
|
1271
|
+
const packageName = INSTRUMENTATION_CLASS_TO_PACKAGE[className];
|
|
1272
|
+
if (packageName) {
|
|
1273
|
+
exclusionConfig[packageName] = { enabled: false };
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
if (integrations === true) {
|
|
1278
|
+
// If exclusions exist, pass them to getNodeAutoInstrumentations
|
|
1279
|
+
if (Object.keys(exclusionConfig).length > 0) {
|
|
1280
|
+
return getNodeAutoInstrumentations(exclusionConfig);
|
|
1281
|
+
}
|
|
1282
|
+
return getNodeAutoInstrumentations();
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if (Array.isArray(integrations)) {
|
|
1286
|
+
const config: Record<string, { enabled: boolean }> = { ...exclusionConfig };
|
|
1287
|
+
for (const name of integrations) {
|
|
1288
|
+
const packageName = `@opentelemetry/instrumentation-${name}`;
|
|
1289
|
+
// Don't override exclusions
|
|
1290
|
+
if (!exclusionConfig[packageName]) {
|
|
1291
|
+
config[packageName] = { enabled: true };
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return getNodeAutoInstrumentations(config);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const config: Record<string, { enabled?: boolean }> = {
|
|
1298
|
+
...exclusionConfig,
|
|
1299
|
+
...integrations,
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
// Override any integrations that conflict with manual instrumentations
|
|
1303
|
+
for (const packageName of Object.keys(exclusionConfig)) {
|
|
1304
|
+
const integrationsKey = Object.keys(integrations).find((key) =>
|
|
1305
|
+
packageName.includes(key),
|
|
1306
|
+
);
|
|
1307
|
+
if (integrationsKey) {
|
|
1308
|
+
// Manual instrumentation takes precedence
|
|
1309
|
+
config[packageName] = { enabled: false };
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
return getNodeAutoInstrumentations(config);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Check if autotel has been initialized
|
|
1318
|
+
*/
|
|
1319
|
+
export function isInitialized(): boolean {
|
|
1320
|
+
return initialized;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Get current config (internal use)
|
|
1325
|
+
*/
|
|
1326
|
+
export function getConfig(): AutotelConfig | null {
|
|
1327
|
+
return config;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Get current logger (internal use)
|
|
1332
|
+
*/
|
|
1333
|
+
export function getLogger(): Logger {
|
|
1334
|
+
return logger;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Get validation config (internal use)
|
|
1339
|
+
*/
|
|
1340
|
+
export function getValidationConfig(): Partial<ValidationConfig> | null {
|
|
1341
|
+
return validationConfig;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Warn once if not initialized (same behavior in all environments)
|
|
1346
|
+
*/
|
|
1347
|
+
export function warnIfNotInitialized(context: string): void {
|
|
1348
|
+
if (!initialized && !warnedOnce) {
|
|
1349
|
+
logger.warn(
|
|
1350
|
+
`[autotel] ${context} used before init() called. ` +
|
|
1351
|
+
'Call init({ service: "..." }) first. See: https://docs.autotel.dev/quickstart',
|
|
1352
|
+
);
|
|
1353
|
+
warnedOnce = true;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Get default sampler
|
|
1359
|
+
*/
|
|
1360
|
+
export function getDefaultSampler(): Sampler {
|
|
1361
|
+
return (
|
|
1362
|
+
config?.sampler ||
|
|
1363
|
+
new AdaptiveSampler({
|
|
1364
|
+
baselineSampleRate: 0.1,
|
|
1365
|
+
alwaysSampleErrors: true,
|
|
1366
|
+
alwaysSampleSlow: true,
|
|
1367
|
+
})
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* Auto-detect version from package.json
|
|
1373
|
+
*/
|
|
1374
|
+
function detectVersion(): string {
|
|
1375
|
+
try {
|
|
1376
|
+
// Try to read package.json from cwd using fs
|
|
1377
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1378
|
+
const fs = require('node:fs');
|
|
1379
|
+
const pkg = JSON.parse(
|
|
1380
|
+
fs.readFileSync(`${process.cwd()}/package.json`, 'utf8'),
|
|
1381
|
+
);
|
|
1382
|
+
return pkg.version || '1.0.0';
|
|
1383
|
+
} catch {
|
|
1384
|
+
return '1.0.0';
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* Detect hostname for resource attributes.
|
|
1390
|
+
* Supports Datadog conventions (DD_HOSTNAME) and falls back to system hostname.
|
|
1391
|
+
*
|
|
1392
|
+
* Priority order:
|
|
1393
|
+
* 1. DD_HOSTNAME environment variable (Datadog convention)
|
|
1394
|
+
* 2. HOSTNAME environment variable (common Unix convention)
|
|
1395
|
+
* 3. os.hostname() (system hostname)
|
|
1396
|
+
*
|
|
1397
|
+
* @returns hostname string or undefined if detection fails
|
|
1398
|
+
*/
|
|
1399
|
+
function detectHostname(): string | undefined {
|
|
1400
|
+
// Priority 1: DD_HOSTNAME (Datadog convention)
|
|
1401
|
+
if (process.env.DD_HOSTNAME) {
|
|
1402
|
+
return process.env.DD_HOSTNAME;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Priority 2: HOSTNAME (common in containers and Unix systems)
|
|
1406
|
+
if (process.env.HOSTNAME) {
|
|
1407
|
+
return process.env.HOSTNAME;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Priority 3: System hostname
|
|
1411
|
+
try {
|
|
1412
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1413
|
+
const os = require('node:os') as typeof import('node:os');
|
|
1414
|
+
return os.hostname();
|
|
1415
|
+
} catch {
|
|
1416
|
+
// os module not available (edge runtime, browser, etc.)
|
|
1417
|
+
return undefined;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* Get SDK instance (for shutdown)
|
|
1423
|
+
*/
|
|
1424
|
+
export function getSdk(): NodeSDK | null {
|
|
1425
|
+
return sdk;
|
|
1426
|
+
}
|