@odla-ai/o11y 1.0.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/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # @odla-ai/o11y
2
+
3
+ Official observability client for [odla](https://github.com/) Cloudflare Workers.
4
+ Wrap your Worker once and get OpenTelemetry **traces**, **metrics**, structured
5
+ **errors**, and **LLM cost** — exported over OTLP to the odla-o11y collector.
6
+
7
+ - Traces via [`@microlabs/otel-cf-workers`](https://github.com/evanderkoogh/otel-cf-workers) (auto-instruments incoming/outgoing fetch, Durable Objects, and bindings).
8
+ - Metrics + logs via lightweight OTLP/JSON emitters (the traces-only otel library doesn't cover them, and the Node metrics SDK doesn't fit the isolate model).
9
+ - Targets the Cloudflare Workers runtime; requires the `nodejs_compat` flag.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm i @odla-ai/o11y
15
+ ```
16
+
17
+ ## Use
18
+
19
+ ```ts
20
+ import { withObservability, span, count, recordError, recordLlmUsage } from "@odla-ai/o11y";
21
+
22
+ const handler = {
23
+ async fetch(req: Request, env: Env): Promise<Response> {
24
+ count("http.requests", 1, { "http.route": new URL(req.url).pathname });
25
+ return span("handle", async () => new Response("ok"), { kind: "server" });
26
+ },
27
+ } satisfies ExportedHandler<Env>;
28
+
29
+ export default withObservability(handler);
30
+ ```
31
+
32
+ Wrap a Durable Object class with `instrumentDurableObject(MyDO)`.
33
+
34
+ ## Configuration
35
+
36
+ Config is read from env vars (override per call via the second `withObservability` arg):
37
+
38
+ | Var | Meaning |
39
+ | --- | --- |
40
+ | `ODLA_O11Y_ENDPOINT` | Collector base URL (e.g. `https://odla-o11y.workers.dev`) |
41
+ | `ODLA_O11Y_SERVICE` | This service's name |
42
+ | `ODLA_O11Y_TOKEN` | Per-service bearer token (secret) |
43
+ | `ODLA_O11Y_VERSION` | Release tag (optional) |
44
+
45
+ `wrangler.jsonc` needs `"compatibility_flags": ["nodejs_compat"]` (for AsyncLocalStorage).
46
+
47
+ ## API
48
+
49
+ - `withObservability(handler, opts?)` — wrap a Worker handler (fetch/scheduled).
50
+ - `instrumentDurableObject(cls, opts?)` — wrap a Durable Object class.
51
+ - `span(name, fn, opts?)` — run `fn` inside an active span.
52
+ - `count(name, by?, attrs?)` / `metrics()` — counters, gauges, histograms.
53
+ - `recordError(err, report?)` — structured error; message/stack go only to the
54
+ collector's R2 artifact bundle, never to metrics.
55
+ - `recordLlmUsage(usage, opts)` — turn `{calls, inputTokens, outputTokens}` into
56
+ cost + token metrics and GenAI span attributes.
57
+
58
+ ## License
59
+
60
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,352 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ count: () => count,
24
+ instrumentDurableObject: () => instrumentDurableObject,
25
+ metrics: () => metrics,
26
+ recordError: () => recordError,
27
+ recordLlmUsage: () => recordLlmUsage,
28
+ span: () => span,
29
+ withObservability: () => withObservability
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/instrument.ts
34
+ var import_otel_cf_workers = require("@microlabs/otel-cf-workers");
35
+
36
+ // src/context.ts
37
+ var import_node_async_hooks = require("async_hooks");
38
+
39
+ // ../o11y-core/src/encode.ts
40
+ function attrsToKvList(attrs) {
41
+ const out = [];
42
+ for (const [key, value] of Object.entries(attrs)) {
43
+ if (typeof value === "string") out.push({ key, value: { stringValue: value } });
44
+ else if (typeof value === "boolean") out.push({ key, value: { boolValue: value } });
45
+ else if (Number.isInteger(value)) out.push({ key, value: { intValue: String(value) } });
46
+ else out.push({ key, value: { doubleValue: value } });
47
+ }
48
+ return out;
49
+ }
50
+ var DELTA = 1;
51
+ function encodeMetricsRequest(resource, scopeName, points) {
52
+ const metrics2 = points.map((p) => {
53
+ const dp = {
54
+ attributes: attrsToKvList(p.attrs),
55
+ startTimeUnixNano: p.startUnixNano,
56
+ timeUnixNano: p.timeUnixNano,
57
+ asDouble: p.value
58
+ };
59
+ return p.kind === "gauge" ? { name: p.name, unit: p.unit, gauge: { dataPoints: [dp] } } : { name: p.name, unit: p.unit, sum: { aggregationTemporality: DELTA, isMonotonic: true, dataPoints: [dp] } };
60
+ });
61
+ return {
62
+ resourceMetrics: [
63
+ { resource: { attributes: attrsToKvList(resource) }, scopeMetrics: [{ scope: { name: scopeName }, metrics: metrics2 }] }
64
+ ]
65
+ };
66
+ }
67
+ function encodeLogsRequest(resource, scopeName, records) {
68
+ return {
69
+ resourceLogs: [
70
+ {
71
+ resource: { attributes: attrsToKvList(resource) },
72
+ scopeLogs: [
73
+ {
74
+ scope: { name: scopeName },
75
+ logRecords: records.map((r) => ({
76
+ timeUnixNano: r.timeUnixNano,
77
+ observedTimeUnixNano: r.timeUnixNano,
78
+ severityNumber: r.severityNumber,
79
+ severityText: r.severityText,
80
+ body: { stringValue: r.body },
81
+ attributes: attrsToKvList(r.attrs),
82
+ ...r.traceId ? { traceId: r.traceId } : {},
83
+ ...r.spanId ? { spanId: r.spanId } : {}
84
+ }))
85
+ }
86
+ ]
87
+ }
88
+ ]
89
+ };
90
+ }
91
+
92
+ // ../o11y-core/src/pricing.ts
93
+ var MODEL_PRICING = {
94
+ "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 },
95
+ "claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
96
+ "claude-sonnet-5": { inputPerMTok: 3, outputPerMTok: 15 },
97
+ "claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25 },
98
+ "claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25 },
99
+ "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
100
+ "claude-fable-5": { inputPerMTok: 10, outputPerMTok: 50 }
101
+ };
102
+ function costUsd(model, inputTokens, outputTokens, override) {
103
+ const price = override ?? MODEL_PRICING[model];
104
+ if (!price) return 0;
105
+ return inputTokens / 1e6 * price.inputPerMTok + outputTokens / 1e6 * price.outputPerMTok;
106
+ }
107
+
108
+ // src/context.ts
109
+ var SCOPE = "@odla-ai/o11y";
110
+ function evalOpts(opts, env) {
111
+ return typeof opts === "function" ? opts(env) : opts ?? {};
112
+ }
113
+ function resolveConfig(env, opts) {
114
+ const o = evalOpts(opts, env);
115
+ const e = env;
116
+ return {
117
+ service: o.service ?? e["ODLA_O11Y_SERVICE"] ?? "unknown",
118
+ endpoint: (o.endpoint ?? e["ODLA_O11Y_ENDPOINT"] ?? "").replace(/\/$/, ""),
119
+ token: o.token ?? e["ODLA_O11Y_TOKEN"],
120
+ version: o.version ?? e["ODLA_O11Y_VERSION"] ?? "0.0.0",
121
+ attributes: o.attributes ?? {},
122
+ sampleRatio: o.sampleRatio
123
+ };
124
+ }
125
+ function nowNano() {
126
+ return `${Date.now()}000000`;
127
+ }
128
+ var als = new import_node_async_hooks.AsyncLocalStorage();
129
+ function currentSink() {
130
+ return als.getStore();
131
+ }
132
+ function runWithSink(sink, fn) {
133
+ return als.run(sink, fn);
134
+ }
135
+ function resourceAttrs(config) {
136
+ return { "service.name": config.service, "service.version": config.version, ...config.attributes };
137
+ }
138
+ async function post(url, body, token) {
139
+ try {
140
+ await fetch(url, {
141
+ method: "POST",
142
+ headers: {
143
+ "content-type": "application/json",
144
+ ...token ? { authorization: `Bearer ${token}` } : {}
145
+ },
146
+ body: JSON.stringify(body)
147
+ });
148
+ } catch {
149
+ }
150
+ }
151
+ function createSink(config) {
152
+ return {
153
+ config,
154
+ metrics: [],
155
+ logs: [],
156
+ async flush() {
157
+ if (!this.config.endpoint) {
158
+ this.metrics = [];
159
+ this.logs = [];
160
+ return;
161
+ }
162
+ const resource = resourceAttrs(this.config);
163
+ const jobs = [];
164
+ if (this.metrics.length) {
165
+ jobs.push(
166
+ post(`${this.config.endpoint}/v1/metrics`, encodeMetricsRequest(resource, SCOPE, this.metrics), this.config.token)
167
+ );
168
+ this.metrics = [];
169
+ }
170
+ if (this.logs.length) {
171
+ jobs.push(
172
+ post(`${this.config.endpoint}/v1/logs`, encodeLogsRequest(resource, SCOPE, this.logs), this.config.token)
173
+ );
174
+ this.logs = [];
175
+ }
176
+ await Promise.all(jobs);
177
+ }
178
+ };
179
+ }
180
+
181
+ // src/instrument.ts
182
+ function traceConfig(env, opts) {
183
+ const c = resolveConfig(env, opts);
184
+ const headers = {};
185
+ if (c.token) headers.authorization = `Bearer ${c.token}`;
186
+ const config = {
187
+ exporter: { url: `${c.endpoint}/v1/traces`, headers },
188
+ service: { name: c.service, version: c.version }
189
+ };
190
+ if (c.sampleRatio != null) config.sampling = { headSampler: { ratio: c.sampleRatio } };
191
+ return config;
192
+ }
193
+ function wrapHandler(handler, opts) {
194
+ const wrapped = { ...handler };
195
+ const fetchImpl = handler.fetch;
196
+ if (fetchImpl) {
197
+ wrapped.fetch = (req, env, ctx) => {
198
+ const sink = createSink(resolveConfig(env, opts));
199
+ return runWithSink(sink, async () => {
200
+ try {
201
+ return await fetchImpl(req, env, ctx);
202
+ } finally {
203
+ ctx.waitUntil(sink.flush());
204
+ }
205
+ });
206
+ };
207
+ }
208
+ const scheduledImpl = handler.scheduled;
209
+ if (scheduledImpl) {
210
+ wrapped.scheduled = (controller, env, ctx) => {
211
+ const sink = createSink(resolveConfig(env, opts));
212
+ return runWithSink(sink, async () => {
213
+ try {
214
+ return await scheduledImpl(controller, env, ctx);
215
+ } finally {
216
+ ctx.waitUntil(sink.flush());
217
+ }
218
+ });
219
+ };
220
+ }
221
+ return wrapped;
222
+ }
223
+ function withObservability(handler, opts) {
224
+ const configFn = (env, _trigger) => traceConfig(env, opts);
225
+ return (0, import_otel_cf_workers.instrument)(wrapHandler(handler, opts), configFn);
226
+ }
227
+ function instrumentDurableObject(cls, opts) {
228
+ const configFn = (env, _trigger) => traceConfig(env, opts);
229
+ return (0, import_otel_cf_workers.instrumentDO)(cls, configFn);
230
+ }
231
+
232
+ // src/span.ts
233
+ var import_api = require("@opentelemetry/api");
234
+ var KIND = {
235
+ internal: import_api.SpanKind.INTERNAL,
236
+ server: import_api.SpanKind.SERVER,
237
+ client: import_api.SpanKind.CLIENT,
238
+ producer: import_api.SpanKind.PRODUCER,
239
+ consumer: import_api.SpanKind.CONSUMER
240
+ };
241
+ async function span(name, fn, opts) {
242
+ const tracer = import_api.trace.getTracer(SCOPE);
243
+ return tracer.startActiveSpan(
244
+ name,
245
+ { attributes: opts?.attributes, kind: opts?.kind ? KIND[opts.kind] : void 0 },
246
+ async (s) => {
247
+ try {
248
+ const result = await fn(s);
249
+ s.setStatus({ code: import_api.SpanStatusCode.OK });
250
+ return result;
251
+ } catch (err) {
252
+ s.setStatus({ code: import_api.SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) });
253
+ throw err;
254
+ } finally {
255
+ s.end();
256
+ }
257
+ }
258
+ );
259
+ }
260
+
261
+ // src/metrics.ts
262
+ function record(kind, name, value, attrs, unit) {
263
+ const sink = currentSink();
264
+ if (!sink) return;
265
+ const ts = nowNano();
266
+ sink.metrics.push({ name, unit, kind, value, attrs: attrs ?? {}, startUnixNano: ts, timeUnixNano: ts });
267
+ }
268
+ function metrics() {
269
+ return {
270
+ count: (name, by = 1, attrs) => record("sum", name, by, attrs),
271
+ gauge: (name, value, attrs) => record("gauge", name, value, attrs),
272
+ histogram: (name, value, attrs, unit) => record("sum", name, value, attrs, unit)
273
+ };
274
+ }
275
+ function count(name, by = 1, attrs) {
276
+ record("sum", name, by, attrs);
277
+ }
278
+
279
+ // src/logs.ts
280
+ var import_api2 = require("@opentelemetry/api");
281
+ var SEVERITY_ERROR = 17;
282
+ function recordError(err, report) {
283
+ const error = err instanceof Error ? err : new Error(String(err));
284
+ const artifactId = `${Date.now().toString(16)}${crypto.randomUUID().replace(/-/g, "")}`;
285
+ const span2 = import_api2.trace.getActiveSpan();
286
+ const attrs = {
287
+ "error.type": error.name,
288
+ "odla.artifact_id": artifactId,
289
+ ...report?.code ? { "error.code": report.code } : {},
290
+ ...report?.route ? { "odla.route": report.route } : {},
291
+ ...report?.fingerprint ? { "odla.fingerprint": report.fingerprint } : {},
292
+ ...report?.attributes ?? {}
293
+ };
294
+ if (span2) {
295
+ span2.recordException(error);
296
+ span2.setStatus({ code: import_api2.SpanStatusCode.ERROR, message: error.message });
297
+ span2.setAttributes(attrs);
298
+ }
299
+ const sink = currentSink();
300
+ if (sink) {
301
+ const ctx = span2?.spanContext();
302
+ sink.logs.push({
303
+ timeUnixNano: nowNano(),
304
+ severityNumber: SEVERITY_ERROR,
305
+ severityText: "ERROR",
306
+ body: error.message,
307
+ // full message → R2 bundle only (collector-enforced)
308
+ attrs: {
309
+ ...attrs,
310
+ // Denied keys: stripped from metrics/index, retained in the R2 bundle.
311
+ ...error.stack ? { "exception.stacktrace": error.stack } : {},
312
+ ...report?.artifacts ? { "odla.artifacts": JSON.stringify(report.artifacts) } : {}
313
+ },
314
+ ...ctx ? { traceId: ctx.traceId, spanId: ctx.spanId } : {}
315
+ });
316
+ }
317
+ return artifactId;
318
+ }
319
+
320
+ // src/llm.ts
321
+ var import_api3 = require("@opentelemetry/api");
322
+ function recordLlmUsage(usage, opts) {
323
+ const cost = costUsd(opts.model, usage.inputTokens, usage.outputTokens, opts.price);
324
+ const base = {
325
+ "gen_ai.provider.name": opts.provider,
326
+ "gen_ai.request.model": opts.model,
327
+ ...opts.operation ? { "gen_ai.operation.name": opts.operation } : {},
328
+ ...opts.attributes ?? {}
329
+ };
330
+ count("odla.llm.cost.usd", cost, base);
331
+ count("odla.llm.calls", usage.calls, base);
332
+ count("odla.llm.tokens", usage.inputTokens, { ...base, "gen_ai.token.type": "input" });
333
+ count("odla.llm.tokens", usage.outputTokens, { ...base, "gen_ai.token.type": "output" });
334
+ import_api3.trace.getActiveSpan()?.setAttributes({
335
+ ...base,
336
+ "gen_ai.usage.input_tokens": usage.inputTokens,
337
+ "gen_ai.usage.output_tokens": usage.outputTokens,
338
+ "odla.llm.cost.usd": cost
339
+ });
340
+ return { costUsd: cost };
341
+ }
342
+ // Annotate the CommonJS export names for ESM import in node:
343
+ 0 && (module.exports = {
344
+ count,
345
+ instrumentDurableObject,
346
+ metrics,
347
+ recordError,
348
+ recordLlmUsage,
349
+ span,
350
+ withObservability
351
+ });
352
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/instrument.ts","../src/context.ts","../../o11y-core/src/encode.ts","../../o11y-core/src/pricing.ts","../src/span.ts","../src/metrics.ts","../src/logs.ts","../src/llm.ts"],"sourcesContent":["export { instrumentDurableObject, withObservability } from \"./instrument.js\";\nexport { span } from \"./span.js\";\nexport { count, metrics } from \"./metrics.js\";\nexport { recordError } from \"./logs.js\";\nexport { recordLlmUsage } from \"./llm.js\";\n\nexport type { ObservabilityOptions, OptsFn } from \"./context.js\";\nexport type { SpanKindName, SpanOpts } from \"./span.js\";\nexport type { Metrics } from \"./metrics.js\";\nexport type { ErrorReport } from \"./logs.js\";\nexport type { LlmCostOpts, LlmUsage } from \"./llm.js\";\nexport type { Attrs } from \"@odla/o11y-core\";\n","import { instrument, instrumentDO, type ResolveConfigFn, type TraceConfig } from \"@microlabs/otel-cf-workers\";\nimport { createSink, resolveConfig, runWithSink, type OptsFn } from \"./context.js\";\n\n/** Build the otel-cf-workers trace config (exporter + service) from env/opts. */\nfunction traceConfig<E>(env: E, opts?: OptsFn<E>): TraceConfig {\n const c = resolveConfig(env, opts);\n const headers: Record<string, string> = {};\n if (c.token) headers.authorization = `Bearer ${c.token}`;\n const config: TraceConfig = {\n exporter: { url: `${c.endpoint}/v1/traces`, headers },\n service: { name: c.service, version: c.version },\n };\n if (c.sampleRatio != null) config.sampling = { headSampler: { ratio: c.sampleRatio } };\n return config;\n}\n\n/** Wrap each handler entrypoint so an ALS metrics/logs sink is live for the\n * duration of the invocation and flushed via ctx.waitUntil at the end. */\nfunction wrapHandler<E>(handler: ExportedHandler<E>, opts?: OptsFn<E>): ExportedHandler<E> {\n const wrapped: ExportedHandler<E> = { ...handler };\n\n const fetchImpl = handler.fetch;\n if (fetchImpl) {\n wrapped.fetch = (req, env, ctx) => {\n const sink = createSink(resolveConfig(env, opts));\n return runWithSink(sink, async () => {\n try {\n return await fetchImpl(req, env, ctx);\n } finally {\n ctx.waitUntil(sink.flush());\n }\n });\n };\n }\n\n const scheduledImpl = handler.scheduled;\n if (scheduledImpl) {\n wrapped.scheduled = (controller, env, ctx) => {\n const sink = createSink(resolveConfig(env, opts));\n return runWithSink(sink, async () => {\n try {\n return await scheduledImpl(controller, env, ctx);\n } finally {\n ctx.waitUntil(sink.flush());\n }\n });\n };\n }\n\n return wrapped;\n}\n\n/** Wrap a Worker handler with tracing (otel-cf-workers) + the metrics/logs sink.\n * Config is read from env vars ODLA_O11Y_{ENDPOINT,SERVICE,TOKEN,VERSION} unless\n * overridden by `opts`. */\nexport function withObservability<E = unknown>(\n handler: ExportedHandler<E>,\n opts?: OptsFn<E>,\n): ExportedHandler<E> {\n const configFn: ResolveConfigFn = (env, _trigger) => traceConfig(env as E, opts);\n // instrument constrains E to its own Env type; we support any service env, so\n // cross the boundary with `any` and restore the caller's E on the way out.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return instrument(wrapHandler(handler, opts) as ExportedHandler<any>, configFn) as ExportedHandler<E>;\n}\n\n/** Wrap a Durable Object class with tracing. (Establishing the metrics/logs sink\n * inside DO methods is added in P1 when odla-db's usage counters are bridged.) */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function instrumentDurableObject<T extends abstract new (...args: any[]) => object>(\n cls: T,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n opts?: OptsFn<any>,\n): T {\n const configFn: ResolveConfigFn = (env, _trigger) => traceConfig(env, opts);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return instrumentDO(cls as any, configFn) as unknown as T;\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\";\nimport {\n encodeLogsRequest,\n encodeMetricsRequest,\n type Attrs,\n type LogRecord,\n type MetricPoint,\n} from \"@odla/o11y-core\";\n\n/** The instrumentation scope name stamped on every signal. */\nexport const SCOPE = \"@odla-ai/o11y\";\n\nexport interface ObservabilityOptions {\n /** Service name (default: env.ODLA_O11Y_SERVICE). */\n service?: string;\n /** Collector base URL (default: env.ODLA_O11Y_ENDPOINT). */\n endpoint?: string;\n /** Per-service bearer token (default: env.ODLA_O11Y_TOKEN). */\n token?: string;\n /** Release/version tag (default: env.ODLA_O11Y_VERSION ?? \"0.0.0\"). */\n version?: string;\n /** Head-sampling ratio 0..1 for traces (default: 1). */\n sampleRatio?: number;\n /** Static resource attributes merged onto every signal. */\n attributes?: Attrs;\n}\n\nexport type OptsFn<E> = ObservabilityOptions | ((env: E) => ObservabilityOptions);\n\nexport interface ResolvedConfig {\n service: string;\n endpoint: string;\n token?: string;\n version: string;\n attributes: Attrs;\n sampleRatio?: number;\n}\n\nfunction evalOpts<E>(opts: OptsFn<E> | undefined, env: E): ObservabilityOptions {\n return typeof opts === \"function\" ? opts(env) : (opts ?? {});\n}\n\n/** Resolve options against the service's env vars. */\nexport function resolveConfig<E>(env: E, opts?: OptsFn<E>): ResolvedConfig {\n const o = evalOpts(opts, env);\n const e = env as Record<string, string | undefined>;\n return {\n service: o.service ?? e[\"ODLA_O11Y_SERVICE\"] ?? \"unknown\",\n endpoint: (o.endpoint ?? e[\"ODLA_O11Y_ENDPOINT\"] ?? \"\").replace(/\\/$/, \"\"),\n token: o.token ?? e[\"ODLA_O11Y_TOKEN\"],\n version: o.version ?? e[\"ODLA_O11Y_VERSION\"] ?? \"0.0.0\",\n attributes: o.attributes ?? {},\n sampleRatio: o.sampleRatio,\n };\n}\n\n/** Nanoseconds since epoch as a decimal string (OTLP timestamp format). */\nexport function nowNano(): string {\n return `${Date.now()}000000`;\n}\n\nexport interface Sink {\n config: ResolvedConfig;\n metrics: MetricPoint[];\n logs: LogRecord[];\n flush(): Promise<void>;\n}\n\nconst als = new AsyncLocalStorage<Sink>();\n\nexport function currentSink(): Sink | undefined {\n return als.getStore();\n}\n\nexport function runWithSink<T>(sink: Sink, fn: () => Promise<T>): Promise<T> {\n return als.run(sink, fn);\n}\n\nfunction resourceAttrs(config: ResolvedConfig): Attrs {\n return { \"service.name\": config.service, \"service.version\": config.version, ...config.attributes };\n}\n\nasync function post(url: string, body: unknown, token?: string): Promise<void> {\n try {\n await fetch(url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...(token ? { authorization: `Bearer ${token}` } : {}),\n },\n body: JSON.stringify(body),\n });\n } catch {\n // Telemetry export is best-effort — never surface into the app.\n }\n}\n\n/** A per-invocation sink: accumulates metric/log records and flushes them to the\n * collector as OTLP/JSON (traces are exported separately by otel-cf-workers). */\nexport function createSink(config: ResolvedConfig): Sink {\n return {\n config,\n metrics: [],\n logs: [],\n async flush(): Promise<void> {\n if (!this.config.endpoint) {\n this.metrics = [];\n this.logs = [];\n return;\n }\n const resource = resourceAttrs(this.config);\n const jobs: Promise<void>[] = [];\n if (this.metrics.length) {\n jobs.push(\n post(`${this.config.endpoint}/v1/metrics`, encodeMetricsRequest(resource, SCOPE, this.metrics), this.config.token),\n );\n this.metrics = [];\n }\n if (this.logs.length) {\n jobs.push(\n post(`${this.config.endpoint}/v1/logs`, encodeLogsRequest(resource, SCOPE, this.logs), this.config.token),\n );\n this.logs = [];\n }\n await Promise.all(jobs);\n },\n };\n}\n","// OTLP/JSON encoders — the emit side of the wire contract (the SDK produces\n// these; the collector decodes them in later phases). Traces are produced by\n// @microlabs/otel-cf-workers; metrics and logs are hand-rolled here because that\n// library is traces-only and the Node OTel metrics SDK doesn't fit the isolate\n// model.\n\nimport type { Attrs, OtlpKeyValue, OtlpResource, OtlpScope } from \"./otlp.js\";\n\n/** Inverse of kvListToAttrs: a flat attr map → OTLP KeyValue[]. */\nexport function attrsToKvList(attrs: Attrs): OtlpKeyValue[] {\n const out: OtlpKeyValue[] = [];\n for (const [key, value] of Object.entries(attrs)) {\n if (typeof value === \"string\") out.push({ key, value: { stringValue: value } });\n else if (typeof value === \"boolean\") out.push({ key, value: { boolValue: value } });\n else if (Number.isInteger(value)) out.push({ key, value: { intValue: String(value) } });\n else out.push({ key, value: { doubleValue: value } });\n }\n return out;\n}\n\n// ---- Metrics ----\n\nexport type MetricKind = \"sum\" | \"gauge\";\n\nexport interface MetricPoint {\n name: string;\n unit?: string;\n kind: MetricKind;\n value: number;\n attrs: Attrs;\n startUnixNano: string;\n timeUnixNano: string;\n}\n\ninterface OtlpNumberDataPoint {\n attributes?: OtlpKeyValue[];\n startTimeUnixNano?: string;\n timeUnixNano: string;\n asDouble: number;\n}\n\ninterface OtlpMetric {\n name: string;\n unit?: string;\n sum?: { aggregationTemporality: number; isMonotonic: boolean; dataPoints: OtlpNumberDataPoint[] };\n gauge?: { dataPoints: OtlpNumberDataPoint[] };\n}\n\nexport interface ExportMetricsServiceRequest {\n resourceMetrics: Array<{\n resource: OtlpResource;\n scopeMetrics: Array<{ scope: OtlpScope; metrics: OtlpMetric[] }>;\n }>;\n}\n\n// OTLP AggregationTemporality: 1 = DELTA. Each ephemeral isolate reports its own\n// delta; the collector aggregates across invocations into Analytics Engine.\nconst DELTA = 1;\n\nexport function encodeMetricsRequest(\n resource: Attrs,\n scopeName: string,\n points: MetricPoint[],\n): ExportMetricsServiceRequest {\n const metrics: OtlpMetric[] = points.map((p) => {\n const dp: OtlpNumberDataPoint = {\n attributes: attrsToKvList(p.attrs),\n startTimeUnixNano: p.startUnixNano,\n timeUnixNano: p.timeUnixNano,\n asDouble: p.value,\n };\n return p.kind === \"gauge\"\n ? { name: p.name, unit: p.unit, gauge: { dataPoints: [dp] } }\n : { name: p.name, unit: p.unit, sum: { aggregationTemporality: DELTA, isMonotonic: true, dataPoints: [dp] } };\n });\n return {\n resourceMetrics: [\n { resource: { attributes: attrsToKvList(resource) }, scopeMetrics: [{ scope: { name: scopeName }, metrics }] },\n ],\n };\n}\n\n// ---- Logs (errors ride the logs signal) ----\n\nexport interface LogRecord {\n timeUnixNano: string;\n severityNumber: number; // OTel: 17 = ERROR, 9 = INFO\n severityText: string;\n body: string;\n attrs: Attrs;\n traceId?: string;\n spanId?: string;\n}\n\nexport interface ExportLogsServiceRequest {\n resourceLogs: Array<{\n resource: OtlpResource;\n scopeLogs: Array<{\n scope: OtlpScope;\n logRecords: Array<{\n timeUnixNano: string;\n observedTimeUnixNano: string;\n severityNumber: number;\n severityText: string;\n body: { stringValue: string };\n attributes: OtlpKeyValue[];\n traceId?: string;\n spanId?: string;\n }>;\n }>;\n }>;\n}\n\nexport function encodeLogsRequest(\n resource: Attrs,\n scopeName: string,\n records: LogRecord[],\n): ExportLogsServiceRequest {\n return {\n resourceLogs: [\n {\n resource: { attributes: attrsToKvList(resource) },\n scopeLogs: [\n {\n scope: { name: scopeName },\n logRecords: records.map((r) => ({\n timeUnixNano: r.timeUnixNano,\n observedTimeUnixNano: r.timeUnixNano,\n severityNumber: r.severityNumber,\n severityText: r.severityText,\n body: { stringValue: r.body },\n attributes: attrsToKvList(r.attrs),\n ...(r.traceId ? { traceId: r.traceId } : {}),\n ...(r.spanId ? { spanId: r.spanId } : {}),\n })),\n },\n ],\n },\n ],\n };\n}\n","// LLM price table + cost computation. OTel GenAI semconv has no cost attribute,\n// so we compute cost downstream from raw token counts × model price. Keep this\n// table in sync with current model pricing (verify against the claude-api\n// reference); unknown models cost 0 and should be added here.\n\nexport interface ModelPrice {\n /** USD per million input tokens. */\n inputPerMTok: number;\n /** USD per million output tokens. */\n outputPerMTok: number;\n}\n\n/** Prices in USD per million tokens (MTok). */\nexport const MODEL_PRICING: Record<string, ModelPrice> = {\n \"claude-haiku-4-5\": { inputPerMTok: 1, outputPerMTok: 5 },\n \"claude-sonnet-4-6\": { inputPerMTok: 3, outputPerMTok: 15 },\n \"claude-sonnet-5\": { inputPerMTok: 3, outputPerMTok: 15 },\n \"claude-opus-4-6\": { inputPerMTok: 5, outputPerMTok: 25 },\n \"claude-opus-4-7\": { inputPerMTok: 5, outputPerMTok: 25 },\n \"claude-opus-4-8\": { inputPerMTok: 5, outputPerMTok: 25 },\n \"claude-fable-5\": { inputPerMTok: 10, outputPerMTok: 50 },\n};\n\n/** Compute USD cost for a call. Pass `override` to price a model not in the\n * table (or to apply promotional pricing). Returns 0 for unknown models. */\nexport function costUsd(\n model: string,\n inputTokens: number,\n outputTokens: number,\n override?: ModelPrice,\n): number {\n const price = override ?? MODEL_PRICING[model];\n if (!price) return 0;\n return (inputTokens / 1_000_000) * price.inputPerMTok + (outputTokens / 1_000_000) * price.outputPerMTok;\n}\n","import { SpanKind, SpanStatusCode, trace, type Span } from \"@opentelemetry/api\";\nimport type { Attrs } from \"@odla/o11y-core\";\nimport { SCOPE } from \"./context.js\";\n\nexport type SpanKindName = \"internal\" | \"server\" | \"client\" | \"producer\" | \"consumer\";\n\nexport interface SpanOpts {\n attributes?: Attrs;\n kind?: SpanKindName;\n}\n\nconst KIND: Record<SpanKindName, SpanKind> = {\n internal: SpanKind.INTERNAL,\n server: SpanKind.SERVER,\n client: SpanKind.CLIENT,\n producer: SpanKind.PRODUCER,\n consumer: SpanKind.CONSUMER,\n};\n\n/** Run `fn` inside a new active span. The span is ended automatically and its\n * status set from success/throw. Works whether or not tracing is exporting —\n * falls back to a no-op span if no tracer is installed. */\nexport async function span<T>(\n name: string,\n fn: (span: Span) => Promise<T> | T,\n opts?: SpanOpts,\n): Promise<T> {\n const tracer = trace.getTracer(SCOPE);\n return tracer.startActiveSpan(\n name,\n { attributes: opts?.attributes, kind: opts?.kind ? KIND[opts.kind] : undefined },\n async (s) => {\n try {\n const result = await fn(s);\n s.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (err) {\n s.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) });\n throw err;\n } finally {\n s.end();\n }\n },\n );\n}\n","import type { Attrs } from \"@odla/o11y-core\";\nimport { currentSink, nowNano } from \"./context.js\";\n\nexport interface Metrics {\n /** Monotonic counter (delta). Formalizes odla-db's usage counters. */\n count(name: string, by?: number, attrs?: Attrs): void;\n /** Last-value gauge. */\n gauge(name: string, value: number, attrs?: Attrs): void;\n /** A single observation (recorded as a delta sum for now; histograms later). */\n histogram(name: string, value: number, attrs?: Attrs, unit?: string): void;\n}\n\nfunction record(kind: \"sum\" | \"gauge\", name: string, value: number, attrs?: Attrs, unit?: string): void {\n const sink = currentSink();\n if (!sink) return; // outside an instrumented invocation — no-op\n const ts = nowNano();\n sink.metrics.push({ name, unit, kind, value, attrs: attrs ?? {}, startUnixNano: ts, timeUnixNano: ts });\n}\n\n/** The ambient metrics API for the current invocation. */\nexport function metrics(): Metrics {\n return {\n count: (name, by = 1, attrs) => record(\"sum\", name, by, attrs),\n gauge: (name, value, attrs) => record(\"gauge\", name, value, attrs),\n histogram: (name, value, attrs, unit) => record(\"sum\", name, value, attrs, unit),\n };\n}\n\n/** Convenience: increment a monotonic counter by `by` (default 1). */\nexport function count(name: string, by = 1, attrs?: Attrs): void {\n record(\"sum\", name, by, attrs);\n}\n","import { SpanStatusCode, trace } from \"@opentelemetry/api\";\nimport type { Attrs } from \"@odla/o11y-core\";\nimport { currentSink, nowNano } from \"./context.js\";\n\nexport interface ErrorReport {\n /** Stable, low-cardinality error code (e.g. \"unique_violation\"). */\n code?: string;\n /** Low-cardinality route/step where it happened. */\n route?: string;\n /** Extra low-cardinality attributes for the index. */\n attributes?: Attrs;\n /** Rich context for triage — kept only in the R2 artifact bundle. */\n artifacts?: Record<string, unknown>;\n /** Dedup key; the collector fingerprints by (type+code+route) if absent. */\n fingerprint?: string;\n}\n\n// OTel severity number for ERROR.\nconst SEVERITY_ERROR = 17;\n\n/** Record a structured error. Returns an artifact id. The message and stack ride\n * the log body + a denied attribute so the collector persists them ONLY in the\n * R2 artifact bundle, never in metrics or the queryable index. */\nexport function recordError(err: unknown, report?: ErrorReport): string {\n const error = err instanceof Error ? err : new Error(String(err));\n const artifactId = `${Date.now().toString(16)}${crypto.randomUUID().replace(/-/g, \"\")}`;\n const span = trace.getActiveSpan();\n\n const attrs: Attrs = {\n \"error.type\": error.name,\n \"odla.artifact_id\": artifactId,\n ...(report?.code ? { \"error.code\": report.code } : {}),\n ...(report?.route ? { \"odla.route\": report.route } : {}),\n ...(report?.fingerprint ? { \"odla.fingerprint\": report.fingerprint } : {}),\n ...(report?.attributes ?? {}),\n };\n\n if (span) {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n span.setAttributes(attrs);\n }\n\n const sink = currentSink();\n if (sink) {\n const ctx = span?.spanContext();\n sink.logs.push({\n timeUnixNano: nowNano(),\n severityNumber: SEVERITY_ERROR,\n severityText: \"ERROR\",\n body: error.message, // full message → R2 bundle only (collector-enforced)\n attrs: {\n ...attrs,\n // Denied keys: stripped from metrics/index, retained in the R2 bundle.\n ...(error.stack ? { \"exception.stacktrace\": error.stack } : {}),\n ...(report?.artifacts ? { \"odla.artifacts\": JSON.stringify(report.artifacts) } : {}),\n },\n ...(ctx ? { traceId: ctx.traceId, spanId: ctx.spanId } : {}),\n });\n }\n\n return artifactId;\n}\n","import { trace } from \"@opentelemetry/api\";\nimport { costUsd, type Attrs, type ModelPrice } from \"@odla/o11y-core\";\nimport { count } from \"./metrics.js\";\n\n/** The token accounting odla-kg's Provider already returns (and drops today). */\nexport interface LlmUsage {\n calls: number;\n inputTokens: number;\n outputTokens: number;\n}\n\nexport interface LlmCostOpts {\n provider: string; // \"claude\" | \"openai\" | ...\n model: string; // e.g. \"claude-sonnet-4-6\"\n operation?: string; // \"extract\" | \"search\" | ...\n attributes?: Attrs;\n /** Override the built-in price table for this call. */\n price?: ModelPrice;\n}\n\n/** Turn LLM token usage into first-class cost. Emits cost/token/call counters\n * and stamps the active span with GenAI semconv attributes. */\nexport function recordLlmUsage(usage: LlmUsage, opts: LlmCostOpts): { costUsd: number } {\n const cost = costUsd(opts.model, usage.inputTokens, usage.outputTokens, opts.price);\n const base: Attrs = {\n \"gen_ai.provider.name\": opts.provider,\n \"gen_ai.request.model\": opts.model,\n ...(opts.operation ? { \"gen_ai.operation.name\": opts.operation } : {}),\n ...(opts.attributes ?? {}),\n };\n\n count(\"odla.llm.cost.usd\", cost, base);\n count(\"odla.llm.calls\", usage.calls, base);\n count(\"odla.llm.tokens\", usage.inputTokens, { ...base, \"gen_ai.token.type\": \"input\" });\n count(\"odla.llm.tokens\", usage.outputTokens, { ...base, \"gen_ai.token.type\": \"output\" });\n\n trace.getActiveSpan()?.setAttributes({\n ...base,\n \"gen_ai.usage.input_tokens\": usage.inputTokens,\n \"gen_ai.usage.output_tokens\": usage.outputTokens,\n \"odla.llm.cost.usd\": cost,\n });\n\n return { costUsd: cost };\n}\n"],"mappings":"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","names":["metrics","import_api","span","import_api"]}
@@ -0,0 +1,89 @@
1
+ import { Attrs, ModelPrice } from '@odla/o11y-core';
2
+ export { Attrs } from '@odla/o11y-core';
3
+ import { Span } from '@opentelemetry/api';
4
+
5
+ interface ObservabilityOptions {
6
+ /** Service name (default: env.ODLA_O11Y_SERVICE). */
7
+ service?: string;
8
+ /** Collector base URL (default: env.ODLA_O11Y_ENDPOINT). */
9
+ endpoint?: string;
10
+ /** Per-service bearer token (default: env.ODLA_O11Y_TOKEN). */
11
+ token?: string;
12
+ /** Release/version tag (default: env.ODLA_O11Y_VERSION ?? "0.0.0"). */
13
+ version?: string;
14
+ /** Head-sampling ratio 0..1 for traces (default: 1). */
15
+ sampleRatio?: number;
16
+ /** Static resource attributes merged onto every signal. */
17
+ attributes?: Attrs;
18
+ }
19
+ type OptsFn<E> = ObservabilityOptions | ((env: E) => ObservabilityOptions);
20
+
21
+ /** Wrap a Worker handler with tracing (otel-cf-workers) + the metrics/logs sink.
22
+ * Config is read from env vars ODLA_O11Y_{ENDPOINT,SERVICE,TOKEN,VERSION} unless
23
+ * overridden by `opts`. */
24
+ declare function withObservability<E = unknown>(handler: ExportedHandler<E>, opts?: OptsFn<E>): ExportedHandler<E>;
25
+ /** Wrap a Durable Object class with tracing. (Establishing the metrics/logs sink
26
+ * inside DO methods is added in P1 when odla-db's usage counters are bridged.) */
27
+ declare function instrumentDurableObject<T extends abstract new (...args: any[]) => object>(cls: T, opts?: OptsFn<any>): T;
28
+
29
+ type SpanKindName = "internal" | "server" | "client" | "producer" | "consumer";
30
+ interface SpanOpts {
31
+ attributes?: Attrs;
32
+ kind?: SpanKindName;
33
+ }
34
+ /** Run `fn` inside a new active span. The span is ended automatically and its
35
+ * status set from success/throw. Works whether or not tracing is exporting —
36
+ * falls back to a no-op span if no tracer is installed. */
37
+ declare function span<T>(name: string, fn: (span: Span) => Promise<T> | T, opts?: SpanOpts): Promise<T>;
38
+
39
+ interface Metrics {
40
+ /** Monotonic counter (delta). Formalizes odla-db's usage counters. */
41
+ count(name: string, by?: number, attrs?: Attrs): void;
42
+ /** Last-value gauge. */
43
+ gauge(name: string, value: number, attrs?: Attrs): void;
44
+ /** A single observation (recorded as a delta sum for now; histograms later). */
45
+ histogram(name: string, value: number, attrs?: Attrs, unit?: string): void;
46
+ }
47
+ /** The ambient metrics API for the current invocation. */
48
+ declare function metrics(): Metrics;
49
+ /** Convenience: increment a monotonic counter by `by` (default 1). */
50
+ declare function count(name: string, by?: number, attrs?: Attrs): void;
51
+
52
+ interface ErrorReport {
53
+ /** Stable, low-cardinality error code (e.g. "unique_violation"). */
54
+ code?: string;
55
+ /** Low-cardinality route/step where it happened. */
56
+ route?: string;
57
+ /** Extra low-cardinality attributes for the index. */
58
+ attributes?: Attrs;
59
+ /** Rich context for triage — kept only in the R2 artifact bundle. */
60
+ artifacts?: Record<string, unknown>;
61
+ /** Dedup key; the collector fingerprints by (type+code+route) if absent. */
62
+ fingerprint?: string;
63
+ }
64
+ /** Record a structured error. Returns an artifact id. The message and stack ride
65
+ * the log body + a denied attribute so the collector persists them ONLY in the
66
+ * R2 artifact bundle, never in metrics or the queryable index. */
67
+ declare function recordError(err: unknown, report?: ErrorReport): string;
68
+
69
+ /** The token accounting odla-kg's Provider already returns (and drops today). */
70
+ interface LlmUsage {
71
+ calls: number;
72
+ inputTokens: number;
73
+ outputTokens: number;
74
+ }
75
+ interface LlmCostOpts {
76
+ provider: string;
77
+ model: string;
78
+ operation?: string;
79
+ attributes?: Attrs;
80
+ /** Override the built-in price table for this call. */
81
+ price?: ModelPrice;
82
+ }
83
+ /** Turn LLM token usage into first-class cost. Emits cost/token/call counters
84
+ * and stamps the active span with GenAI semconv attributes. */
85
+ declare function recordLlmUsage(usage: LlmUsage, opts: LlmCostOpts): {
86
+ costUsd: number;
87
+ };
88
+
89
+ export { type ErrorReport, type LlmCostOpts, type LlmUsage, type Metrics, type ObservabilityOptions, type OptsFn, type SpanKindName, type SpanOpts, count, instrumentDurableObject, metrics, recordError, recordLlmUsage, span, withObservability };
@@ -0,0 +1,89 @@
1
+ import { Attrs, ModelPrice } from '@odla/o11y-core';
2
+ export { Attrs } from '@odla/o11y-core';
3
+ import { Span } from '@opentelemetry/api';
4
+
5
+ interface ObservabilityOptions {
6
+ /** Service name (default: env.ODLA_O11Y_SERVICE). */
7
+ service?: string;
8
+ /** Collector base URL (default: env.ODLA_O11Y_ENDPOINT). */
9
+ endpoint?: string;
10
+ /** Per-service bearer token (default: env.ODLA_O11Y_TOKEN). */
11
+ token?: string;
12
+ /** Release/version tag (default: env.ODLA_O11Y_VERSION ?? "0.0.0"). */
13
+ version?: string;
14
+ /** Head-sampling ratio 0..1 for traces (default: 1). */
15
+ sampleRatio?: number;
16
+ /** Static resource attributes merged onto every signal. */
17
+ attributes?: Attrs;
18
+ }
19
+ type OptsFn<E> = ObservabilityOptions | ((env: E) => ObservabilityOptions);
20
+
21
+ /** Wrap a Worker handler with tracing (otel-cf-workers) + the metrics/logs sink.
22
+ * Config is read from env vars ODLA_O11Y_{ENDPOINT,SERVICE,TOKEN,VERSION} unless
23
+ * overridden by `opts`. */
24
+ declare function withObservability<E = unknown>(handler: ExportedHandler<E>, opts?: OptsFn<E>): ExportedHandler<E>;
25
+ /** Wrap a Durable Object class with tracing. (Establishing the metrics/logs sink
26
+ * inside DO methods is added in P1 when odla-db's usage counters are bridged.) */
27
+ declare function instrumentDurableObject<T extends abstract new (...args: any[]) => object>(cls: T, opts?: OptsFn<any>): T;
28
+
29
+ type SpanKindName = "internal" | "server" | "client" | "producer" | "consumer";
30
+ interface SpanOpts {
31
+ attributes?: Attrs;
32
+ kind?: SpanKindName;
33
+ }
34
+ /** Run `fn` inside a new active span. The span is ended automatically and its
35
+ * status set from success/throw. Works whether or not tracing is exporting —
36
+ * falls back to a no-op span if no tracer is installed. */
37
+ declare function span<T>(name: string, fn: (span: Span) => Promise<T> | T, opts?: SpanOpts): Promise<T>;
38
+
39
+ interface Metrics {
40
+ /** Monotonic counter (delta). Formalizes odla-db's usage counters. */
41
+ count(name: string, by?: number, attrs?: Attrs): void;
42
+ /** Last-value gauge. */
43
+ gauge(name: string, value: number, attrs?: Attrs): void;
44
+ /** A single observation (recorded as a delta sum for now; histograms later). */
45
+ histogram(name: string, value: number, attrs?: Attrs, unit?: string): void;
46
+ }
47
+ /** The ambient metrics API for the current invocation. */
48
+ declare function metrics(): Metrics;
49
+ /** Convenience: increment a monotonic counter by `by` (default 1). */
50
+ declare function count(name: string, by?: number, attrs?: Attrs): void;
51
+
52
+ interface ErrorReport {
53
+ /** Stable, low-cardinality error code (e.g. "unique_violation"). */
54
+ code?: string;
55
+ /** Low-cardinality route/step where it happened. */
56
+ route?: string;
57
+ /** Extra low-cardinality attributes for the index. */
58
+ attributes?: Attrs;
59
+ /** Rich context for triage — kept only in the R2 artifact bundle. */
60
+ artifacts?: Record<string, unknown>;
61
+ /** Dedup key; the collector fingerprints by (type+code+route) if absent. */
62
+ fingerprint?: string;
63
+ }
64
+ /** Record a structured error. Returns an artifact id. The message and stack ride
65
+ * the log body + a denied attribute so the collector persists them ONLY in the
66
+ * R2 artifact bundle, never in metrics or the queryable index. */
67
+ declare function recordError(err: unknown, report?: ErrorReport): string;
68
+
69
+ /** The token accounting odla-kg's Provider already returns (and drops today). */
70
+ interface LlmUsage {
71
+ calls: number;
72
+ inputTokens: number;
73
+ outputTokens: number;
74
+ }
75
+ interface LlmCostOpts {
76
+ provider: string;
77
+ model: string;
78
+ operation?: string;
79
+ attributes?: Attrs;
80
+ /** Override the built-in price table for this call. */
81
+ price?: ModelPrice;
82
+ }
83
+ /** Turn LLM token usage into first-class cost. Emits cost/token/call counters
84
+ * and stamps the active span with GenAI semconv attributes. */
85
+ declare function recordLlmUsage(usage: LlmUsage, opts: LlmCostOpts): {
86
+ costUsd: number;
87
+ };
88
+
89
+ export { type ErrorReport, type LlmCostOpts, type LlmUsage, type Metrics, type ObservabilityOptions, type OptsFn, type SpanKindName, type SpanOpts, count, instrumentDurableObject, metrics, recordError, recordLlmUsage, span, withObservability };
package/dist/index.js ADDED
@@ -0,0 +1,319 @@
1
+ // src/instrument.ts
2
+ import { instrument, instrumentDO } from "@microlabs/otel-cf-workers";
3
+
4
+ // src/context.ts
5
+ import { AsyncLocalStorage } from "async_hooks";
6
+
7
+ // ../o11y-core/src/encode.ts
8
+ function attrsToKvList(attrs) {
9
+ const out = [];
10
+ for (const [key, value] of Object.entries(attrs)) {
11
+ if (typeof value === "string") out.push({ key, value: { stringValue: value } });
12
+ else if (typeof value === "boolean") out.push({ key, value: { boolValue: value } });
13
+ else if (Number.isInteger(value)) out.push({ key, value: { intValue: String(value) } });
14
+ else out.push({ key, value: { doubleValue: value } });
15
+ }
16
+ return out;
17
+ }
18
+ var DELTA = 1;
19
+ function encodeMetricsRequest(resource, scopeName, points) {
20
+ const metrics2 = points.map((p) => {
21
+ const dp = {
22
+ attributes: attrsToKvList(p.attrs),
23
+ startTimeUnixNano: p.startUnixNano,
24
+ timeUnixNano: p.timeUnixNano,
25
+ asDouble: p.value
26
+ };
27
+ return p.kind === "gauge" ? { name: p.name, unit: p.unit, gauge: { dataPoints: [dp] } } : { name: p.name, unit: p.unit, sum: { aggregationTemporality: DELTA, isMonotonic: true, dataPoints: [dp] } };
28
+ });
29
+ return {
30
+ resourceMetrics: [
31
+ { resource: { attributes: attrsToKvList(resource) }, scopeMetrics: [{ scope: { name: scopeName }, metrics: metrics2 }] }
32
+ ]
33
+ };
34
+ }
35
+ function encodeLogsRequest(resource, scopeName, records) {
36
+ return {
37
+ resourceLogs: [
38
+ {
39
+ resource: { attributes: attrsToKvList(resource) },
40
+ scopeLogs: [
41
+ {
42
+ scope: { name: scopeName },
43
+ logRecords: records.map((r) => ({
44
+ timeUnixNano: r.timeUnixNano,
45
+ observedTimeUnixNano: r.timeUnixNano,
46
+ severityNumber: r.severityNumber,
47
+ severityText: r.severityText,
48
+ body: { stringValue: r.body },
49
+ attributes: attrsToKvList(r.attrs),
50
+ ...r.traceId ? { traceId: r.traceId } : {},
51
+ ...r.spanId ? { spanId: r.spanId } : {}
52
+ }))
53
+ }
54
+ ]
55
+ }
56
+ ]
57
+ };
58
+ }
59
+
60
+ // ../o11y-core/src/pricing.ts
61
+ var MODEL_PRICING = {
62
+ "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 },
63
+ "claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
64
+ "claude-sonnet-5": { inputPerMTok: 3, outputPerMTok: 15 },
65
+ "claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25 },
66
+ "claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25 },
67
+ "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
68
+ "claude-fable-5": { inputPerMTok: 10, outputPerMTok: 50 }
69
+ };
70
+ function costUsd(model, inputTokens, outputTokens, override) {
71
+ const price = override ?? MODEL_PRICING[model];
72
+ if (!price) return 0;
73
+ return inputTokens / 1e6 * price.inputPerMTok + outputTokens / 1e6 * price.outputPerMTok;
74
+ }
75
+
76
+ // src/context.ts
77
+ var SCOPE = "@odla-ai/o11y";
78
+ function evalOpts(opts, env) {
79
+ return typeof opts === "function" ? opts(env) : opts ?? {};
80
+ }
81
+ function resolveConfig(env, opts) {
82
+ const o = evalOpts(opts, env);
83
+ const e = env;
84
+ return {
85
+ service: o.service ?? e["ODLA_O11Y_SERVICE"] ?? "unknown",
86
+ endpoint: (o.endpoint ?? e["ODLA_O11Y_ENDPOINT"] ?? "").replace(/\/$/, ""),
87
+ token: o.token ?? e["ODLA_O11Y_TOKEN"],
88
+ version: o.version ?? e["ODLA_O11Y_VERSION"] ?? "0.0.0",
89
+ attributes: o.attributes ?? {},
90
+ sampleRatio: o.sampleRatio
91
+ };
92
+ }
93
+ function nowNano() {
94
+ return `${Date.now()}000000`;
95
+ }
96
+ var als = new AsyncLocalStorage();
97
+ function currentSink() {
98
+ return als.getStore();
99
+ }
100
+ function runWithSink(sink, fn) {
101
+ return als.run(sink, fn);
102
+ }
103
+ function resourceAttrs(config) {
104
+ return { "service.name": config.service, "service.version": config.version, ...config.attributes };
105
+ }
106
+ async function post(url, body, token) {
107
+ try {
108
+ await fetch(url, {
109
+ method: "POST",
110
+ headers: {
111
+ "content-type": "application/json",
112
+ ...token ? { authorization: `Bearer ${token}` } : {}
113
+ },
114
+ body: JSON.stringify(body)
115
+ });
116
+ } catch {
117
+ }
118
+ }
119
+ function createSink(config) {
120
+ return {
121
+ config,
122
+ metrics: [],
123
+ logs: [],
124
+ async flush() {
125
+ if (!this.config.endpoint) {
126
+ this.metrics = [];
127
+ this.logs = [];
128
+ return;
129
+ }
130
+ const resource = resourceAttrs(this.config);
131
+ const jobs = [];
132
+ if (this.metrics.length) {
133
+ jobs.push(
134
+ post(`${this.config.endpoint}/v1/metrics`, encodeMetricsRequest(resource, SCOPE, this.metrics), this.config.token)
135
+ );
136
+ this.metrics = [];
137
+ }
138
+ if (this.logs.length) {
139
+ jobs.push(
140
+ post(`${this.config.endpoint}/v1/logs`, encodeLogsRequest(resource, SCOPE, this.logs), this.config.token)
141
+ );
142
+ this.logs = [];
143
+ }
144
+ await Promise.all(jobs);
145
+ }
146
+ };
147
+ }
148
+
149
+ // src/instrument.ts
150
+ function traceConfig(env, opts) {
151
+ const c = resolveConfig(env, opts);
152
+ const headers = {};
153
+ if (c.token) headers.authorization = `Bearer ${c.token}`;
154
+ const config = {
155
+ exporter: { url: `${c.endpoint}/v1/traces`, headers },
156
+ service: { name: c.service, version: c.version }
157
+ };
158
+ if (c.sampleRatio != null) config.sampling = { headSampler: { ratio: c.sampleRatio } };
159
+ return config;
160
+ }
161
+ function wrapHandler(handler, opts) {
162
+ const wrapped = { ...handler };
163
+ const fetchImpl = handler.fetch;
164
+ if (fetchImpl) {
165
+ wrapped.fetch = (req, env, ctx) => {
166
+ const sink = createSink(resolveConfig(env, opts));
167
+ return runWithSink(sink, async () => {
168
+ try {
169
+ return await fetchImpl(req, env, ctx);
170
+ } finally {
171
+ ctx.waitUntil(sink.flush());
172
+ }
173
+ });
174
+ };
175
+ }
176
+ const scheduledImpl = handler.scheduled;
177
+ if (scheduledImpl) {
178
+ wrapped.scheduled = (controller, env, ctx) => {
179
+ const sink = createSink(resolveConfig(env, opts));
180
+ return runWithSink(sink, async () => {
181
+ try {
182
+ return await scheduledImpl(controller, env, ctx);
183
+ } finally {
184
+ ctx.waitUntil(sink.flush());
185
+ }
186
+ });
187
+ };
188
+ }
189
+ return wrapped;
190
+ }
191
+ function withObservability(handler, opts) {
192
+ const configFn = (env, _trigger) => traceConfig(env, opts);
193
+ return instrument(wrapHandler(handler, opts), configFn);
194
+ }
195
+ function instrumentDurableObject(cls, opts) {
196
+ const configFn = (env, _trigger) => traceConfig(env, opts);
197
+ return instrumentDO(cls, configFn);
198
+ }
199
+
200
+ // src/span.ts
201
+ import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
202
+ var KIND = {
203
+ internal: SpanKind.INTERNAL,
204
+ server: SpanKind.SERVER,
205
+ client: SpanKind.CLIENT,
206
+ producer: SpanKind.PRODUCER,
207
+ consumer: SpanKind.CONSUMER
208
+ };
209
+ async function span(name, fn, opts) {
210
+ const tracer = trace.getTracer(SCOPE);
211
+ return tracer.startActiveSpan(
212
+ name,
213
+ { attributes: opts?.attributes, kind: opts?.kind ? KIND[opts.kind] : void 0 },
214
+ async (s) => {
215
+ try {
216
+ const result = await fn(s);
217
+ s.setStatus({ code: SpanStatusCode.OK });
218
+ return result;
219
+ } catch (err) {
220
+ s.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) });
221
+ throw err;
222
+ } finally {
223
+ s.end();
224
+ }
225
+ }
226
+ );
227
+ }
228
+
229
+ // src/metrics.ts
230
+ function record(kind, name, value, attrs, unit) {
231
+ const sink = currentSink();
232
+ if (!sink) return;
233
+ const ts = nowNano();
234
+ sink.metrics.push({ name, unit, kind, value, attrs: attrs ?? {}, startUnixNano: ts, timeUnixNano: ts });
235
+ }
236
+ function metrics() {
237
+ return {
238
+ count: (name, by = 1, attrs) => record("sum", name, by, attrs),
239
+ gauge: (name, value, attrs) => record("gauge", name, value, attrs),
240
+ histogram: (name, value, attrs, unit) => record("sum", name, value, attrs, unit)
241
+ };
242
+ }
243
+ function count(name, by = 1, attrs) {
244
+ record("sum", name, by, attrs);
245
+ }
246
+
247
+ // src/logs.ts
248
+ import { SpanStatusCode as SpanStatusCode2, trace as trace2 } from "@opentelemetry/api";
249
+ var SEVERITY_ERROR = 17;
250
+ function recordError(err, report) {
251
+ const error = err instanceof Error ? err : new Error(String(err));
252
+ const artifactId = `${Date.now().toString(16)}${crypto.randomUUID().replace(/-/g, "")}`;
253
+ const span2 = trace2.getActiveSpan();
254
+ const attrs = {
255
+ "error.type": error.name,
256
+ "odla.artifact_id": artifactId,
257
+ ...report?.code ? { "error.code": report.code } : {},
258
+ ...report?.route ? { "odla.route": report.route } : {},
259
+ ...report?.fingerprint ? { "odla.fingerprint": report.fingerprint } : {},
260
+ ...report?.attributes ?? {}
261
+ };
262
+ if (span2) {
263
+ span2.recordException(error);
264
+ span2.setStatus({ code: SpanStatusCode2.ERROR, message: error.message });
265
+ span2.setAttributes(attrs);
266
+ }
267
+ const sink = currentSink();
268
+ if (sink) {
269
+ const ctx = span2?.spanContext();
270
+ sink.logs.push({
271
+ timeUnixNano: nowNano(),
272
+ severityNumber: SEVERITY_ERROR,
273
+ severityText: "ERROR",
274
+ body: error.message,
275
+ // full message → R2 bundle only (collector-enforced)
276
+ attrs: {
277
+ ...attrs,
278
+ // Denied keys: stripped from metrics/index, retained in the R2 bundle.
279
+ ...error.stack ? { "exception.stacktrace": error.stack } : {},
280
+ ...report?.artifacts ? { "odla.artifacts": JSON.stringify(report.artifacts) } : {}
281
+ },
282
+ ...ctx ? { traceId: ctx.traceId, spanId: ctx.spanId } : {}
283
+ });
284
+ }
285
+ return artifactId;
286
+ }
287
+
288
+ // src/llm.ts
289
+ import { trace as trace3 } from "@opentelemetry/api";
290
+ function recordLlmUsage(usage, opts) {
291
+ const cost = costUsd(opts.model, usage.inputTokens, usage.outputTokens, opts.price);
292
+ const base = {
293
+ "gen_ai.provider.name": opts.provider,
294
+ "gen_ai.request.model": opts.model,
295
+ ...opts.operation ? { "gen_ai.operation.name": opts.operation } : {},
296
+ ...opts.attributes ?? {}
297
+ };
298
+ count("odla.llm.cost.usd", cost, base);
299
+ count("odla.llm.calls", usage.calls, base);
300
+ count("odla.llm.tokens", usage.inputTokens, { ...base, "gen_ai.token.type": "input" });
301
+ count("odla.llm.tokens", usage.outputTokens, { ...base, "gen_ai.token.type": "output" });
302
+ trace3.getActiveSpan()?.setAttributes({
303
+ ...base,
304
+ "gen_ai.usage.input_tokens": usage.inputTokens,
305
+ "gen_ai.usage.output_tokens": usage.outputTokens,
306
+ "odla.llm.cost.usd": cost
307
+ });
308
+ return { costUsd: cost };
309
+ }
310
+ export {
311
+ count,
312
+ instrumentDurableObject,
313
+ metrics,
314
+ recordError,
315
+ recordLlmUsage,
316
+ span,
317
+ withObservability
318
+ };
319
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/instrument.ts","../src/context.ts","../../o11y-core/src/encode.ts","../../o11y-core/src/pricing.ts","../src/span.ts","../src/metrics.ts","../src/logs.ts","../src/llm.ts"],"sourcesContent":["import { instrument, instrumentDO, type ResolveConfigFn, type TraceConfig } from \"@microlabs/otel-cf-workers\";\nimport { createSink, resolveConfig, runWithSink, type OptsFn } from \"./context.js\";\n\n/** Build the otel-cf-workers trace config (exporter + service) from env/opts. */\nfunction traceConfig<E>(env: E, opts?: OptsFn<E>): TraceConfig {\n const c = resolveConfig(env, opts);\n const headers: Record<string, string> = {};\n if (c.token) headers.authorization = `Bearer ${c.token}`;\n const config: TraceConfig = {\n exporter: { url: `${c.endpoint}/v1/traces`, headers },\n service: { name: c.service, version: c.version },\n };\n if (c.sampleRatio != null) config.sampling = { headSampler: { ratio: c.sampleRatio } };\n return config;\n}\n\n/** Wrap each handler entrypoint so an ALS metrics/logs sink is live for the\n * duration of the invocation and flushed via ctx.waitUntil at the end. */\nfunction wrapHandler<E>(handler: ExportedHandler<E>, opts?: OptsFn<E>): ExportedHandler<E> {\n const wrapped: ExportedHandler<E> = { ...handler };\n\n const fetchImpl = handler.fetch;\n if (fetchImpl) {\n wrapped.fetch = (req, env, ctx) => {\n const sink = createSink(resolveConfig(env, opts));\n return runWithSink(sink, async () => {\n try {\n return await fetchImpl(req, env, ctx);\n } finally {\n ctx.waitUntil(sink.flush());\n }\n });\n };\n }\n\n const scheduledImpl = handler.scheduled;\n if (scheduledImpl) {\n wrapped.scheduled = (controller, env, ctx) => {\n const sink = createSink(resolveConfig(env, opts));\n return runWithSink(sink, async () => {\n try {\n return await scheduledImpl(controller, env, ctx);\n } finally {\n ctx.waitUntil(sink.flush());\n }\n });\n };\n }\n\n return wrapped;\n}\n\n/** Wrap a Worker handler with tracing (otel-cf-workers) + the metrics/logs sink.\n * Config is read from env vars ODLA_O11Y_{ENDPOINT,SERVICE,TOKEN,VERSION} unless\n * overridden by `opts`. */\nexport function withObservability<E = unknown>(\n handler: ExportedHandler<E>,\n opts?: OptsFn<E>,\n): ExportedHandler<E> {\n const configFn: ResolveConfigFn = (env, _trigger) => traceConfig(env as E, opts);\n // instrument constrains E to its own Env type; we support any service env, so\n // cross the boundary with `any` and restore the caller's E on the way out.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return instrument(wrapHandler(handler, opts) as ExportedHandler<any>, configFn) as ExportedHandler<E>;\n}\n\n/** Wrap a Durable Object class with tracing. (Establishing the metrics/logs sink\n * inside DO methods is added in P1 when odla-db's usage counters are bridged.) */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function instrumentDurableObject<T extends abstract new (...args: any[]) => object>(\n cls: T,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n opts?: OptsFn<any>,\n): T {\n const configFn: ResolveConfigFn = (env, _trigger) => traceConfig(env, opts);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return instrumentDO(cls as any, configFn) as unknown as T;\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\";\nimport {\n encodeLogsRequest,\n encodeMetricsRequest,\n type Attrs,\n type LogRecord,\n type MetricPoint,\n} from \"@odla/o11y-core\";\n\n/** The instrumentation scope name stamped on every signal. */\nexport const SCOPE = \"@odla-ai/o11y\";\n\nexport interface ObservabilityOptions {\n /** Service name (default: env.ODLA_O11Y_SERVICE). */\n service?: string;\n /** Collector base URL (default: env.ODLA_O11Y_ENDPOINT). */\n endpoint?: string;\n /** Per-service bearer token (default: env.ODLA_O11Y_TOKEN). */\n token?: string;\n /** Release/version tag (default: env.ODLA_O11Y_VERSION ?? \"0.0.0\"). */\n version?: string;\n /** Head-sampling ratio 0..1 for traces (default: 1). */\n sampleRatio?: number;\n /** Static resource attributes merged onto every signal. */\n attributes?: Attrs;\n}\n\nexport type OptsFn<E> = ObservabilityOptions | ((env: E) => ObservabilityOptions);\n\nexport interface ResolvedConfig {\n service: string;\n endpoint: string;\n token?: string;\n version: string;\n attributes: Attrs;\n sampleRatio?: number;\n}\n\nfunction evalOpts<E>(opts: OptsFn<E> | undefined, env: E): ObservabilityOptions {\n return typeof opts === \"function\" ? opts(env) : (opts ?? {});\n}\n\n/** Resolve options against the service's env vars. */\nexport function resolveConfig<E>(env: E, opts?: OptsFn<E>): ResolvedConfig {\n const o = evalOpts(opts, env);\n const e = env as Record<string, string | undefined>;\n return {\n service: o.service ?? e[\"ODLA_O11Y_SERVICE\"] ?? \"unknown\",\n endpoint: (o.endpoint ?? e[\"ODLA_O11Y_ENDPOINT\"] ?? \"\").replace(/\\/$/, \"\"),\n token: o.token ?? e[\"ODLA_O11Y_TOKEN\"],\n version: o.version ?? e[\"ODLA_O11Y_VERSION\"] ?? \"0.0.0\",\n attributes: o.attributes ?? {},\n sampleRatio: o.sampleRatio,\n };\n}\n\n/** Nanoseconds since epoch as a decimal string (OTLP timestamp format). */\nexport function nowNano(): string {\n return `${Date.now()}000000`;\n}\n\nexport interface Sink {\n config: ResolvedConfig;\n metrics: MetricPoint[];\n logs: LogRecord[];\n flush(): Promise<void>;\n}\n\nconst als = new AsyncLocalStorage<Sink>();\n\nexport function currentSink(): Sink | undefined {\n return als.getStore();\n}\n\nexport function runWithSink<T>(sink: Sink, fn: () => Promise<T>): Promise<T> {\n return als.run(sink, fn);\n}\n\nfunction resourceAttrs(config: ResolvedConfig): Attrs {\n return { \"service.name\": config.service, \"service.version\": config.version, ...config.attributes };\n}\n\nasync function post(url: string, body: unknown, token?: string): Promise<void> {\n try {\n await fetch(url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...(token ? { authorization: `Bearer ${token}` } : {}),\n },\n body: JSON.stringify(body),\n });\n } catch {\n // Telemetry export is best-effort — never surface into the app.\n }\n}\n\n/** A per-invocation sink: accumulates metric/log records and flushes them to the\n * collector as OTLP/JSON (traces are exported separately by otel-cf-workers). */\nexport function createSink(config: ResolvedConfig): Sink {\n return {\n config,\n metrics: [],\n logs: [],\n async flush(): Promise<void> {\n if (!this.config.endpoint) {\n this.metrics = [];\n this.logs = [];\n return;\n }\n const resource = resourceAttrs(this.config);\n const jobs: Promise<void>[] = [];\n if (this.metrics.length) {\n jobs.push(\n post(`${this.config.endpoint}/v1/metrics`, encodeMetricsRequest(resource, SCOPE, this.metrics), this.config.token),\n );\n this.metrics = [];\n }\n if (this.logs.length) {\n jobs.push(\n post(`${this.config.endpoint}/v1/logs`, encodeLogsRequest(resource, SCOPE, this.logs), this.config.token),\n );\n this.logs = [];\n }\n await Promise.all(jobs);\n },\n };\n}\n","// OTLP/JSON encoders — the emit side of the wire contract (the SDK produces\n// these; the collector decodes them in later phases). Traces are produced by\n// @microlabs/otel-cf-workers; metrics and logs are hand-rolled here because that\n// library is traces-only and the Node OTel metrics SDK doesn't fit the isolate\n// model.\n\nimport type { Attrs, OtlpKeyValue, OtlpResource, OtlpScope } from \"./otlp.js\";\n\n/** Inverse of kvListToAttrs: a flat attr map → OTLP KeyValue[]. */\nexport function attrsToKvList(attrs: Attrs): OtlpKeyValue[] {\n const out: OtlpKeyValue[] = [];\n for (const [key, value] of Object.entries(attrs)) {\n if (typeof value === \"string\") out.push({ key, value: { stringValue: value } });\n else if (typeof value === \"boolean\") out.push({ key, value: { boolValue: value } });\n else if (Number.isInteger(value)) out.push({ key, value: { intValue: String(value) } });\n else out.push({ key, value: { doubleValue: value } });\n }\n return out;\n}\n\n// ---- Metrics ----\n\nexport type MetricKind = \"sum\" | \"gauge\";\n\nexport interface MetricPoint {\n name: string;\n unit?: string;\n kind: MetricKind;\n value: number;\n attrs: Attrs;\n startUnixNano: string;\n timeUnixNano: string;\n}\n\ninterface OtlpNumberDataPoint {\n attributes?: OtlpKeyValue[];\n startTimeUnixNano?: string;\n timeUnixNano: string;\n asDouble: number;\n}\n\ninterface OtlpMetric {\n name: string;\n unit?: string;\n sum?: { aggregationTemporality: number; isMonotonic: boolean; dataPoints: OtlpNumberDataPoint[] };\n gauge?: { dataPoints: OtlpNumberDataPoint[] };\n}\n\nexport interface ExportMetricsServiceRequest {\n resourceMetrics: Array<{\n resource: OtlpResource;\n scopeMetrics: Array<{ scope: OtlpScope; metrics: OtlpMetric[] }>;\n }>;\n}\n\n// OTLP AggregationTemporality: 1 = DELTA. Each ephemeral isolate reports its own\n// delta; the collector aggregates across invocations into Analytics Engine.\nconst DELTA = 1;\n\nexport function encodeMetricsRequest(\n resource: Attrs,\n scopeName: string,\n points: MetricPoint[],\n): ExportMetricsServiceRequest {\n const metrics: OtlpMetric[] = points.map((p) => {\n const dp: OtlpNumberDataPoint = {\n attributes: attrsToKvList(p.attrs),\n startTimeUnixNano: p.startUnixNano,\n timeUnixNano: p.timeUnixNano,\n asDouble: p.value,\n };\n return p.kind === \"gauge\"\n ? { name: p.name, unit: p.unit, gauge: { dataPoints: [dp] } }\n : { name: p.name, unit: p.unit, sum: { aggregationTemporality: DELTA, isMonotonic: true, dataPoints: [dp] } };\n });\n return {\n resourceMetrics: [\n { resource: { attributes: attrsToKvList(resource) }, scopeMetrics: [{ scope: { name: scopeName }, metrics }] },\n ],\n };\n}\n\n// ---- Logs (errors ride the logs signal) ----\n\nexport interface LogRecord {\n timeUnixNano: string;\n severityNumber: number; // OTel: 17 = ERROR, 9 = INFO\n severityText: string;\n body: string;\n attrs: Attrs;\n traceId?: string;\n spanId?: string;\n}\n\nexport interface ExportLogsServiceRequest {\n resourceLogs: Array<{\n resource: OtlpResource;\n scopeLogs: Array<{\n scope: OtlpScope;\n logRecords: Array<{\n timeUnixNano: string;\n observedTimeUnixNano: string;\n severityNumber: number;\n severityText: string;\n body: { stringValue: string };\n attributes: OtlpKeyValue[];\n traceId?: string;\n spanId?: string;\n }>;\n }>;\n }>;\n}\n\nexport function encodeLogsRequest(\n resource: Attrs,\n scopeName: string,\n records: LogRecord[],\n): ExportLogsServiceRequest {\n return {\n resourceLogs: [\n {\n resource: { attributes: attrsToKvList(resource) },\n scopeLogs: [\n {\n scope: { name: scopeName },\n logRecords: records.map((r) => ({\n timeUnixNano: r.timeUnixNano,\n observedTimeUnixNano: r.timeUnixNano,\n severityNumber: r.severityNumber,\n severityText: r.severityText,\n body: { stringValue: r.body },\n attributes: attrsToKvList(r.attrs),\n ...(r.traceId ? { traceId: r.traceId } : {}),\n ...(r.spanId ? { spanId: r.spanId } : {}),\n })),\n },\n ],\n },\n ],\n };\n}\n","// LLM price table + cost computation. OTel GenAI semconv has no cost attribute,\n// so we compute cost downstream from raw token counts × model price. Keep this\n// table in sync with current model pricing (verify against the claude-api\n// reference); unknown models cost 0 and should be added here.\n\nexport interface ModelPrice {\n /** USD per million input tokens. */\n inputPerMTok: number;\n /** USD per million output tokens. */\n outputPerMTok: number;\n}\n\n/** Prices in USD per million tokens (MTok). */\nexport const MODEL_PRICING: Record<string, ModelPrice> = {\n \"claude-haiku-4-5\": { inputPerMTok: 1, outputPerMTok: 5 },\n \"claude-sonnet-4-6\": { inputPerMTok: 3, outputPerMTok: 15 },\n \"claude-sonnet-5\": { inputPerMTok: 3, outputPerMTok: 15 },\n \"claude-opus-4-6\": { inputPerMTok: 5, outputPerMTok: 25 },\n \"claude-opus-4-7\": { inputPerMTok: 5, outputPerMTok: 25 },\n \"claude-opus-4-8\": { inputPerMTok: 5, outputPerMTok: 25 },\n \"claude-fable-5\": { inputPerMTok: 10, outputPerMTok: 50 },\n};\n\n/** Compute USD cost for a call. Pass `override` to price a model not in the\n * table (or to apply promotional pricing). Returns 0 for unknown models. */\nexport function costUsd(\n model: string,\n inputTokens: number,\n outputTokens: number,\n override?: ModelPrice,\n): number {\n const price = override ?? MODEL_PRICING[model];\n if (!price) return 0;\n return (inputTokens / 1_000_000) * price.inputPerMTok + (outputTokens / 1_000_000) * price.outputPerMTok;\n}\n","import { SpanKind, SpanStatusCode, trace, type Span } from \"@opentelemetry/api\";\nimport type { Attrs } from \"@odla/o11y-core\";\nimport { SCOPE } from \"./context.js\";\n\nexport type SpanKindName = \"internal\" | \"server\" | \"client\" | \"producer\" | \"consumer\";\n\nexport interface SpanOpts {\n attributes?: Attrs;\n kind?: SpanKindName;\n}\n\nconst KIND: Record<SpanKindName, SpanKind> = {\n internal: SpanKind.INTERNAL,\n server: SpanKind.SERVER,\n client: SpanKind.CLIENT,\n producer: SpanKind.PRODUCER,\n consumer: SpanKind.CONSUMER,\n};\n\n/** Run `fn` inside a new active span. The span is ended automatically and its\n * status set from success/throw. Works whether or not tracing is exporting —\n * falls back to a no-op span if no tracer is installed. */\nexport async function span<T>(\n name: string,\n fn: (span: Span) => Promise<T> | T,\n opts?: SpanOpts,\n): Promise<T> {\n const tracer = trace.getTracer(SCOPE);\n return tracer.startActiveSpan(\n name,\n { attributes: opts?.attributes, kind: opts?.kind ? KIND[opts.kind] : undefined },\n async (s) => {\n try {\n const result = await fn(s);\n s.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (err) {\n s.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) });\n throw err;\n } finally {\n s.end();\n }\n },\n );\n}\n","import type { Attrs } from \"@odla/o11y-core\";\nimport { currentSink, nowNano } from \"./context.js\";\n\nexport interface Metrics {\n /** Monotonic counter (delta). Formalizes odla-db's usage counters. */\n count(name: string, by?: number, attrs?: Attrs): void;\n /** Last-value gauge. */\n gauge(name: string, value: number, attrs?: Attrs): void;\n /** A single observation (recorded as a delta sum for now; histograms later). */\n histogram(name: string, value: number, attrs?: Attrs, unit?: string): void;\n}\n\nfunction record(kind: \"sum\" | \"gauge\", name: string, value: number, attrs?: Attrs, unit?: string): void {\n const sink = currentSink();\n if (!sink) return; // outside an instrumented invocation — no-op\n const ts = nowNano();\n sink.metrics.push({ name, unit, kind, value, attrs: attrs ?? {}, startUnixNano: ts, timeUnixNano: ts });\n}\n\n/** The ambient metrics API for the current invocation. */\nexport function metrics(): Metrics {\n return {\n count: (name, by = 1, attrs) => record(\"sum\", name, by, attrs),\n gauge: (name, value, attrs) => record(\"gauge\", name, value, attrs),\n histogram: (name, value, attrs, unit) => record(\"sum\", name, value, attrs, unit),\n };\n}\n\n/** Convenience: increment a monotonic counter by `by` (default 1). */\nexport function count(name: string, by = 1, attrs?: Attrs): void {\n record(\"sum\", name, by, attrs);\n}\n","import { SpanStatusCode, trace } from \"@opentelemetry/api\";\nimport type { Attrs } from \"@odla/o11y-core\";\nimport { currentSink, nowNano } from \"./context.js\";\n\nexport interface ErrorReport {\n /** Stable, low-cardinality error code (e.g. \"unique_violation\"). */\n code?: string;\n /** Low-cardinality route/step where it happened. */\n route?: string;\n /** Extra low-cardinality attributes for the index. */\n attributes?: Attrs;\n /** Rich context for triage — kept only in the R2 artifact bundle. */\n artifacts?: Record<string, unknown>;\n /** Dedup key; the collector fingerprints by (type+code+route) if absent. */\n fingerprint?: string;\n}\n\n// OTel severity number for ERROR.\nconst SEVERITY_ERROR = 17;\n\n/** Record a structured error. Returns an artifact id. The message and stack ride\n * the log body + a denied attribute so the collector persists them ONLY in the\n * R2 artifact bundle, never in metrics or the queryable index. */\nexport function recordError(err: unknown, report?: ErrorReport): string {\n const error = err instanceof Error ? err : new Error(String(err));\n const artifactId = `${Date.now().toString(16)}${crypto.randomUUID().replace(/-/g, \"\")}`;\n const span = trace.getActiveSpan();\n\n const attrs: Attrs = {\n \"error.type\": error.name,\n \"odla.artifact_id\": artifactId,\n ...(report?.code ? { \"error.code\": report.code } : {}),\n ...(report?.route ? { \"odla.route\": report.route } : {}),\n ...(report?.fingerprint ? { \"odla.fingerprint\": report.fingerprint } : {}),\n ...(report?.attributes ?? {}),\n };\n\n if (span) {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n span.setAttributes(attrs);\n }\n\n const sink = currentSink();\n if (sink) {\n const ctx = span?.spanContext();\n sink.logs.push({\n timeUnixNano: nowNano(),\n severityNumber: SEVERITY_ERROR,\n severityText: \"ERROR\",\n body: error.message, // full message → R2 bundle only (collector-enforced)\n attrs: {\n ...attrs,\n // Denied keys: stripped from metrics/index, retained in the R2 bundle.\n ...(error.stack ? { \"exception.stacktrace\": error.stack } : {}),\n ...(report?.artifacts ? { \"odla.artifacts\": JSON.stringify(report.artifacts) } : {}),\n },\n ...(ctx ? { traceId: ctx.traceId, spanId: ctx.spanId } : {}),\n });\n }\n\n return artifactId;\n}\n","import { trace } from \"@opentelemetry/api\";\nimport { costUsd, type Attrs, type ModelPrice } from \"@odla/o11y-core\";\nimport { count } from \"./metrics.js\";\n\n/** The token accounting odla-kg's Provider already returns (and drops today). */\nexport interface LlmUsage {\n calls: number;\n inputTokens: number;\n outputTokens: number;\n}\n\nexport interface LlmCostOpts {\n provider: string; // \"claude\" | \"openai\" | ...\n model: string; // e.g. \"claude-sonnet-4-6\"\n operation?: string; // \"extract\" | \"search\" | ...\n attributes?: Attrs;\n /** Override the built-in price table for this call. */\n price?: ModelPrice;\n}\n\n/** Turn LLM token usage into first-class cost. Emits cost/token/call counters\n * and stamps the active span with GenAI semconv attributes. */\nexport function recordLlmUsage(usage: LlmUsage, opts: LlmCostOpts): { costUsd: number } {\n const cost = costUsd(opts.model, usage.inputTokens, usage.outputTokens, opts.price);\n const base: Attrs = {\n \"gen_ai.provider.name\": opts.provider,\n \"gen_ai.request.model\": opts.model,\n ...(opts.operation ? { \"gen_ai.operation.name\": opts.operation } : {}),\n ...(opts.attributes ?? {}),\n };\n\n count(\"odla.llm.cost.usd\", cost, base);\n count(\"odla.llm.calls\", usage.calls, base);\n count(\"odla.llm.tokens\", usage.inputTokens, { ...base, \"gen_ai.token.type\": \"input\" });\n count(\"odla.llm.tokens\", usage.outputTokens, { ...base, \"gen_ai.token.type\": \"output\" });\n\n trace.getActiveSpan()?.setAttributes({\n ...base,\n \"gen_ai.usage.input_tokens\": usage.inputTokens,\n \"gen_ai.usage.output_tokens\": usage.outputTokens,\n \"odla.llm.cost.usd\": cost,\n });\n\n return { costUsd: cost };\n}\n"],"mappings":"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","names":["metrics","SpanStatusCode","trace","span","trace","SpanStatusCode","trace","trace"]}
package/llms.txt ADDED
@@ -0,0 +1,156 @@
1
+ # @odla-ai/o11y — LLM context
2
+
3
+ > Official observability client for odla Cloudflare Workers — OpenTelemetry traces, metrics, structured errors, and LLM cost, exported over OTLP to the odla-o11y collector.
4
+
5
+ Official observability client for [odla](https://github.com/) Cloudflare Workers.
6
+ Wrap your Worker once and get OpenTelemetry **traces**, **metrics**, structured
7
+ **errors**, and **LLM cost** — exported over OTLP to the odla-o11y collector.
8
+
9
+ - Traces via [`@microlabs/otel-cf-workers`](https://github.com/evanderkoogh/otel-cf-workers) (auto-instruments incoming/outgoing fetch, Durable Objects, and bindings).
10
+ - Metrics + logs via lightweight OTLP/JSON emitters (the traces-only otel library doesn't cover them, and the Node metrics SDK doesn't fit the isolate model).
11
+ - Targets the Cloudflare Workers runtime; requires the `nodejs_compat` flag.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm i @odla-ai/o11y
17
+ ```
18
+
19
+ ## Use
20
+
21
+ ```ts
22
+ import { withObservability, span, count, recordError, recordLlmUsage } from "@odla-ai/o11y";
23
+
24
+ const handler = {
25
+ async fetch(req: Request, env: Env): Promise<Response> {
26
+ count("http.requests", 1, { "http.route": new URL(req.url).pathname });
27
+ return span("handle", async () => new Response("ok"), { kind: "server" });
28
+ },
29
+ } satisfies ExportedHandler<Env>;
30
+
31
+ export default withObservability(handler);
32
+ ```
33
+
34
+ Wrap a Durable Object class with `instrumentDurableObject(MyDO)`.
35
+
36
+ ## Configuration
37
+
38
+ Config is read from env vars (override per call via the second `withObservability` arg):
39
+
40
+ | Var | Meaning |
41
+ | --- | --- |
42
+ | `ODLA_O11Y_ENDPOINT` | Collector base URL (e.g. `https://odla-o11y.workers.dev`) |
43
+ | `ODLA_O11Y_SERVICE` | This service's name |
44
+ | `ODLA_O11Y_TOKEN` | Per-service bearer token (secret) |
45
+ | `ODLA_O11Y_VERSION` | Release tag (optional) |
46
+
47
+ `wrangler.jsonc` needs `"compatibility_flags": ["nodejs_compat"]` (for AsyncLocalStorage).
48
+
49
+ ## API
50
+
51
+ - `withObservability(handler, opts?)` — wrap a Worker handler (fetch/scheduled).
52
+ - `instrumentDurableObject(cls, opts?)` — wrap a Durable Object class.
53
+ - `span(name, fn, opts?)` — run `fn` inside an active span.
54
+ - `count(name, by?, attrs?)` / `metrics()` — counters, gauges, histograms.
55
+ - `recordError(err, report?)` — structured error; message/stack go only to the
56
+ collector's R2 artifact bundle, never to metrics.
57
+ - `recordLlmUsage(usage, opts)` — turn `{calls, inputTokens, outputTokens}` into
58
+ cost + token metrics and GenAI span attributes.
59
+
60
+ ## License
61
+
62
+ MIT
63
+
64
+ ## API reference (generated from dist/index.d.ts, v1.0.0)
65
+
66
+ ```ts
67
+ import { Attrs, ModelPrice } from '@odla/o11y-core';
68
+ export { Attrs } from '@odla/o11y-core';
69
+ import { Span } from '@opentelemetry/api';
70
+
71
+ interface ObservabilityOptions {
72
+ /** Service name (default: env.ODLA_O11Y_SERVICE). */
73
+ service?: string;
74
+ /** Collector base URL (default: env.ODLA_O11Y_ENDPOINT). */
75
+ endpoint?: string;
76
+ /** Per-service bearer token (default: env.ODLA_O11Y_TOKEN). */
77
+ token?: string;
78
+ /** Release/version tag (default: env.ODLA_O11Y_VERSION ?? "0.0.0"). */
79
+ version?: string;
80
+ /** Head-sampling ratio 0..1 for traces (default: 1). */
81
+ sampleRatio?: number;
82
+ /** Static resource attributes merged onto every signal. */
83
+ attributes?: Attrs;
84
+ }
85
+ type OptsFn<E> = ObservabilityOptions | ((env: E) => ObservabilityOptions);
86
+
87
+ /** Wrap a Worker handler with tracing (otel-cf-workers) + the metrics/logs sink.
88
+ * Config is read from env vars ODLA_O11Y_{ENDPOINT,SERVICE,TOKEN,VERSION} unless
89
+ * overridden by `opts`. */
90
+ declare function withObservability<E = unknown>(handler: ExportedHandler<E>, opts?: OptsFn<E>): ExportedHandler<E>;
91
+ /** Wrap a Durable Object class with tracing. (Establishing the metrics/logs sink
92
+ * inside DO methods is added in P1 when odla-db's usage counters are bridged.) */
93
+ declare function instrumentDurableObject<T extends abstract new (...args: any[]) => object>(cls: T, opts?: OptsFn<any>): T;
94
+
95
+ type SpanKindName = "internal" | "server" | "client" | "producer" | "consumer";
96
+ interface SpanOpts {
97
+ attributes?: Attrs;
98
+ kind?: SpanKindName;
99
+ }
100
+ /** Run `fn` inside a new active span. The span is ended automatically and its
101
+ * status set from success/throw. Works whether or not tracing is exporting —
102
+ * falls back to a no-op span if no tracer is installed. */
103
+ declare function span<T>(name: string, fn: (span: Span) => Promise<T> | T, opts?: SpanOpts): Promise<T>;
104
+
105
+ interface Metrics {
106
+ /** Monotonic counter (delta). Formalizes odla-db's usage counters. */
107
+ count(name: string, by?: number, attrs?: Attrs): void;
108
+ /** Last-value gauge. */
109
+ gauge(name: string, value: number, attrs?: Attrs): void;
110
+ /** A single observation (recorded as a delta sum for now; histograms later). */
111
+ histogram(name: string, value: number, attrs?: Attrs, unit?: string): void;
112
+ }
113
+ /** The ambient metrics API for the current invocation. */
114
+ declare function metrics(): Metrics;
115
+ /** Convenience: increment a monotonic counter by `by` (default 1). */
116
+ declare function count(name: string, by?: number, attrs?: Attrs): void;
117
+
118
+ interface ErrorReport {
119
+ /** Stable, low-cardinality error code (e.g. "unique_violation"). */
120
+ code?: string;
121
+ /** Low-cardinality route/step where it happened. */
122
+ route?: string;
123
+ /** Extra low-cardinality attributes for the index. */
124
+ attributes?: Attrs;
125
+ /** Rich context for triage — kept only in the R2 artifact bundle. */
126
+ artifacts?: Record<string, unknown>;
127
+ /** Dedup key; the collector fingerprints by (type+code+route) if absent. */
128
+ fingerprint?: string;
129
+ }
130
+ /** Record a structured error. Returns an artifact id. The message and stack ride
131
+ * the log body + a denied attribute so the collector persists them ONLY in the
132
+ * R2 artifact bundle, never in metrics or the queryable index. */
133
+ declare function recordError(err: unknown, report?: ErrorReport): string;
134
+
135
+ /** The token accounting odla-kg's Provider already returns (and drops today). */
136
+ interface LlmUsage {
137
+ calls: number;
138
+ inputTokens: number;
139
+ outputTokens: number;
140
+ }
141
+ interface LlmCostOpts {
142
+ provider: string;
143
+ model: string;
144
+ operation?: string;
145
+ attributes?: Attrs;
146
+ /** Override the built-in price table for this call. */
147
+ price?: ModelPrice;
148
+ }
149
+ /** Turn LLM token usage into first-class cost. Emits cost/token/call counters
150
+ * and stamps the active span with GenAI semconv attributes. */
151
+ declare function recordLlmUsage(usage: LlmUsage, opts: LlmCostOpts): {
152
+ costUsd: number;
153
+ };
154
+
155
+ export { type ErrorReport, type LlmCostOpts, type LlmUsage, type Metrics, type ObservabilityOptions, type OptsFn, type SpanKindName, type SpanOpts, count, instrumentDurableObject, metrics, recordError, recordLlmUsage, span, withObservability };
156
+ ```
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@odla-ai/o11y",
3
+ "version": "1.0.0",
4
+ "description": "Official observability client for odla Cloudflare Workers — OpenTelemetry traces, metrics, structured errors, and LLM cost, exported over OTLP to the odla-o11y collector.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "files": ["dist", "README.md", "llms.txt"],
18
+ "sideEffects": false,
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "keywords": ["odla", "odla-o11y", "observability", "opentelemetry", "otlp", "tracing", "metrics", "llm-cost", "cloudflare", "workers"],
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "clean": "rm -rf dist",
26
+ "test": "vitest run --passWithNoTests",
27
+ "gen:llms": "node ../../scripts/gen-llms.mjs",
28
+ "prepublishOnly": "npm run build && npm run gen:llms"
29
+ },
30
+ "dependencies": {
31
+ "@microlabs/otel-cf-workers": "^1.0.0-rc.52",
32
+ "@opentelemetry/api": "^1.9.0"
33
+ },
34
+ "devDependencies": {
35
+ "@odla/o11y-core": "*",
36
+ "tsup": "^8.5.1",
37
+ "typescript": "^6.0.3"
38
+ }
39
+ }