@odla-ai/o11y 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cory Ondrejka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # @odla-ai/o11y
2
2
 
3
+ > ⚠️ **Experimental — an agentic-coding experiment.** This package is built and
4
+ > operated by autonomous coding agents as an experiment in agentic loops. APIs
5
+ > change without notice and nothing here is production-hardened. **Use at your
6
+ > own risk.**
7
+
3
8
  Official observability client for [odla](https://github.com/) Cloudflare Workers.
4
9
  Wrap your Worker once and get OpenTelemetry **traces**, **metrics**, structured
5
10
  **errors**, and **LLM cost** — exported over OTLP to the odla-o11y collector.
@@ -44,6 +49,16 @@ Config is read from env vars (override per call via the second `withObservabilit
44
49
 
45
50
  `wrangler.jsonc` needs `"compatibility_flags": ["nodejs_compat"]` (for AsyncLocalStorage).
46
51
 
52
+ ### Private ingest via a service binding
53
+
54
+ By default all signals export over `fetch` to `ODLA_O11Y_ENDPOINT`. If the Worker
55
+ is bound to the collector (a service binding named `ODLA_O11Y_COLLECTOR`, or one
56
+ passed as `opts.fetcher`), the SDK routes **traces, metrics, and logs** through
57
+ that binding instead — private worker-to-worker, no public hop, and the endpoint
58
+ host becomes irrelevant. This is the transport odla's first-party hosting uses;
59
+ the binding and its credential are injected at deploy time, so instrumented code
60
+ stays `export default withObservability(handler)` with nothing else to set.
61
+
47
62
  Keep `ODLA_O11Y_ENDPOINT` and `ODLA_O11Y_SERVICE` set as plain vars in every
48
63
  environment and treat only `ODLA_O11Y_TOKEN` as an optional secret: the
49
64
  metrics/logs sink no-ops without an endpoint, but the trace exporter builds
package/dist/index.cjs CHANGED
@@ -89,6 +89,29 @@ function encodeLogsRequest(resource, scopeName, records) {
89
89
  };
90
90
  }
91
91
 
92
+ // ../o11y-core/src/encode-trace.ts
93
+ function encodeTraceRequest(resource, scopeName, spans) {
94
+ const otlpSpans = spans.map((s) => ({
95
+ traceId: s.traceId,
96
+ spanId: s.spanId,
97
+ ...s.parentSpanId ? { parentSpanId: s.parentSpanId } : {},
98
+ name: s.name,
99
+ kind: s.kind,
100
+ startTimeUnixNano: s.startUnixNano,
101
+ endTimeUnixNano: s.endUnixNano,
102
+ attributes: attrsToKvList(s.attributes),
103
+ status: { code: s.statusCode }
104
+ }));
105
+ return {
106
+ resourceSpans: [
107
+ {
108
+ resource: { attributes: attrsToKvList(resource) },
109
+ scopeSpans: [{ scope: { name: scopeName }, spans: otlpSpans }]
110
+ }
111
+ ]
112
+ };
113
+ }
114
+
92
115
  // ../o11y-core/src/pricing.ts
93
116
  var MODEL_PRICING = {
94
117
  "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 },
@@ -107,19 +130,25 @@ function costUsd(model, inputTokens, outputTokens, override) {
107
130
 
108
131
  // src/context.ts
109
132
  var SCOPE = "@odla-ai/o11y";
133
+ function asBinding(x) {
134
+ return x && typeof x.fetch === "function" ? x : void 0;
135
+ }
110
136
  function evalOpts(opts, env) {
111
137
  return typeof opts === "function" ? opts(env) : opts ?? {};
112
138
  }
113
139
  function resolveConfig(env, opts) {
114
140
  const o = evalOpts(opts, env);
115
141
  const e = env;
142
+ const fetcher = o.fetcher ?? asBinding(env["ODLA_O11Y_COLLECTOR"]);
143
+ const endpoint = (o.endpoint ?? e["ODLA_O11Y_ENDPOINT"] ?? (fetcher ? "https://svc.internal" : "")).replace(/\/$/, "");
116
144
  return {
117
145
  service: o.service ?? e["ODLA_O11Y_SERVICE"] ?? "unknown",
118
- endpoint: (o.endpoint ?? e["ODLA_O11Y_ENDPOINT"] ?? "").replace(/\/$/, ""),
146
+ endpoint,
119
147
  token: o.token ?? e["ODLA_O11Y_TOKEN"],
120
148
  version: o.version ?? e["ODLA_O11Y_VERSION"] ?? "0.0.0",
121
149
  attributes: o.attributes ?? {},
122
- sampleRatio: o.sampleRatio
150
+ sampleRatio: o.sampleRatio,
151
+ fetcher
123
152
  };
124
153
  }
125
154
  function nowNano() {
@@ -135,16 +164,17 @@ function runWithSink(sink, fn) {
135
164
  function resourceAttrs(config) {
136
165
  return { "service.name": config.service, "service.version": config.version, ...config.attributes };
137
166
  }
138
- async function post(url, body, token) {
167
+ async function post(url, body, token, fetcher) {
168
+ const init = {
169
+ method: "POST",
170
+ headers: {
171
+ "content-type": "application/json",
172
+ ...token ? { authorization: `Bearer ${token}` } : {}
173
+ },
174
+ body: JSON.stringify(body)
175
+ };
139
176
  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
- });
177
+ await (fetcher ? fetcher.fetch(url, init) : fetch(url, init));
148
178
  } catch {
149
179
  }
150
180
  }
@@ -161,15 +191,16 @@ function createSink(config) {
161
191
  }
162
192
  const resource = resourceAttrs(this.config);
163
193
  const jobs = [];
194
+ const { token, fetcher } = this.config;
164
195
  if (this.metrics.length) {
165
196
  jobs.push(
166
- post(`${this.config.endpoint}/v1/metrics`, encodeMetricsRequest(resource, SCOPE, this.metrics), this.config.token)
197
+ post(`${this.config.endpoint}/v1/metrics`, encodeMetricsRequest(resource, SCOPE, this.metrics), token, fetcher)
167
198
  );
168
199
  this.metrics = [];
169
200
  }
170
201
  if (this.logs.length) {
171
202
  jobs.push(
172
- post(`${this.config.endpoint}/v1/logs`, encodeLogsRequest(resource, SCOPE, this.logs), this.config.token)
203
+ post(`${this.config.endpoint}/v1/logs`, encodeLogsRequest(resource, SCOPE, this.logs), token, fetcher)
173
204
  );
174
205
  this.logs = [];
175
206
  }
@@ -178,13 +209,68 @@ function createSink(config) {
178
209
  };
179
210
  }
180
211
 
212
+ // src/trace-export.ts
213
+ var SUCCESS = 0;
214
+ var FAILED = 1;
215
+ var OTLP_KIND_OFFSET = 1;
216
+ function hrToNano([seconds, nanos]) {
217
+ return (BigInt(seconds) * 1000000000n + BigInt(nanos)).toString();
218
+ }
219
+ function toSpanInput(span2) {
220
+ const ctx = span2.spanContext();
221
+ const attributes = {};
222
+ for (const [key, value] of Object.entries(span2.attributes)) {
223
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
224
+ attributes[key] = value;
225
+ }
226
+ }
227
+ return {
228
+ traceId: ctx.traceId,
229
+ spanId: ctx.spanId,
230
+ parentSpanId: span2.parentSpanContext?.spanId ?? "",
231
+ name: span2.name,
232
+ kind: span2.kind + OTLP_KIND_OFFSET,
233
+ statusCode: span2.status.code,
234
+ startUnixNano: hrToNano(span2.startTime),
235
+ endUnixNano: hrToNano(span2.endTime),
236
+ attributes
237
+ };
238
+ }
239
+ var BindingSpanExporter = class {
240
+ constructor(binding, url, headers, resource, scope) {
241
+ this.binding = binding;
242
+ this.url = url;
243
+ this.headers = headers;
244
+ this.resource = resource;
245
+ this.scope = scope;
246
+ }
247
+ binding;
248
+ url;
249
+ headers;
250
+ resource;
251
+ scope;
252
+ export(spans, resultCallback) {
253
+ const body = encodeTraceRequest(this.resource, this.scope, spans.map(toSpanInput));
254
+ this.binding.fetch(this.url, {
255
+ method: "POST",
256
+ headers: { "content-type": "application/json", ...this.headers },
257
+ body: JSON.stringify(body)
258
+ }).then(() => resultCallback({ code: SUCCESS })).catch((error) => resultCallback({ code: FAILED, error }));
259
+ }
260
+ async shutdown() {
261
+ }
262
+ };
263
+
181
264
  // src/instrument.ts
182
265
  function traceConfig(env, opts) {
183
266
  const c = resolveConfig(env, opts);
184
267
  const headers = {};
185
268
  if (c.token) headers.authorization = `Bearer ${c.token}`;
269
+ const url = `${c.endpoint}/v1/traces`;
186
270
  const config = {
187
- exporter: { url: `${c.endpoint}/v1/traces`, headers },
271
+ // A service binding needs a custom exporter (the stock OTLP exporter posts
272
+ // over the global fetch); without one, fall back to the public URL exporter.
273
+ exporter: c.fetcher ? new BindingSpanExporter(c.fetcher, url, headers, resourceAttrs(c), SCOPE) : { url, headers },
188
274
  service: { name: c.service, version: c.version }
189
275
  };
190
276
  if (c.sampleRatio != null) config.sampling = { headSampler: { ratio: c.sampleRatio } };
@@ -1 +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"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/instrument.ts","../src/context.ts","../../o11y-core/src/encode.ts","../../o11y-core/src/encode-trace.ts","../../o11y-core/src/pricing.ts","../src/trace-export.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 { CollectorBinding, 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, resourceAttrs, runWithSink, SCOPE, type OptsFn } from \"./context.js\";\nimport { BindingSpanExporter } from \"./trace-export.js\";\n\n/** Build the otel-cf-workers trace config (exporter + service) from env/opts.\n * Exported for tests; not part of the package's public surface (see index.ts). */\nexport function 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 url = `${c.endpoint}/v1/traces`;\n const config: TraceConfig = {\n // A service binding needs a custom exporter (the stock OTLP exporter posts\n // over the global fetch); without one, fall back to the public URL exporter.\n exporter: c.fetcher\n ? new BindingSpanExporter(c.fetcher, url, headers, resourceAttrs(c), SCOPE)\n : { url, 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.\n * Exported for tests; not part of the package's public surface (see index.ts). */\nexport function 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\n/** A Cloudflare service binding to the collector Worker (structural — avoids a\n * hard dependency on `@cloudflare/workers-types`). When present the SDK exports\n * every signal through `binding.fetch` (private worker-to-worker) instead of\n * the public endpoint; first-party hosting injects it as `env.ODLA_O11Y_COLLECTOR`. */\nexport interface CollectorBinding {\n fetch: typeof fetch;\n}\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 /** Service binding to the collector. Defaults to an `ODLA_O11Y_COLLECTOR`\n * binding on env; when set, all signals export through it instead of `fetch`. */\n fetcher?: CollectorBinding;\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 fetcher?: CollectorBinding;\n}\n\n/** Narrow an arbitrary env binding to a CollectorBinding (has a `fetch` method). */\nfunction asBinding(x: unknown): CollectorBinding | undefined {\n return x && typeof (x as CollectorBinding).fetch === \"function\" ? (x as CollectorBinding) : undefined;\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 const fetcher = o.fetcher ?? asBinding((env as Record<string, unknown>)[\"ODLA_O11Y_COLLECTOR\"]);\n // A binding ignores the URL host (it routes to the bound Worker), but the SDK\n // still needs a well-formed base to build /v1/* paths — default it internally.\n const endpoint = (o.endpoint ?? e[\"ODLA_O11Y_ENDPOINT\"] ?? (fetcher ? \"https://svc.internal\" : \"\")).replace(/\\/$/, \"\");\n return {\n service: o.service ?? e[\"ODLA_O11Y_SERVICE\"] ?? \"unknown\",\n endpoint,\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 fetcher,\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\n/** Resource attributes stamped on every signal. Exported so the trace exporter\n * builds the same resource the metrics/logs sink does. */\nexport function 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, fetcher?: CollectorBinding): Promise<void> {\n const init = {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...(token ? { authorization: `Bearer ${token}` } : {}),\n },\n body: JSON.stringify(body),\n };\n try {\n await (fetcher ? fetcher.fetch(url, init) : fetch(url, init));\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 const { token, fetcher } = this.config;\n if (this.metrics.length) {\n jobs.push(\n post(`${this.config.endpoint}/v1/metrics`, encodeMetricsRequest(resource, SCOPE, this.metrics), token, fetcher),\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), token, fetcher),\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","// OTLP/JSON trace encoder — the emit-side inverse of `decodeTraces` (otlp.ts).\n// The default SDK path produces spans via @microlabs/otel-cf-workers, but that\n// library's exporter posts over the *global* fetch and can't be pointed at a\n// Cloudflare service binding. When a Worker exports through a binding instead\n// (first-party hosting, private ingest), the SDK hand-encodes its spans here so\n// the whole batch rides the same private transport as metrics and logs.\n\nimport { attrsToKvList } from \"./encode.js\";\nimport type { Attrs, ExportTraceServiceRequest, OtlpSpan } from \"./otlp.js\";\n\n/** The flat span shape the SDK hands the encoder. Field semantics mirror the\n * OTLP wire fields decoded in `decodeTraces`: `kind` is the OTLP span kind\n * (1 INTERNAL .. 5 CONSUMER) and `statusCode` is 0 UNSET / 1 OK / 2 ERROR. */\nexport interface SpanInput {\n traceId: string;\n spanId: string;\n parentSpanId: string;\n name: string;\n kind: number;\n statusCode: number;\n startUnixNano: string;\n endUnixNano: string;\n attributes: Attrs;\n}\n\n/** Encode a batch of spans (one service + scope) as an OTLP/JSON\n * ExportTraceServiceRequest — the exact shape `decodeTraces` consumes. */\nexport function encodeTraceRequest(\n resource: Attrs,\n scopeName: string,\n spans: SpanInput[],\n): ExportTraceServiceRequest {\n const otlpSpans: OtlpSpan[] = spans.map((s) => ({\n traceId: s.traceId,\n spanId: s.spanId,\n ...(s.parentSpanId ? { parentSpanId: s.parentSpanId } : {}),\n name: s.name,\n kind: s.kind,\n startTimeUnixNano: s.startUnixNano,\n endTimeUnixNano: s.endUnixNano,\n attributes: attrsToKvList(s.attributes),\n status: { code: s.statusCode },\n }));\n return {\n resourceSpans: [\n {\n resource: { attributes: attrsToKvList(resource) },\n scopeSpans: [{ scope: { name: scopeName }, spans: otlpSpans }],\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 type { ExportResult } from \"@opentelemetry/core\";\nimport type { ReadableSpan, SpanExporter } from \"@opentelemetry/sdk-trace-base\";\nimport { encodeTraceRequest, type Attrs, type SpanInput } from \"@odla/o11y-core\";\nimport type { CollectorBinding } from \"./context.js\";\n\n// ExportResultCode: 0 = SUCCESS, 1 = FAILED (@opentelemetry/core). Kept as\n// literals so we don't take a runtime dependency on the enum.\nconst SUCCESS = 0;\nconst FAILED = 1;\n\n// OTLP span kind is the OpenTelemetry SDK `SpanKind` (INTERNAL=0 .. CONSUMER=4)\n// shifted by one (OTLP INTERNAL=1 .. CONSUMER=5).\nconst OTLP_KIND_OFFSET = 1;\n\n/** OTel `HrTime` ([seconds, nanos]) → a decimal-nanosecond string (OTLP). */\nfunction hrToNano([seconds, nanos]: [number, number]): string {\n return (BigInt(seconds) * 1_000_000_000n + BigInt(nanos)).toString();\n}\n\n/** Project an OTel ReadableSpan onto the flat shape the core encoder wants,\n * dropping non-primitive attribute values (arrays/objects) the wire can't hold. */\nexport function toSpanInput(span: ReadableSpan): SpanInput {\n const ctx = span.spanContext();\n const attributes: Attrs = {};\n for (const [key, value] of Object.entries(span.attributes)) {\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n attributes[key] = value;\n }\n }\n return {\n traceId: ctx.traceId,\n spanId: ctx.spanId,\n parentSpanId: span.parentSpanContext?.spanId ?? \"\",\n name: span.name,\n kind: span.kind + OTLP_KIND_OFFSET,\n statusCode: span.status.code,\n startUnixNano: hrToNano(span.startTime),\n endUnixNano: hrToNano(span.endTime),\n attributes,\n };\n}\n\n/** A SpanExporter that ships OTLP/JSON spans through a Cloudflare service\n * binding instead of the global `fetch` the stock OTLP exporter uses — so\n * traces travel the same private worker-to-worker path as metrics and logs. */\nexport class BindingSpanExporter implements SpanExporter {\n constructor(\n private readonly binding: CollectorBinding,\n private readonly url: string,\n private readonly headers: Record<string, string>,\n private readonly resource: Attrs,\n private readonly scope: string,\n ) {}\n\n export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {\n const body = encodeTraceRequest(this.resource, this.scope, spans.map(toSpanInput));\n this.binding\n .fetch(this.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", ...this.headers },\n body: JSON.stringify(body),\n })\n .then(() => resultCallback({ code: SUCCESS }))\n .catch((error: Error) => resultCallback({ code: FAILED, error }));\n }\n\n async shutdown(): Promise<void> {}\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","span","import_api","span","import_api"]}
package/dist/index.d.cts CHANGED
@@ -2,6 +2,13 @@ import { Attrs, ModelPrice } from '@odla/o11y-core';
2
2
  export { Attrs } from '@odla/o11y-core';
3
3
  import { Span } from '@opentelemetry/api';
4
4
 
5
+ /** A Cloudflare service binding to the collector Worker (structural — avoids a
6
+ * hard dependency on `@cloudflare/workers-types`). When present the SDK exports
7
+ * every signal through `binding.fetch` (private worker-to-worker) instead of
8
+ * the public endpoint; first-party hosting injects it as `env.ODLA_O11Y_COLLECTOR`. */
9
+ interface CollectorBinding {
10
+ fetch: typeof fetch;
11
+ }
5
12
  interface ObservabilityOptions {
6
13
  /** Service name (default: env.ODLA_O11Y_SERVICE). */
7
14
  service?: string;
@@ -15,6 +22,9 @@ interface ObservabilityOptions {
15
22
  sampleRatio?: number;
16
23
  /** Static resource attributes merged onto every signal. */
17
24
  attributes?: Attrs;
25
+ /** Service binding to the collector. Defaults to an `ODLA_O11Y_COLLECTOR`
26
+ * binding on env; when set, all signals export through it instead of `fetch`. */
27
+ fetcher?: CollectorBinding;
18
28
  }
19
29
  type OptsFn<E> = ObservabilityOptions | ((env: E) => ObservabilityOptions);
20
30
 
@@ -86,4 +96,4 @@ declare function recordLlmUsage(usage: LlmUsage, opts: LlmCostOpts): {
86
96
  costUsd: number;
87
97
  };
88
98
 
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 };
99
+ export { type CollectorBinding, 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.d.ts CHANGED
@@ -2,6 +2,13 @@ import { Attrs, ModelPrice } from '@odla/o11y-core';
2
2
  export { Attrs } from '@odla/o11y-core';
3
3
  import { Span } from '@opentelemetry/api';
4
4
 
5
+ /** A Cloudflare service binding to the collector Worker (structural — avoids a
6
+ * hard dependency on `@cloudflare/workers-types`). When present the SDK exports
7
+ * every signal through `binding.fetch` (private worker-to-worker) instead of
8
+ * the public endpoint; first-party hosting injects it as `env.ODLA_O11Y_COLLECTOR`. */
9
+ interface CollectorBinding {
10
+ fetch: typeof fetch;
11
+ }
5
12
  interface ObservabilityOptions {
6
13
  /** Service name (default: env.ODLA_O11Y_SERVICE). */
7
14
  service?: string;
@@ -15,6 +22,9 @@ interface ObservabilityOptions {
15
22
  sampleRatio?: number;
16
23
  /** Static resource attributes merged onto every signal. */
17
24
  attributes?: Attrs;
25
+ /** Service binding to the collector. Defaults to an `ODLA_O11Y_COLLECTOR`
26
+ * binding on env; when set, all signals export through it instead of `fetch`. */
27
+ fetcher?: CollectorBinding;
18
28
  }
19
29
  type OptsFn<E> = ObservabilityOptions | ((env: E) => ObservabilityOptions);
20
30
 
@@ -86,4 +96,4 @@ declare function recordLlmUsage(usage: LlmUsage, opts: LlmCostOpts): {
86
96
  costUsd: number;
87
97
  };
88
98
 
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 };
99
+ export { type CollectorBinding, 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 CHANGED
@@ -57,6 +57,29 @@ function encodeLogsRequest(resource, scopeName, records) {
57
57
  };
58
58
  }
59
59
 
60
+ // ../o11y-core/src/encode-trace.ts
61
+ function encodeTraceRequest(resource, scopeName, spans) {
62
+ const otlpSpans = spans.map((s) => ({
63
+ traceId: s.traceId,
64
+ spanId: s.spanId,
65
+ ...s.parentSpanId ? { parentSpanId: s.parentSpanId } : {},
66
+ name: s.name,
67
+ kind: s.kind,
68
+ startTimeUnixNano: s.startUnixNano,
69
+ endTimeUnixNano: s.endUnixNano,
70
+ attributes: attrsToKvList(s.attributes),
71
+ status: { code: s.statusCode }
72
+ }));
73
+ return {
74
+ resourceSpans: [
75
+ {
76
+ resource: { attributes: attrsToKvList(resource) },
77
+ scopeSpans: [{ scope: { name: scopeName }, spans: otlpSpans }]
78
+ }
79
+ ]
80
+ };
81
+ }
82
+
60
83
  // ../o11y-core/src/pricing.ts
61
84
  var MODEL_PRICING = {
62
85
  "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 },
@@ -75,19 +98,25 @@ function costUsd(model, inputTokens, outputTokens, override) {
75
98
 
76
99
  // src/context.ts
77
100
  var SCOPE = "@odla-ai/o11y";
101
+ function asBinding(x) {
102
+ return x && typeof x.fetch === "function" ? x : void 0;
103
+ }
78
104
  function evalOpts(opts, env) {
79
105
  return typeof opts === "function" ? opts(env) : opts ?? {};
80
106
  }
81
107
  function resolveConfig(env, opts) {
82
108
  const o = evalOpts(opts, env);
83
109
  const e = env;
110
+ const fetcher = o.fetcher ?? asBinding(env["ODLA_O11Y_COLLECTOR"]);
111
+ const endpoint = (o.endpoint ?? e["ODLA_O11Y_ENDPOINT"] ?? (fetcher ? "https://svc.internal" : "")).replace(/\/$/, "");
84
112
  return {
85
113
  service: o.service ?? e["ODLA_O11Y_SERVICE"] ?? "unknown",
86
- endpoint: (o.endpoint ?? e["ODLA_O11Y_ENDPOINT"] ?? "").replace(/\/$/, ""),
114
+ endpoint,
87
115
  token: o.token ?? e["ODLA_O11Y_TOKEN"],
88
116
  version: o.version ?? e["ODLA_O11Y_VERSION"] ?? "0.0.0",
89
117
  attributes: o.attributes ?? {},
90
- sampleRatio: o.sampleRatio
118
+ sampleRatio: o.sampleRatio,
119
+ fetcher
91
120
  };
92
121
  }
93
122
  function nowNano() {
@@ -103,16 +132,17 @@ function runWithSink(sink, fn) {
103
132
  function resourceAttrs(config) {
104
133
  return { "service.name": config.service, "service.version": config.version, ...config.attributes };
105
134
  }
106
- async function post(url, body, token) {
135
+ async function post(url, body, token, fetcher) {
136
+ const init = {
137
+ method: "POST",
138
+ headers: {
139
+ "content-type": "application/json",
140
+ ...token ? { authorization: `Bearer ${token}` } : {}
141
+ },
142
+ body: JSON.stringify(body)
143
+ };
107
144
  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
- });
145
+ await (fetcher ? fetcher.fetch(url, init) : fetch(url, init));
116
146
  } catch {
117
147
  }
118
148
  }
@@ -129,15 +159,16 @@ function createSink(config) {
129
159
  }
130
160
  const resource = resourceAttrs(this.config);
131
161
  const jobs = [];
162
+ const { token, fetcher } = this.config;
132
163
  if (this.metrics.length) {
133
164
  jobs.push(
134
- post(`${this.config.endpoint}/v1/metrics`, encodeMetricsRequest(resource, SCOPE, this.metrics), this.config.token)
165
+ post(`${this.config.endpoint}/v1/metrics`, encodeMetricsRequest(resource, SCOPE, this.metrics), token, fetcher)
135
166
  );
136
167
  this.metrics = [];
137
168
  }
138
169
  if (this.logs.length) {
139
170
  jobs.push(
140
- post(`${this.config.endpoint}/v1/logs`, encodeLogsRequest(resource, SCOPE, this.logs), this.config.token)
171
+ post(`${this.config.endpoint}/v1/logs`, encodeLogsRequest(resource, SCOPE, this.logs), token, fetcher)
141
172
  );
142
173
  this.logs = [];
143
174
  }
@@ -146,13 +177,68 @@ function createSink(config) {
146
177
  };
147
178
  }
148
179
 
180
+ // src/trace-export.ts
181
+ var SUCCESS = 0;
182
+ var FAILED = 1;
183
+ var OTLP_KIND_OFFSET = 1;
184
+ function hrToNano([seconds, nanos]) {
185
+ return (BigInt(seconds) * 1000000000n + BigInt(nanos)).toString();
186
+ }
187
+ function toSpanInput(span2) {
188
+ const ctx = span2.spanContext();
189
+ const attributes = {};
190
+ for (const [key, value] of Object.entries(span2.attributes)) {
191
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
192
+ attributes[key] = value;
193
+ }
194
+ }
195
+ return {
196
+ traceId: ctx.traceId,
197
+ spanId: ctx.spanId,
198
+ parentSpanId: span2.parentSpanContext?.spanId ?? "",
199
+ name: span2.name,
200
+ kind: span2.kind + OTLP_KIND_OFFSET,
201
+ statusCode: span2.status.code,
202
+ startUnixNano: hrToNano(span2.startTime),
203
+ endUnixNano: hrToNano(span2.endTime),
204
+ attributes
205
+ };
206
+ }
207
+ var BindingSpanExporter = class {
208
+ constructor(binding, url, headers, resource, scope) {
209
+ this.binding = binding;
210
+ this.url = url;
211
+ this.headers = headers;
212
+ this.resource = resource;
213
+ this.scope = scope;
214
+ }
215
+ binding;
216
+ url;
217
+ headers;
218
+ resource;
219
+ scope;
220
+ export(spans, resultCallback) {
221
+ const body = encodeTraceRequest(this.resource, this.scope, spans.map(toSpanInput));
222
+ this.binding.fetch(this.url, {
223
+ method: "POST",
224
+ headers: { "content-type": "application/json", ...this.headers },
225
+ body: JSON.stringify(body)
226
+ }).then(() => resultCallback({ code: SUCCESS })).catch((error) => resultCallback({ code: FAILED, error }));
227
+ }
228
+ async shutdown() {
229
+ }
230
+ };
231
+
149
232
  // src/instrument.ts
150
233
  function traceConfig(env, opts) {
151
234
  const c = resolveConfig(env, opts);
152
235
  const headers = {};
153
236
  if (c.token) headers.authorization = `Bearer ${c.token}`;
237
+ const url = `${c.endpoint}/v1/traces`;
154
238
  const config = {
155
- exporter: { url: `${c.endpoint}/v1/traces`, headers },
239
+ // A service binding needs a custom exporter (the stock OTLP exporter posts
240
+ // over the global fetch); without one, fall back to the public URL exporter.
241
+ exporter: c.fetcher ? new BindingSpanExporter(c.fetcher, url, headers, resourceAttrs(c), SCOPE) : { url, headers },
156
242
  service: { name: c.service, version: c.version }
157
243
  };
158
244
  if (c.sampleRatio != null) config.sampling = { headSampler: { ratio: c.sampleRatio } };
package/dist/index.js.map CHANGED
@@ -1 +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"]}
1
+ {"version":3,"sources":["../src/instrument.ts","../src/context.ts","../../o11y-core/src/encode.ts","../../o11y-core/src/encode-trace.ts","../../o11y-core/src/pricing.ts","../src/trace-export.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, resourceAttrs, runWithSink, SCOPE, type OptsFn } from \"./context.js\";\nimport { BindingSpanExporter } from \"./trace-export.js\";\n\n/** Build the otel-cf-workers trace config (exporter + service) from env/opts.\n * Exported for tests; not part of the package's public surface (see index.ts). */\nexport function 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 url = `${c.endpoint}/v1/traces`;\n const config: TraceConfig = {\n // A service binding needs a custom exporter (the stock OTLP exporter posts\n // over the global fetch); without one, fall back to the public URL exporter.\n exporter: c.fetcher\n ? new BindingSpanExporter(c.fetcher, url, headers, resourceAttrs(c), SCOPE)\n : { url, 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.\n * Exported for tests; not part of the package's public surface (see index.ts). */\nexport function 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\n/** A Cloudflare service binding to the collector Worker (structural — avoids a\n * hard dependency on `@cloudflare/workers-types`). When present the SDK exports\n * every signal through `binding.fetch` (private worker-to-worker) instead of\n * the public endpoint; first-party hosting injects it as `env.ODLA_O11Y_COLLECTOR`. */\nexport interface CollectorBinding {\n fetch: typeof fetch;\n}\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 /** Service binding to the collector. Defaults to an `ODLA_O11Y_COLLECTOR`\n * binding on env; when set, all signals export through it instead of `fetch`. */\n fetcher?: CollectorBinding;\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 fetcher?: CollectorBinding;\n}\n\n/** Narrow an arbitrary env binding to a CollectorBinding (has a `fetch` method). */\nfunction asBinding(x: unknown): CollectorBinding | undefined {\n return x && typeof (x as CollectorBinding).fetch === \"function\" ? (x as CollectorBinding) : undefined;\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 const fetcher = o.fetcher ?? asBinding((env as Record<string, unknown>)[\"ODLA_O11Y_COLLECTOR\"]);\n // A binding ignores the URL host (it routes to the bound Worker), but the SDK\n // still needs a well-formed base to build /v1/* paths — default it internally.\n const endpoint = (o.endpoint ?? e[\"ODLA_O11Y_ENDPOINT\"] ?? (fetcher ? \"https://svc.internal\" : \"\")).replace(/\\/$/, \"\");\n return {\n service: o.service ?? e[\"ODLA_O11Y_SERVICE\"] ?? \"unknown\",\n endpoint,\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 fetcher,\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\n/** Resource attributes stamped on every signal. Exported so the trace exporter\n * builds the same resource the metrics/logs sink does. */\nexport function 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, fetcher?: CollectorBinding): Promise<void> {\n const init = {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...(token ? { authorization: `Bearer ${token}` } : {}),\n },\n body: JSON.stringify(body),\n };\n try {\n await (fetcher ? fetcher.fetch(url, init) : fetch(url, init));\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 const { token, fetcher } = this.config;\n if (this.metrics.length) {\n jobs.push(\n post(`${this.config.endpoint}/v1/metrics`, encodeMetricsRequest(resource, SCOPE, this.metrics), token, fetcher),\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), token, fetcher),\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","// OTLP/JSON trace encoder — the emit-side inverse of `decodeTraces` (otlp.ts).\n// The default SDK path produces spans via @microlabs/otel-cf-workers, but that\n// library's exporter posts over the *global* fetch and can't be pointed at a\n// Cloudflare service binding. When a Worker exports through a binding instead\n// (first-party hosting, private ingest), the SDK hand-encodes its spans here so\n// the whole batch rides the same private transport as metrics and logs.\n\nimport { attrsToKvList } from \"./encode.js\";\nimport type { Attrs, ExportTraceServiceRequest, OtlpSpan } from \"./otlp.js\";\n\n/** The flat span shape the SDK hands the encoder. Field semantics mirror the\n * OTLP wire fields decoded in `decodeTraces`: `kind` is the OTLP span kind\n * (1 INTERNAL .. 5 CONSUMER) and `statusCode` is 0 UNSET / 1 OK / 2 ERROR. */\nexport interface SpanInput {\n traceId: string;\n spanId: string;\n parentSpanId: string;\n name: string;\n kind: number;\n statusCode: number;\n startUnixNano: string;\n endUnixNano: string;\n attributes: Attrs;\n}\n\n/** Encode a batch of spans (one service + scope) as an OTLP/JSON\n * ExportTraceServiceRequest — the exact shape `decodeTraces` consumes. */\nexport function encodeTraceRequest(\n resource: Attrs,\n scopeName: string,\n spans: SpanInput[],\n): ExportTraceServiceRequest {\n const otlpSpans: OtlpSpan[] = spans.map((s) => ({\n traceId: s.traceId,\n spanId: s.spanId,\n ...(s.parentSpanId ? { parentSpanId: s.parentSpanId } : {}),\n name: s.name,\n kind: s.kind,\n startTimeUnixNano: s.startUnixNano,\n endTimeUnixNano: s.endUnixNano,\n attributes: attrsToKvList(s.attributes),\n status: { code: s.statusCode },\n }));\n return {\n resourceSpans: [\n {\n resource: { attributes: attrsToKvList(resource) },\n scopeSpans: [{ scope: { name: scopeName }, spans: otlpSpans }],\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 type { ExportResult } from \"@opentelemetry/core\";\nimport type { ReadableSpan, SpanExporter } from \"@opentelemetry/sdk-trace-base\";\nimport { encodeTraceRequest, type Attrs, type SpanInput } from \"@odla/o11y-core\";\nimport type { CollectorBinding } from \"./context.js\";\n\n// ExportResultCode: 0 = SUCCESS, 1 = FAILED (@opentelemetry/core). Kept as\n// literals so we don't take a runtime dependency on the enum.\nconst SUCCESS = 0;\nconst FAILED = 1;\n\n// OTLP span kind is the OpenTelemetry SDK `SpanKind` (INTERNAL=0 .. CONSUMER=4)\n// shifted by one (OTLP INTERNAL=1 .. CONSUMER=5).\nconst OTLP_KIND_OFFSET = 1;\n\n/** OTel `HrTime` ([seconds, nanos]) → a decimal-nanosecond string (OTLP). */\nfunction hrToNano([seconds, nanos]: [number, number]): string {\n return (BigInt(seconds) * 1_000_000_000n + BigInt(nanos)).toString();\n}\n\n/** Project an OTel ReadableSpan onto the flat shape the core encoder wants,\n * dropping non-primitive attribute values (arrays/objects) the wire can't hold. */\nexport function toSpanInput(span: ReadableSpan): SpanInput {\n const ctx = span.spanContext();\n const attributes: Attrs = {};\n for (const [key, value] of Object.entries(span.attributes)) {\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n attributes[key] = value;\n }\n }\n return {\n traceId: ctx.traceId,\n spanId: ctx.spanId,\n parentSpanId: span.parentSpanContext?.spanId ?? \"\",\n name: span.name,\n kind: span.kind + OTLP_KIND_OFFSET,\n statusCode: span.status.code,\n startUnixNano: hrToNano(span.startTime),\n endUnixNano: hrToNano(span.endTime),\n attributes,\n };\n}\n\n/** A SpanExporter that ships OTLP/JSON spans through a Cloudflare service\n * binding instead of the global `fetch` the stock OTLP exporter uses — so\n * traces travel the same private worker-to-worker path as metrics and logs. */\nexport class BindingSpanExporter implements SpanExporter {\n constructor(\n private readonly binding: CollectorBinding,\n private readonly url: string,\n private readonly headers: Record<string, string>,\n private readonly resource: Attrs,\n private readonly scope: string,\n ) {}\n\n export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {\n const body = encodeTraceRequest(this.resource, this.scope, spans.map(toSpanInput));\n this.binding\n .fetch(this.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", ...this.headers },\n body: JSON.stringify(body),\n })\n .then(() => resultCallback({ code: SUCCESS }))\n .catch((error: Error) => resultCallback({ code: FAILED, error }));\n }\n\n async shutdown(): Promise<void> {}\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","span","SpanStatusCode","trace","span","trace","SpanStatusCode","trace","trace"]}
package/llms.txt CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
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
4
 
5
+ > ⚠️ **Experimental — an agentic-coding experiment.** This package is built and
6
+ > operated by autonomous coding agents as an experiment in agentic loops. APIs
7
+ > change without notice and nothing here is production-hardened. **Use at your
8
+ > own risk.**
9
+
5
10
  Official observability client for [odla](https://github.com/) Cloudflare Workers.
6
11
  Wrap your Worker once and get OpenTelemetry **traces**, **metrics**, structured
7
12
  **errors**, and **LLM cost** — exported over OTLP to the odla-o11y collector.
@@ -46,6 +51,16 @@ Config is read from env vars (override per call via the second `withObservabilit
46
51
 
47
52
  `wrangler.jsonc` needs `"compatibility_flags": ["nodejs_compat"]` (for AsyncLocalStorage).
48
53
 
54
+ ### Private ingest via a service binding
55
+
56
+ By default all signals export over `fetch` to `ODLA_O11Y_ENDPOINT`. If the Worker
57
+ is bound to the collector (a service binding named `ODLA_O11Y_COLLECTOR`, or one
58
+ passed as `opts.fetcher`), the SDK routes **traces, metrics, and logs** through
59
+ that binding instead — private worker-to-worker, no public hop, and the endpoint
60
+ host becomes irrelevant. This is the transport odla's first-party hosting uses;
61
+ the binding and its credential are injected at deploy time, so instrumented code
62
+ stays `export default withObservability(handler)` with nothing else to set.
63
+
49
64
  Keep `ODLA_O11Y_ENDPOINT` and `ODLA_O11Y_SERVICE` set as plain vars in every
50
65
  environment and treat only `ODLA_O11Y_TOKEN` as an optional secret: the
51
66
  metrics/logs sink no-ops without an endpoint, but the trace exporter builds
@@ -68,13 +83,20 @@ collector rejects the export; the app itself is unaffected.
68
83
 
69
84
  MIT
70
85
 
71
- ## API reference (generated from dist/index.d.ts, v1.0.1)
86
+ ## API reference (generated from dist/index.d.ts, v1.1.0)
72
87
 
73
88
  ```ts
74
89
  import { Attrs, ModelPrice } from '@odla/o11y-core';
75
90
  export { Attrs } from '@odla/o11y-core';
76
91
  import { Span } from '@opentelemetry/api';
77
92
 
93
+ /** A Cloudflare service binding to the collector Worker (structural — avoids a
94
+ * hard dependency on `@cloudflare/workers-types`). When present the SDK exports
95
+ * every signal through `binding.fetch` (private worker-to-worker) instead of
96
+ * the public endpoint; first-party hosting injects it as `env.ODLA_O11Y_COLLECTOR`. */
97
+ interface CollectorBinding {
98
+ fetch: typeof fetch;
99
+ }
78
100
  interface ObservabilityOptions {
79
101
  /** Service name (default: env.ODLA_O11Y_SERVICE). */
80
102
  service?: string;
@@ -88,6 +110,9 @@ interface ObservabilityOptions {
88
110
  sampleRatio?: number;
89
111
  /** Static resource attributes merged onto every signal. */
90
112
  attributes?: Attrs;
113
+ /** Service binding to the collector. Defaults to an `ODLA_O11Y_COLLECTOR`
114
+ * binding on env; when set, all signals export through it instead of `fetch`. */
115
+ fetcher?: CollectorBinding;
91
116
  }
92
117
  type OptsFn<E> = ObservabilityOptions | ((env: E) => ObservabilityOptions);
93
118
 
@@ -159,5 +184,5 @@ declare function recordLlmUsage(usage: LlmUsage, opts: LlmCostOpts): {
159
184
  costUsd: number;
160
185
  };
161
186
 
162
- export { type ErrorReport, type LlmCostOpts, type LlmUsage, type Metrics, type ObservabilityOptions, type OptsFn, type SpanKindName, type SpanOpts, count, instrumentDurableObject, metrics, recordError, recordLlmUsage, span, withObservability };
187
+ export { type CollectorBinding, type ErrorReport, type LlmCostOpts, type LlmUsage, type Metrics, type ObservabilityOptions, type OptsFn, type SpanKindName, type SpanOpts, count, instrumentDurableObject, metrics, recordError, recordLlmUsage, span, withObservability };
163
188
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odla-ai/o11y",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Official observability client for odla Cloudflare Workers \u2014 OpenTelemetry traces, metrics, structured errors, and LLM cost, exported over OTLP to the odla-o11y collector.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,7 +38,8 @@
38
38
  "scripts": {
39
39
  "build": "tsup",
40
40
  "clean": "rm -rf dist",
41
- "test": "vitest run --passWithNoTests",
41
+ "test": "vitest run",
42
+ "coverage": "vitest run --coverage",
42
43
  "gen:llms": "node ../../scripts/gen-llms.mjs",
43
44
  "prepublishOnly": "npm run build && npm run gen:llms"
44
45
  },
@@ -48,6 +49,8 @@
48
49
  },
49
50
  "devDependencies": {
50
51
  "@odla/o11y-core": "*",
52
+ "@opentelemetry/core": "^2.9.0",
53
+ "@opentelemetry/sdk-trace-base": "^2.9.0",
51
54
  "tsup": "^8.5.1",
52
55
  "typescript": "^6.0.3"
53
56
  }