@questpie/observability 3.17.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,72 @@
1
+ import { Attributes } from "@opentelemetry/api";
2
+ import { LogRecordProcessor } from "@opentelemetry/sdk-logs";
3
+ import { SpanProcessor } from "@opentelemetry/sdk-trace-base";
4
+ import { ObservabilityAdapter } from "questpie/observability";
5
+
6
+ //#region src/otel-adapter.d.ts
7
+
8
+ interface OtelObservabilityOptions {
9
+ /** `service.name`. Required — an unnamed service is unusable in any backend. */
10
+ serviceName: string;
11
+ serviceVersion?: string;
12
+ /** Becomes `deployment.environment.name`. */
13
+ environment?: string;
14
+ /**
15
+ * OTLP/HTTP base endpoint, e.g. `http://localhost:4318`. Omit to skip the
16
+ * OTLP exporter entirely — useful with `console: true` in development.
17
+ */
18
+ otlpEndpoint?: string;
19
+ otlpHeaders?: Record<string, string>;
20
+ /**
21
+ * Head sampling ratio, 0..1. Wrapped in a parent-based sampler, so a
22
+ * sampled upstream request stays sampled here regardless of the ratio.
23
+ * Omit for always-on.
24
+ */
25
+ samplingRatio?: number;
26
+ /** Also print spans to stdout. Development only — it is very noisy. */
27
+ console?: boolean;
28
+ /** Metric export interval. Default 60s, matching the OTLP default. */
29
+ metricIntervalMs?: number;
30
+ /** Extra resource attributes, merged last so they can override the above. */
31
+ resourceAttributes?: Attributes;
32
+ /**
33
+ * Additional span processors, appended after the OTLP and console ones.
34
+ *
35
+ * `BasicTracerProvider` takes its processors at construction and has no
36
+ * `addSpanProcessor`, so without this there is no way to attach a different
37
+ * exporter (Jaeger, Zipkin, a custom enricher) or to capture spans in memory
38
+ * for a test — which is how the span-tree assertions in this package's
39
+ * `otel-tree.test.ts` work.
40
+ */
41
+ spanProcessors?: SpanProcessor[];
42
+ /**
43
+ * Additional log-record processors, appended after the OTLP one. Same
44
+ * reasoning as `spanProcessors`: `LoggerProvider` takes its processors at
45
+ * construction, so without this there is no way to capture records in a test
46
+ * or send them anywhere else.
47
+ */
48
+ logRecordProcessors?: LogRecordProcessor[];
49
+ }
50
+ /**
51
+ * Build the OTel-backed adapter.
52
+ *
53
+ * ```ts
54
+ * import { otelObservability } from "@questpie/observability";
55
+ *
56
+ * export default runtimeConfig({
57
+ * observability: {
58
+ * adapter: otelObservability({
59
+ * serviceName: "my-app",
60
+ * otlpEndpoint: "http://localhost:4318",
61
+ * }),
62
+ * },
63
+ * });
64
+ * ```
65
+ *
66
+ * The providers are created eagerly — `runtimeConfig` is evaluated at startup,
67
+ * which is early enough, and it means the first request is already traced
68
+ * rather than racing an async init.
69
+ */
70
+ declare function otelObservability(options: OtelObservabilityOptions): ObservabilityAdapter;
71
+ //#endregion
72
+ export { type OtelObservabilityOptions, otelObservability };
package/dist/index.mjs ADDED
@@ -0,0 +1,204 @@
1
+ import { SpanStatusCode, context, isSpanContextValid, metrics, propagation, trace } from "@opentelemetry/api";
2
+ import { SeverityNumber, logs } from "@opentelemetry/api-logs";
3
+ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
4
+ import { W3CTraceContextPropagator } from "@opentelemetry/core";
5
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
6
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
7
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
8
+ import { resourceFromAttributes } from "@opentelemetry/resources";
9
+ import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
10
+ import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
11
+ import { AlwaysOnSampler, BasicTracerProvider, BatchSpanProcessor, ConsoleSpanExporter, ParentBasedSampler, SimpleSpanProcessor, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
12
+ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
13
+
14
+ //#region src/otel-adapter.ts
15
+ /**
16
+ * OpenTelemetry adapter for QUESTPIE.
17
+ *
18
+ * Composes the SDK by hand rather than using `@opentelemetry/sdk-node` or
19
+ * `auto-instrumentations-node`. That is deliberate and was validated on Bun
20
+ * 1.3.14: a manually composed provider initialises and exports under plain
21
+ * `bun run` with no `--require` flag, no preload, and no monkey-patching of
22
+ * built-ins — which is what auto-instrumentation relies on and what tends to be
23
+ * fragile outside Node.
24
+ *
25
+ * Everything here implements the framework-side interfaces from
26
+ * `questpie/observability`. The framework itself has no OpenTelemetry
27
+ * dependency; this package is the only place OTel appears.
28
+ */
29
+ /** OTel severity numbers for the four levels the framework logger exposes. */
30
+ const SEVERITY = {
31
+ debug: SeverityNumber.DEBUG,
32
+ info: SeverityNumber.INFO,
33
+ warn: SeverityNumber.WARN,
34
+ error: SeverityNumber.ERROR
35
+ };
36
+ const SPAN_KINDS = {
37
+ internal: 0,
38
+ server: 1,
39
+ client: 2,
40
+ producer: 3,
41
+ consumer: 4
42
+ };
43
+ /** Drops undefined values — OTel rejects them, the framework type allows them. */
44
+ function toOtelAttributes(attributes) {
45
+ if (!attributes) return {};
46
+ const out = {};
47
+ for (const [key, value] of Object.entries(attributes)) if (value !== void 0) out[key] = value;
48
+ return out;
49
+ }
50
+ function wrapSpan(span) {
51
+ return {
52
+ setAttribute(key, value) {
53
+ span.setAttribute(key, value);
54
+ },
55
+ setAttributes(attributes) {
56
+ span.setAttributes(toOtelAttributes(attributes));
57
+ },
58
+ recordError(error) {
59
+ const err = error instanceof Error ? error : new Error(String(error));
60
+ span.recordException(err);
61
+ span.setStatus({
62
+ code: SpanStatusCode.ERROR,
63
+ message: err.message
64
+ });
65
+ },
66
+ addEvent(name, attributes) {
67
+ span.addEvent(name, toOtelAttributes(attributes));
68
+ },
69
+ end() {
70
+ span.end();
71
+ }
72
+ };
73
+ }
74
+ function wrapTracer(tracer) {
75
+ return { startActiveSpan(name, options, fn) {
76
+ const spanOptions = {
77
+ kind: options.kind ? SPAN_KINDS[options.kind] : void 0,
78
+ attributes: toOtelAttributes(options.attributes)
79
+ };
80
+ const run = () => tracer.startActiveSpan(name, spanOptions, (span) => fn(wrapSpan(span)));
81
+ if (!options.carrier) return run();
82
+ return context.with(propagation.extract(context.active(), options.carrier), run);
83
+ } };
84
+ }
85
+ function wrapMeter(meter) {
86
+ return {
87
+ createCounter(name, options) {
88
+ const counter = meter.createCounter(name, options);
89
+ return { add(value, attributes) {
90
+ counter.add(value, toOtelAttributes(attributes));
91
+ } };
92
+ },
93
+ createHistogram(name, options) {
94
+ const histogram = meter.createHistogram(name, options);
95
+ return { record(value, attributes) {
96
+ histogram.record(value, toOtelAttributes(attributes));
97
+ } };
98
+ }
99
+ };
100
+ }
101
+ function buildSampler(ratio) {
102
+ if (ratio === void 0) return new AlwaysOnSampler();
103
+ return new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(ratio) });
104
+ }
105
+ /**
106
+ * Build the OTel-backed adapter.
107
+ *
108
+ * ```ts
109
+ * import { otelObservability } from "@questpie/observability";
110
+ *
111
+ * export default runtimeConfig({
112
+ * observability: {
113
+ * adapter: otelObservability({
114
+ * serviceName: "my-app",
115
+ * otlpEndpoint: "http://localhost:4318",
116
+ * }),
117
+ * },
118
+ * });
119
+ * ```
120
+ *
121
+ * The providers are created eagerly — `runtimeConfig` is evaluated at startup,
122
+ * which is early enough, and it means the first request is already traced
123
+ * rather than racing an async init.
124
+ */
125
+ function otelObservability(options) {
126
+ const resource = resourceFromAttributes({
127
+ [ATTR_SERVICE_NAME]: options.serviceName,
128
+ ...options.serviceVersion ? { [ATTR_SERVICE_VERSION]: options.serviceVersion } : {},
129
+ ...options.environment ? { "deployment.environment.name": options.environment } : {},
130
+ ...options.resourceAttributes
131
+ });
132
+ const spanProcessors = [];
133
+ if (options.otlpEndpoint) spanProcessors.push(new BatchSpanProcessor(new OTLPTraceExporter({
134
+ url: `${options.otlpEndpoint.replace(/\/$/, "")}/v1/traces`,
135
+ headers: options.otlpHeaders
136
+ })));
137
+ if (options.console) spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter()));
138
+ if (options.spanProcessors) spanProcessors.push(...options.spanProcessors);
139
+ const tracerProvider = new BasicTracerProvider({
140
+ resource,
141
+ sampler: buildSampler(options.samplingRatio),
142
+ spanProcessors
143
+ });
144
+ const contextManager = new AsyncLocalStorageContextManager();
145
+ contextManager.enable();
146
+ context.setGlobalContextManager(contextManager);
147
+ propagation.setGlobalPropagator(new W3CTraceContextPropagator());
148
+ trace.setGlobalTracerProvider(tracerProvider);
149
+ const meterProvider = new MeterProvider({
150
+ resource,
151
+ readers: options.otlpEndpoint ? [new PeriodicExportingMetricReader({
152
+ exporter: new OTLPMetricExporter({
153
+ url: `${options.otlpEndpoint.replace(/\/$/, "")}/v1/metrics`,
154
+ headers: options.otlpHeaders
155
+ }),
156
+ exportIntervalMillis: options.metricIntervalMs ?? 6e4
157
+ })] : []
158
+ });
159
+ metrics.setGlobalMeterProvider(meterProvider);
160
+ const logRecordProcessors = [];
161
+ if (options.otlpEndpoint) logRecordProcessors.push(new BatchLogRecordProcessor({ exporter: new OTLPLogExporter({
162
+ url: `${options.otlpEndpoint.replace(/\/$/, "")}/v1/logs`,
163
+ headers: options.otlpHeaders
164
+ }) }));
165
+ if (options.logRecordProcessors) logRecordProcessors.push(...options.logRecordProcessors);
166
+ const loggerProvider = new LoggerProvider({
167
+ resource,
168
+ processors: logRecordProcessors
169
+ });
170
+ logs.setGlobalLoggerProvider(loggerProvider);
171
+ const otelLogger = loggerProvider.getLogger("questpie");
172
+ return {
173
+ tracer(name) {
174
+ return wrapTracer(tracerProvider.getTracer(name));
175
+ },
176
+ activeSpanContext() {
177
+ const active = trace.getActiveSpan()?.spanContext();
178
+ if (!active || !isSpanContextValid(active)) return void 0;
179
+ return {
180
+ traceId: active.traceId,
181
+ spanId: active.spanId
182
+ };
183
+ },
184
+ meter(name) {
185
+ return wrapMeter(meterProvider.getMeter(name));
186
+ },
187
+ emitLog(record) {
188
+ otelLogger.emit({
189
+ severityNumber: SEVERITY[record.level] ?? SeverityNumber.INFO,
190
+ severityText: record.level.toUpperCase(),
191
+ body: record.message,
192
+ attributes: toOtelAttributes(record.attributes)
193
+ });
194
+ },
195
+ async shutdown() {
196
+ await tracerProvider.shutdown();
197
+ await meterProvider.shutdown();
198
+ await loggerProvider.shutdown();
199
+ }
200
+ };
201
+ }
202
+
203
+ //#endregion
204
+ export { otelObservability };
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@questpie/observability",
3
+ "version": "3.17.0",
4
+ "description": "OpenTelemetry adapter for QUESTPIE — traces and metrics via OTLP",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/questpie/questpie.git",
9
+ "directory": "packages/observability"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "type": "module",
15
+ "sideEffects": false,
16
+ "main": "./dist/index.mjs",
17
+ "module": "./dist/index.mjs",
18
+ "types": "./dist/index.d.mts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./src/exports/index.ts",
22
+ "default": "./src/exports/index.ts"
23
+ },
24
+ "./package.json": {
25
+ "types": "./package.json",
26
+ "default": "./package.json"
27
+ }
28
+ },
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.mts",
34
+ "default": "./dist/index.mjs"
35
+ },
36
+ "./package.json": {
37
+ "types": "./package.json",
38
+ "default": "./package.json"
39
+ }
40
+ }
41
+ },
42
+ "scripts": {
43
+ "build": "tsdown",
44
+ "check-types": "tsc --noEmit",
45
+ "test": "bun test --timeout=60000 test/"
46
+ },
47
+ "dependencies": {
48
+ "@opentelemetry/api": "^1.9.0",
49
+ "@opentelemetry/api-logs": "^0.221.0",
50
+ "@opentelemetry/context-async-hooks": "^2.2.0",
51
+ "@opentelemetry/core": "^2.2.0",
52
+ "@opentelemetry/exporter-logs-otlp-http": "^0.221.0",
53
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.221.0",
54
+ "@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
55
+ "@opentelemetry/resources": "^2.2.0",
56
+ "@opentelemetry/sdk-logs": "^0.221.0",
57
+ "@opentelemetry/sdk-metrics": "^2.2.0",
58
+ "@opentelemetry/sdk-trace-base": "^2.2.0",
59
+ "@opentelemetry/semantic-conventions": "^1.43.0"
60
+ },
61
+ "devDependencies": {
62
+ "@electric-sql/pglite": "^0.3.14",
63
+ "bun-types": "latest",
64
+ "drizzle-kit": "1.0.0-beta.6-4414a19",
65
+ "drizzle-orm": "1.0.0-beta.6-4414a19",
66
+ "tsdown": "^0.18.3"
67
+ },
68
+ "peerDependencies": {
69
+ "questpie": "workspace:^"
70
+ }
71
+ }