@nightowlsdev/telemetry-core 0.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -21,11 +21,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  KIND_MAP: () => KIND_MAP,
24
+ aisdkTelemetry: () => aisdkTelemetry,
24
25
  createSpanReplayer: () => createSpanReplayer,
25
26
  deriveRootSpanId: () => deriveRootSpanId,
26
27
  deriveTraceId: () => deriveTraceId,
27
28
  replayerExporter: () => replayerExporter,
28
- toOtelAttributes: () => toOtelAttributes
29
+ toOtelAttributes: () => toOtelAttributes,
30
+ withEmbeddingSpan: () => withEmbeddingSpan
29
31
  });
30
32
  module.exports = __toCommonJS(index_exports);
31
33
 
@@ -135,12 +137,48 @@ function replayerExporter(opts) {
135
137
  const replayer = createSpanReplayer(opts);
136
138
  return { export: (spans) => replayer.replay(spans) };
137
139
  }
140
+
141
+ // src/aisdk.ts
142
+ var import_api3 = require("@opentelemetry/api");
143
+ var import_incubating2 = require("@opentelemetry/semantic-conventions/incubating");
144
+ function aisdkTelemetry(opts) {
145
+ return {
146
+ isEnabled: true,
147
+ functionId: opts.functionId,
148
+ ...opts.metadata ? { metadata: opts.metadata } : {},
149
+ recordInputs: opts.recordInputs ?? false,
150
+ recordOutputs: opts.recordOutputs ?? false
151
+ };
152
+ }
153
+ async function withEmbeddingSpan(opts, fn) {
154
+ const tracer = opts.tracer ?? import_api3.trace.getTracer("nightowls");
155
+ return tracer.startActiveSpan(opts.functionId, async (span) => {
156
+ const attrs = { "nightowls.span.kind": "embedding", [import_incubating2.ATTR_GEN_AI_REQUEST_MODEL]: opts.model, "gen_ai.operation.name": "embeddings" };
157
+ if (typeof opts.count === "number") attrs["gen_ai.request.embedding.count"] = opts.count;
158
+ for (const [k, v] of Object.entries(opts.metadata ?? {})) attrs[k] = v;
159
+ span.setAttributes(attrs);
160
+ try {
161
+ const out = await fn();
162
+ const tokens = opts.tokens ?? out?.usage?.tokens;
163
+ if (typeof tokens === "number") span.setAttribute(import_incubating2.ATTR_GEN_AI_USAGE_INPUT_TOKENS, tokens);
164
+ span.setStatus({ code: import_api3.SpanStatusCode.OK });
165
+ return out;
166
+ } catch (err) {
167
+ span.setStatus({ code: import_api3.SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) });
168
+ throw err;
169
+ } finally {
170
+ span.end();
171
+ }
172
+ });
173
+ }
138
174
  // Annotate the CommonJS export names for ESM import in node:
139
175
  0 && (module.exports = {
140
176
  KIND_MAP,
177
+ aisdkTelemetry,
141
178
  createSpanReplayer,
142
179
  deriveRootSpanId,
143
180
  deriveTraceId,
144
181
  replayerExporter,
145
- toOtelAttributes
182
+ toOtelAttributes,
183
+ withEmbeddingSpan
146
184
  });
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { SpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-base';
2
2
  import { SwarmSpan, TelemetryExporter } from '@nightowlsdev/core';
3
- import { SpanKind, Attributes } from '@opentelemetry/api';
3
+ import { SpanKind, Attributes, Tracer } from '@opentelemetry/api';
4
4
 
5
5
  interface SpanReplayerOpts {
6
6
  /** Provide a SpanExporter (telemetry-otel: OTLP) OR a full SpanProcessor (telemetry-langfuse). */
@@ -27,4 +27,59 @@ declare const KIND_MAP: Record<SwarmSpan["kind"], SpanKind>;
27
27
  /** Map a SwarmSpan's attributes to OTel attributes, lifting generation usage/model/cost to gen_ai.* semconv. */
28
28
  declare function toOtelAttributes(span: SwarmSpan): Attributes;
29
29
 
30
- export { KIND_MAP, type SpanReplayer, type SpanReplayerOpts, createSpanReplayer, deriveRootSpanId, deriveTraceId, replayerExporter, toOtelAttributes };
30
+ /** The subset of the AI SDK's `experimental_telemetry` settings we produce (typed locally so this package keeps
31
+ * ZERO dependency on `ai`). Assignable to `generateText`/`streamText`'s `experimental_telemetry` field. */
32
+ interface AiSdkTelemetrySettings {
33
+ isEnabled: boolean;
34
+ functionId?: string;
35
+ metadata?: Record<string, string | number | boolean>;
36
+ recordInputs?: boolean;
37
+ recordOutputs?: boolean;
38
+ }
39
+ interface AiSdkTelemetryOpts {
40
+ /** A stable name for this call (the Langfuse generation name / OTel span functionId), e.g. "entity_extraction". */
41
+ functionId: string;
42
+ /** Arbitrary key/values attached to the span (trace correlation: userId, tenantId, jobId, …). */
43
+ metadata?: Record<string, string | number | boolean>;
44
+ /** Record prompt/response bodies on the span. Default false (privacy-safe — only metadata + usage). */
45
+ recordInputs?: boolean;
46
+ recordOutputs?: boolean;
47
+ }
48
+ /**
49
+ * FR-008 — produce `experimental_telemetry` settings for ANY `generateText`/`streamText`, engine-independent, so a
50
+ * non-swarm call lands in Langfuse/OTel with `gen_ai.*` + tokens + cost via the host's existing exporter.
51
+ *
52
+ * @example
53
+ * await generateText({ model, prompt, experimental_telemetry: aisdkTelemetry({ functionId: "summarize" }) });
54
+ */
55
+ declare function aisdkTelemetry(opts: AiSdkTelemetryOpts): AiSdkTelemetrySettings;
56
+ interface EmbeddingSpanOpts {
57
+ /** A stable name for this embedding call, e.g. "embedding" (the OTel span name). */
58
+ functionId: string;
59
+ /** The embedding model id (lands on `gen_ai.request.model`). */
60
+ model: string;
61
+ /** Number of inputs embedded (lands on `gen_ai.usage.input_tokens` is NOT it — this is the value count). */
62
+ count?: number;
63
+ /** Token usage, if the caller can supply it (the AI SDK's `embedMany` returns `.usage.tokens`). */
64
+ tokens?: number;
65
+ /** Extra correlation attributes (userId, tenantId, …). */
66
+ metadata?: Record<string, string | number | boolean>;
67
+ /** Override the tracer (defaults to the global `nightowls` tracer the host's provider registered). */
68
+ tracer?: Tracer;
69
+ }
70
+ /**
71
+ * FR-008/FR-012 — wrap an EMBEDDING call (`embed`/`embedMany`) in an OTel `gen_ai.*` span, since the AI SDK's
72
+ * `experimental_telemetry` callbacks do NOT fire for embeddings. Records model + value count (+ token usage if the
73
+ * result exposes it). Forwards through the host's existing SpanProcessor. Re-throws after recording on error.
74
+ *
75
+ * @example
76
+ * const res = await withEmbeddingSpan({ functionId: "embedding", model: "text-embedding-3-small", count: values.length },
77
+ * () => embedMany({ model, values }));
78
+ */
79
+ declare function withEmbeddingSpan<T extends {
80
+ usage?: {
81
+ tokens?: number;
82
+ };
83
+ }>(opts: EmbeddingSpanOpts, fn: () => Promise<T>): Promise<T>;
84
+
85
+ export { type AiSdkTelemetryOpts, type AiSdkTelemetrySettings, type EmbeddingSpanOpts, KIND_MAP, type SpanReplayer, type SpanReplayerOpts, aisdkTelemetry, createSpanReplayer, deriveRootSpanId, deriveTraceId, replayerExporter, toOtelAttributes, withEmbeddingSpan };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { SpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-base';
2
2
  import { SwarmSpan, TelemetryExporter } from '@nightowlsdev/core';
3
- import { SpanKind, Attributes } from '@opentelemetry/api';
3
+ import { SpanKind, Attributes, Tracer } from '@opentelemetry/api';
4
4
 
5
5
  interface SpanReplayerOpts {
6
6
  /** Provide a SpanExporter (telemetry-otel: OTLP) OR a full SpanProcessor (telemetry-langfuse). */
@@ -27,4 +27,59 @@ declare const KIND_MAP: Record<SwarmSpan["kind"], SpanKind>;
27
27
  /** Map a SwarmSpan's attributes to OTel attributes, lifting generation usage/model/cost to gen_ai.* semconv. */
28
28
  declare function toOtelAttributes(span: SwarmSpan): Attributes;
29
29
 
30
- export { KIND_MAP, type SpanReplayer, type SpanReplayerOpts, createSpanReplayer, deriveRootSpanId, deriveTraceId, replayerExporter, toOtelAttributes };
30
+ /** The subset of the AI SDK's `experimental_telemetry` settings we produce (typed locally so this package keeps
31
+ * ZERO dependency on `ai`). Assignable to `generateText`/`streamText`'s `experimental_telemetry` field. */
32
+ interface AiSdkTelemetrySettings {
33
+ isEnabled: boolean;
34
+ functionId?: string;
35
+ metadata?: Record<string, string | number | boolean>;
36
+ recordInputs?: boolean;
37
+ recordOutputs?: boolean;
38
+ }
39
+ interface AiSdkTelemetryOpts {
40
+ /** A stable name for this call (the Langfuse generation name / OTel span functionId), e.g. "entity_extraction". */
41
+ functionId: string;
42
+ /** Arbitrary key/values attached to the span (trace correlation: userId, tenantId, jobId, …). */
43
+ metadata?: Record<string, string | number | boolean>;
44
+ /** Record prompt/response bodies on the span. Default false (privacy-safe — only metadata + usage). */
45
+ recordInputs?: boolean;
46
+ recordOutputs?: boolean;
47
+ }
48
+ /**
49
+ * FR-008 — produce `experimental_telemetry` settings for ANY `generateText`/`streamText`, engine-independent, so a
50
+ * non-swarm call lands in Langfuse/OTel with `gen_ai.*` + tokens + cost via the host's existing exporter.
51
+ *
52
+ * @example
53
+ * await generateText({ model, prompt, experimental_telemetry: aisdkTelemetry({ functionId: "summarize" }) });
54
+ */
55
+ declare function aisdkTelemetry(opts: AiSdkTelemetryOpts): AiSdkTelemetrySettings;
56
+ interface EmbeddingSpanOpts {
57
+ /** A stable name for this embedding call, e.g. "embedding" (the OTel span name). */
58
+ functionId: string;
59
+ /** The embedding model id (lands on `gen_ai.request.model`). */
60
+ model: string;
61
+ /** Number of inputs embedded (lands on `gen_ai.usage.input_tokens` is NOT it — this is the value count). */
62
+ count?: number;
63
+ /** Token usage, if the caller can supply it (the AI SDK's `embedMany` returns `.usage.tokens`). */
64
+ tokens?: number;
65
+ /** Extra correlation attributes (userId, tenantId, …). */
66
+ metadata?: Record<string, string | number | boolean>;
67
+ /** Override the tracer (defaults to the global `nightowls` tracer the host's provider registered). */
68
+ tracer?: Tracer;
69
+ }
70
+ /**
71
+ * FR-008/FR-012 — wrap an EMBEDDING call (`embed`/`embedMany`) in an OTel `gen_ai.*` span, since the AI SDK's
72
+ * `experimental_telemetry` callbacks do NOT fire for embeddings. Records model + value count (+ token usage if the
73
+ * result exposes it). Forwards through the host's existing SpanProcessor. Re-throws after recording on error.
74
+ *
75
+ * @example
76
+ * const res = await withEmbeddingSpan({ functionId: "embedding", model: "text-embedding-3-small", count: values.length },
77
+ * () => embedMany({ model, values }));
78
+ */
79
+ declare function withEmbeddingSpan<T extends {
80
+ usage?: {
81
+ tokens?: number;
82
+ };
83
+ }>(opts: EmbeddingSpanOpts, fn: () => Promise<T>): Promise<T>;
84
+
85
+ export { type AiSdkTelemetryOpts, type AiSdkTelemetrySettings, type EmbeddingSpanOpts, KIND_MAP, type SpanReplayer, type SpanReplayerOpts, aisdkTelemetry, createSpanReplayer, deriveRootSpanId, deriveTraceId, replayerExporter, toOtelAttributes, withEmbeddingSpan };
package/dist/index.js CHANGED
@@ -111,11 +111,50 @@ function replayerExporter(opts) {
111
111
  const replayer = createSpanReplayer(opts);
112
112
  return { export: (spans) => replayer.replay(spans) };
113
113
  }
114
+
115
+ // src/aisdk.ts
116
+ import { trace as trace2, SpanStatusCode } from "@opentelemetry/api";
117
+ import {
118
+ ATTR_GEN_AI_REQUEST_MODEL as ATTR_GEN_AI_REQUEST_MODEL2,
119
+ ATTR_GEN_AI_USAGE_INPUT_TOKENS as ATTR_GEN_AI_USAGE_INPUT_TOKENS2
120
+ } from "@opentelemetry/semantic-conventions/incubating";
121
+ function aisdkTelemetry(opts) {
122
+ return {
123
+ isEnabled: true,
124
+ functionId: opts.functionId,
125
+ ...opts.metadata ? { metadata: opts.metadata } : {},
126
+ recordInputs: opts.recordInputs ?? false,
127
+ recordOutputs: opts.recordOutputs ?? false
128
+ };
129
+ }
130
+ async function withEmbeddingSpan(opts, fn) {
131
+ const tracer = opts.tracer ?? trace2.getTracer("nightowls");
132
+ return tracer.startActiveSpan(opts.functionId, async (span) => {
133
+ const attrs = { "nightowls.span.kind": "embedding", [ATTR_GEN_AI_REQUEST_MODEL2]: opts.model, "gen_ai.operation.name": "embeddings" };
134
+ if (typeof opts.count === "number") attrs["gen_ai.request.embedding.count"] = opts.count;
135
+ for (const [k, v] of Object.entries(opts.metadata ?? {})) attrs[k] = v;
136
+ span.setAttributes(attrs);
137
+ try {
138
+ const out = await fn();
139
+ const tokens = opts.tokens ?? out?.usage?.tokens;
140
+ if (typeof tokens === "number") span.setAttribute(ATTR_GEN_AI_USAGE_INPUT_TOKENS2, tokens);
141
+ span.setStatus({ code: SpanStatusCode.OK });
142
+ return out;
143
+ } catch (err) {
144
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) });
145
+ throw err;
146
+ } finally {
147
+ span.end();
148
+ }
149
+ });
150
+ }
114
151
  export {
115
152
  KIND_MAP,
153
+ aisdkTelemetry,
116
154
  createSpanReplayer,
117
155
  deriveRootSpanId,
118
156
  deriveTraceId,
119
157
  replayerExporter,
120
- toOtelAttributes
158
+ toOtelAttributes,
159
+ withEmbeddingSpan
121
160
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nightowlsdev/telemetry-core",
3
- "version": "0.1.2",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -34,16 +34,16 @@
34
34
  "@opentelemetry/semantic-conventions": "^1.36.0"
35
35
  },
36
36
  "peerDependencies": {
37
- "@nightowlsdev/core": "0.3.0"
37
+ "@nightowlsdev/core": "0.5.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^24.12.4",
41
41
  "tsup": "8.5.1",
42
42
  "typescript": "6.0.3",
43
43
  "vitest": "^3.2.0",
44
- "@nightowlsdev/core": "0.3.0",
45
- "@nightowlsdev/tsconfig": "0.0.0",
46
- "@nightowlsdev/eslint-config": "0.0.0"
44
+ "@nightowlsdev/core": "0.5.0",
45
+ "@nightowlsdev/eslint-config": "0.0.0",
46
+ "@nightowlsdev/tsconfig": "0.0.0"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsup",