@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.
- package/dist/index.d.mts +72 -0
- package/dist/index.mjs +204 -0
- package/package.json +71 -0
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
}
|