@nightowlsdev/telemetry-core 0.1.2
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 +21 -0
- package/README.md +84 -0
- package/dist/index.cjs +146 -0
- package/dist/index.d.cts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +121 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Night Owls contributors
|
|
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
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @nightowlsdev/telemetry-core
|
|
2
|
+
|
|
3
|
+
The shared **SwarmSpan → OpenTelemetry replay** for the Night Owls telemetry family. When a
|
|
4
|
+
swarm run finishes, `@nightowlsdev/core`'s engine hands a batch of pre-recorded `SwarmSpan`s
|
|
5
|
+
(one `run`, one `generation` per model call, one `tool` per tool call) to a
|
|
6
|
+
`TelemetryExporter`. This package replays those spans through a `BasicTracerProvider` as
|
|
7
|
+
real OpenTelemetry spans: it derives a valid 32-hex trace id from the run id, nests the
|
|
8
|
+
children under the run span, and maps generations to `gen_ai.*` semantic conventions. The
|
|
9
|
+
two backend adapters (`@nightowlsdev/telemetry-otel`, `@nightowlsdev/telemetry-langfuse`) are thin
|
|
10
|
+
shells over this — they differ only in the `SpanProcessor` they pass in.
|
|
11
|
+
|
|
12
|
+
It never imports `@mastra/*` and re-exports no Mastra types (the engine wall, CONTRACTS §1).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add @nightowlsdev/telemetry-core @nightowlsdev/core
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
OpenTelemetry (`@opentelemetry/api`, `@opentelemetry/sdk-trace-base`,
|
|
21
|
+
`@opentelemetry/resources`, `@opentelemetry/semantic-conventions`) ships as a dependency.
|
|
22
|
+
|
|
23
|
+
## What it does
|
|
24
|
+
|
|
25
|
+
- **`deriveTraceId(runId)`** — a UUID run id becomes its dash-stripped 32-lowercase-hex form;
|
|
26
|
+
any other run id is sha256-hashed to 32 hex. Deterministic, so a run always maps to the
|
|
27
|
+
same trace.
|
|
28
|
+
- **`createSpanReplayer({ exporter | processor, serviceName?, resourceAttributes? })`** —
|
|
29
|
+
builds a long-lived provider. `replay(spans)` groups spans by run id, forces the derived
|
|
30
|
+
trace id via a synthetic parent `SpanContext`, opens the `run` span first and parents the
|
|
31
|
+
`generation`/`tool` children under it (`trace.setSpan`), replays the original epoch-ms
|
|
32
|
+
timings, then **awaits `provider.forceFlush()`** so nothing is lost in a short-lived /
|
|
33
|
+
serverless invocation.
|
|
34
|
+
- **`replayerExporter(opts)`** — wraps a replayer as a `TelemetryExporter` so an adapter can
|
|
35
|
+
return it directly from its factory.
|
|
36
|
+
- **gen_ai mapping** — generation spans carry `gen_ai.request.model`,
|
|
37
|
+
`gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens` (from
|
|
38
|
+
`@opentelemetry/semantic-conventions/incubating`) plus `gen_ai.usage.cost_usd`, so both
|
|
39
|
+
OTLP backends and Langfuse's default `gen_ai.*` filter pick them up.
|
|
40
|
+
|
|
41
|
+
## Usage (building an adapter)
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { replayerExporter } from "@nightowlsdev/telemetry-core";
|
|
45
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
46
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
47
|
+
|
|
48
|
+
export function myTelemetry(opts) {
|
|
49
|
+
const processor = new BatchSpanProcessor(new OTLPTraceExporter({ url: opts.url }));
|
|
50
|
+
return replayerExporter({ processor, serviceName: "nightowls" });
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
You usually don't depend on this package directly — use `@nightowlsdev/telemetry-otel` or
|
|
55
|
+
`@nightowlsdev/telemetry-langfuse`, then wire it into `defineSwarm({ telemetry })`.
|
|
56
|
+
|
|
57
|
+
## Multi-backend: send to OTLP **and** Langfuse at once
|
|
58
|
+
|
|
59
|
+
`@nightowlsdev/core`'s `compositeTelemetry` fans one span batch out to many exporters, isolating
|
|
60
|
+
each one (`Promise.allSettled` — a throwing exporter never blocks the others or the run):
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { defineSwarm, compositeTelemetry } from "@nightowlsdev/core";
|
|
64
|
+
import { otelTelemetry } from "@nightowlsdev/telemetry-otel";
|
|
65
|
+
import { langfuseTelemetry } from "@nightowlsdev/telemetry-langfuse";
|
|
66
|
+
|
|
67
|
+
const swarm = defineSwarm({
|
|
68
|
+
storage, agents, models: { allow: ["openai/gpt-5.5"] }, modelFactory,
|
|
69
|
+
cost: { maxSteps: 8, maxCostUsd: 1 },
|
|
70
|
+
telemetry: compositeTelemetry([
|
|
71
|
+
otelTelemetry({ url, headers }),
|
|
72
|
+
langfuseTelemetry({ publicKey, secretKey }),
|
|
73
|
+
]),
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`defineSwarm({ telemetry })` also accepts a bare array (`telemetry: [otelTelemetry(...),
|
|
78
|
+
langfuseTelemetry(...)]`) — it's composed the same way. `customTelemetry(fn)` wraps any
|
|
79
|
+
`(spans) => void | Promise<void>` into an exporter.
|
|
80
|
+
|
|
81
|
+
## Engine wall
|
|
82
|
+
|
|
83
|
+
The built `dist/index.d.ts` contains zero `@mastra` references (enforced by
|
|
84
|
+
`test/wall.test.ts`). It consumes only `@nightowlsdev/core`'s `SwarmSpan`/`TelemetryExporter`.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
KIND_MAP: () => KIND_MAP,
|
|
24
|
+
createSpanReplayer: () => createSpanReplayer,
|
|
25
|
+
deriveRootSpanId: () => deriveRootSpanId,
|
|
26
|
+
deriveTraceId: () => deriveTraceId,
|
|
27
|
+
replayerExporter: () => replayerExporter,
|
|
28
|
+
toOtelAttributes: () => toOtelAttributes
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/replay.ts
|
|
33
|
+
var import_api2 = require("@opentelemetry/api");
|
|
34
|
+
var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
|
|
35
|
+
var import_resources = require("@opentelemetry/resources");
|
|
36
|
+
|
|
37
|
+
// src/trace-id.ts
|
|
38
|
+
var import_node_crypto = require("crypto");
|
|
39
|
+
var HEX32 = /^[0-9a-f]{32}$/;
|
|
40
|
+
function deriveTraceId(runId) {
|
|
41
|
+
const stripped = runId.replace(/-/g, "").toLowerCase();
|
|
42
|
+
if (HEX32.test(stripped)) return stripped;
|
|
43
|
+
return (0, import_node_crypto.createHash)("sha256").update(runId).digest("hex").slice(0, 32);
|
|
44
|
+
}
|
|
45
|
+
function deriveRootSpanId(runId) {
|
|
46
|
+
return (0, import_node_crypto.createHash)("sha256").update(`root:${runId}`).digest("hex").slice(0, 16);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/attributes.ts
|
|
50
|
+
var import_api = require("@opentelemetry/api");
|
|
51
|
+
var import_incubating = require("@opentelemetry/semantic-conventions/incubating");
|
|
52
|
+
var KIND_MAP = {
|
|
53
|
+
run: import_api.SpanKind.INTERNAL,
|
|
54
|
+
delegation: import_api.SpanKind.INTERNAL,
|
|
55
|
+
generation: import_api.SpanKind.INTERNAL,
|
|
56
|
+
tool: import_api.SpanKind.CLIENT,
|
|
57
|
+
event: import_api.SpanKind.INTERNAL,
|
|
58
|
+
recall: import_api.SpanKind.INTERNAL
|
|
59
|
+
// R4: a memory recall is an internal read (like run/event)
|
|
60
|
+
};
|
|
61
|
+
function coerce(v) {
|
|
62
|
+
if (v == null) return void 0;
|
|
63
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
|
|
64
|
+
return JSON.stringify(v);
|
|
65
|
+
}
|
|
66
|
+
function toOtelAttributes(span) {
|
|
67
|
+
const out = { "nightowls.span.kind": span.kind };
|
|
68
|
+
for (const [k, v] of Object.entries(span.attributes)) {
|
|
69
|
+
const c = coerce(v);
|
|
70
|
+
if (c !== void 0) out[k] = c;
|
|
71
|
+
}
|
|
72
|
+
if (span.kind === "generation") {
|
|
73
|
+
const a = span.attributes;
|
|
74
|
+
if (typeof a.modelId === "string") out[import_incubating.ATTR_GEN_AI_REQUEST_MODEL] = a.modelId;
|
|
75
|
+
if (typeof a.inputTokens === "number") out[import_incubating.ATTR_GEN_AI_USAGE_INPUT_TOKENS] = a.inputTokens;
|
|
76
|
+
if (typeof a.outputTokens === "number") out[import_incubating.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS] = a.outputTokens;
|
|
77
|
+
if (typeof a.costUsd === "number") out["gen_ai.usage.cost_usd"] = a.costUsd;
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/replay.ts
|
|
83
|
+
function createSpanReplayer(opts) {
|
|
84
|
+
const processor = opts.processor ?? new import_sdk_trace_base.SimpleSpanProcessor(
|
|
85
|
+
opts.exporter ?? (() => {
|
|
86
|
+
throw new Error("createSpanReplayer needs `exporter` or `processor`");
|
|
87
|
+
})()
|
|
88
|
+
);
|
|
89
|
+
const provider = new import_sdk_trace_base.BasicTracerProvider({
|
|
90
|
+
resource: (0, import_resources.resourceFromAttributes)({
|
|
91
|
+
"service.name": opts.serviceName ?? "nightowls",
|
|
92
|
+
...opts.resourceAttributes
|
|
93
|
+
}),
|
|
94
|
+
spanProcessors: [processor]
|
|
95
|
+
});
|
|
96
|
+
const tracer = provider.getTracer("@nightowlsdev/telemetry");
|
|
97
|
+
return {
|
|
98
|
+
async replay(spans) {
|
|
99
|
+
if (spans.length === 0) return;
|
|
100
|
+
const groups = /* @__PURE__ */ new Map();
|
|
101
|
+
for (const s of spans) {
|
|
102
|
+
const g = groups.get(s.traceId);
|
|
103
|
+
if (g) g.push(s);
|
|
104
|
+
else groups.set(s.traceId, [s]);
|
|
105
|
+
}
|
|
106
|
+
for (const [runId, group] of groups) {
|
|
107
|
+
const traceId = deriveTraceId(runId);
|
|
108
|
+
const rootSc = {
|
|
109
|
+
traceId,
|
|
110
|
+
spanId: deriveRootSpanId(runId),
|
|
111
|
+
traceFlags: import_api2.TraceFlags.SAMPLED,
|
|
112
|
+
isRemote: false
|
|
113
|
+
};
|
|
114
|
+
const rootCtx = import_api2.trace.setSpanContext(import_api2.context.active(), rootSc);
|
|
115
|
+
const ordered = [...group].sort(
|
|
116
|
+
(a, b) => a.kind === "run" ? -1 : b.kind === "run" ? 1 : a.startedAt - b.startedAt
|
|
117
|
+
);
|
|
118
|
+
let runSpan;
|
|
119
|
+
for (const sw of ordered) {
|
|
120
|
+
const parentCtx = sw.kind === "run" || !runSpan ? rootCtx : import_api2.trace.setSpan(import_api2.context.active(), runSpan);
|
|
121
|
+
const span = tracer.startSpan(sw.name, { kind: KIND_MAP[sw.kind], startTime: sw.startedAt }, parentCtx);
|
|
122
|
+
span.setAttributes(toOtelAttributes(sw));
|
|
123
|
+
span.end(sw.endedAt ?? sw.startedAt);
|
|
124
|
+
if (sw.kind === "run") runSpan = span;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
await provider.forceFlush();
|
|
128
|
+
},
|
|
129
|
+
async shutdown() {
|
|
130
|
+
await provider.shutdown();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function replayerExporter(opts) {
|
|
135
|
+
const replayer = createSpanReplayer(opts);
|
|
136
|
+
return { export: (spans) => replayer.replay(spans) };
|
|
137
|
+
}
|
|
138
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
139
|
+
0 && (module.exports = {
|
|
140
|
+
KIND_MAP,
|
|
141
|
+
createSpanReplayer,
|
|
142
|
+
deriveRootSpanId,
|
|
143
|
+
deriveTraceId,
|
|
144
|
+
replayerExporter,
|
|
145
|
+
toOtelAttributes
|
|
146
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
2
|
+
import { SwarmSpan, TelemetryExporter } from '@nightowlsdev/core';
|
|
3
|
+
import { SpanKind, Attributes } from '@opentelemetry/api';
|
|
4
|
+
|
|
5
|
+
interface SpanReplayerOpts {
|
|
6
|
+
/** Provide a SpanExporter (telemetry-otel: OTLP) OR a full SpanProcessor (telemetry-langfuse). */
|
|
7
|
+
exporter?: SpanExporter;
|
|
8
|
+
processor?: SpanProcessor;
|
|
9
|
+
serviceName?: string;
|
|
10
|
+
resourceAttributes?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
interface SpanReplayer {
|
|
13
|
+
replay(spans: SwarmSpan[]): Promise<void>;
|
|
14
|
+
shutdown(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/** Build a long-lived provider; `replay` re-plays a batch of pre-recorded SwarmSpans as real OTel spans. */
|
|
17
|
+
declare function createSpanReplayer(opts: SpanReplayerOpts): SpanReplayer;
|
|
18
|
+
/** Convenience: wrap a replayer as a TelemetryExporter (so adapters export directly). */
|
|
19
|
+
declare function replayerExporter(opts: SpanReplayerOpts): TelemetryExporter;
|
|
20
|
+
|
|
21
|
+
/** Derive a valid 32-lowercase-hex OTel trace id from any runId (UUID → strip dashes; else sha256). */
|
|
22
|
+
declare function deriveTraceId(runId: string): string;
|
|
23
|
+
/** Derive a stable 16-hex span id for the synthetic trace root (so the run span has a parent to anchor the traceId). */
|
|
24
|
+
declare function deriveRootSpanId(runId: string): string;
|
|
25
|
+
|
|
26
|
+
declare const KIND_MAP: Record<SwarmSpan["kind"], SpanKind>;
|
|
27
|
+
/** Map a SwarmSpan's attributes to OTel attributes, lifting generation usage/model/cost to gen_ai.* semconv. */
|
|
28
|
+
declare function toOtelAttributes(span: SwarmSpan): Attributes;
|
|
29
|
+
|
|
30
|
+
export { KIND_MAP, type SpanReplayer, type SpanReplayerOpts, createSpanReplayer, deriveRootSpanId, deriveTraceId, replayerExporter, toOtelAttributes };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
2
|
+
import { SwarmSpan, TelemetryExporter } from '@nightowlsdev/core';
|
|
3
|
+
import { SpanKind, Attributes } from '@opentelemetry/api';
|
|
4
|
+
|
|
5
|
+
interface SpanReplayerOpts {
|
|
6
|
+
/** Provide a SpanExporter (telemetry-otel: OTLP) OR a full SpanProcessor (telemetry-langfuse). */
|
|
7
|
+
exporter?: SpanExporter;
|
|
8
|
+
processor?: SpanProcessor;
|
|
9
|
+
serviceName?: string;
|
|
10
|
+
resourceAttributes?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
interface SpanReplayer {
|
|
13
|
+
replay(spans: SwarmSpan[]): Promise<void>;
|
|
14
|
+
shutdown(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/** Build a long-lived provider; `replay` re-plays a batch of pre-recorded SwarmSpans as real OTel spans. */
|
|
17
|
+
declare function createSpanReplayer(opts: SpanReplayerOpts): SpanReplayer;
|
|
18
|
+
/** Convenience: wrap a replayer as a TelemetryExporter (so adapters export directly). */
|
|
19
|
+
declare function replayerExporter(opts: SpanReplayerOpts): TelemetryExporter;
|
|
20
|
+
|
|
21
|
+
/** Derive a valid 32-lowercase-hex OTel trace id from any runId (UUID → strip dashes; else sha256). */
|
|
22
|
+
declare function deriveTraceId(runId: string): string;
|
|
23
|
+
/** Derive a stable 16-hex span id for the synthetic trace root (so the run span has a parent to anchor the traceId). */
|
|
24
|
+
declare function deriveRootSpanId(runId: string): string;
|
|
25
|
+
|
|
26
|
+
declare const KIND_MAP: Record<SwarmSpan["kind"], SpanKind>;
|
|
27
|
+
/** Map a SwarmSpan's attributes to OTel attributes, lifting generation usage/model/cost to gen_ai.* semconv. */
|
|
28
|
+
declare function toOtelAttributes(span: SwarmSpan): Attributes;
|
|
29
|
+
|
|
30
|
+
export { KIND_MAP, type SpanReplayer, type SpanReplayerOpts, createSpanReplayer, deriveRootSpanId, deriveTraceId, replayerExporter, toOtelAttributes };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/replay.ts
|
|
2
|
+
import { context, trace, TraceFlags } from "@opentelemetry/api";
|
|
3
|
+
import {
|
|
4
|
+
BasicTracerProvider,
|
|
5
|
+
SimpleSpanProcessor
|
|
6
|
+
} from "@opentelemetry/sdk-trace-base";
|
|
7
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
8
|
+
|
|
9
|
+
// src/trace-id.ts
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
var HEX32 = /^[0-9a-f]{32}$/;
|
|
12
|
+
function deriveTraceId(runId) {
|
|
13
|
+
const stripped = runId.replace(/-/g, "").toLowerCase();
|
|
14
|
+
if (HEX32.test(stripped)) return stripped;
|
|
15
|
+
return createHash("sha256").update(runId).digest("hex").slice(0, 32);
|
|
16
|
+
}
|
|
17
|
+
function deriveRootSpanId(runId) {
|
|
18
|
+
return createHash("sha256").update(`root:${runId}`).digest("hex").slice(0, 16);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/attributes.ts
|
|
22
|
+
import { SpanKind } from "@opentelemetry/api";
|
|
23
|
+
import {
|
|
24
|
+
ATTR_GEN_AI_REQUEST_MODEL,
|
|
25
|
+
ATTR_GEN_AI_USAGE_INPUT_TOKENS,
|
|
26
|
+
ATTR_GEN_AI_USAGE_OUTPUT_TOKENS
|
|
27
|
+
} from "@opentelemetry/semantic-conventions/incubating";
|
|
28
|
+
var KIND_MAP = {
|
|
29
|
+
run: SpanKind.INTERNAL,
|
|
30
|
+
delegation: SpanKind.INTERNAL,
|
|
31
|
+
generation: SpanKind.INTERNAL,
|
|
32
|
+
tool: SpanKind.CLIENT,
|
|
33
|
+
event: SpanKind.INTERNAL,
|
|
34
|
+
recall: SpanKind.INTERNAL
|
|
35
|
+
// R4: a memory recall is an internal read (like run/event)
|
|
36
|
+
};
|
|
37
|
+
function coerce(v) {
|
|
38
|
+
if (v == null) return void 0;
|
|
39
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
|
|
40
|
+
return JSON.stringify(v);
|
|
41
|
+
}
|
|
42
|
+
function toOtelAttributes(span) {
|
|
43
|
+
const out = { "nightowls.span.kind": span.kind };
|
|
44
|
+
for (const [k, v] of Object.entries(span.attributes)) {
|
|
45
|
+
const c = coerce(v);
|
|
46
|
+
if (c !== void 0) out[k] = c;
|
|
47
|
+
}
|
|
48
|
+
if (span.kind === "generation") {
|
|
49
|
+
const a = span.attributes;
|
|
50
|
+
if (typeof a.modelId === "string") out[ATTR_GEN_AI_REQUEST_MODEL] = a.modelId;
|
|
51
|
+
if (typeof a.inputTokens === "number") out[ATTR_GEN_AI_USAGE_INPUT_TOKENS] = a.inputTokens;
|
|
52
|
+
if (typeof a.outputTokens === "number") out[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS] = a.outputTokens;
|
|
53
|
+
if (typeof a.costUsd === "number") out["gen_ai.usage.cost_usd"] = a.costUsd;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/replay.ts
|
|
59
|
+
function createSpanReplayer(opts) {
|
|
60
|
+
const processor = opts.processor ?? new SimpleSpanProcessor(
|
|
61
|
+
opts.exporter ?? (() => {
|
|
62
|
+
throw new Error("createSpanReplayer needs `exporter` or `processor`");
|
|
63
|
+
})()
|
|
64
|
+
);
|
|
65
|
+
const provider = new BasicTracerProvider({
|
|
66
|
+
resource: resourceFromAttributes({
|
|
67
|
+
"service.name": opts.serviceName ?? "nightowls",
|
|
68
|
+
...opts.resourceAttributes
|
|
69
|
+
}),
|
|
70
|
+
spanProcessors: [processor]
|
|
71
|
+
});
|
|
72
|
+
const tracer = provider.getTracer("@nightowlsdev/telemetry");
|
|
73
|
+
return {
|
|
74
|
+
async replay(spans) {
|
|
75
|
+
if (spans.length === 0) return;
|
|
76
|
+
const groups = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const s of spans) {
|
|
78
|
+
const g = groups.get(s.traceId);
|
|
79
|
+
if (g) g.push(s);
|
|
80
|
+
else groups.set(s.traceId, [s]);
|
|
81
|
+
}
|
|
82
|
+
for (const [runId, group] of groups) {
|
|
83
|
+
const traceId = deriveTraceId(runId);
|
|
84
|
+
const rootSc = {
|
|
85
|
+
traceId,
|
|
86
|
+
spanId: deriveRootSpanId(runId),
|
|
87
|
+
traceFlags: TraceFlags.SAMPLED,
|
|
88
|
+
isRemote: false
|
|
89
|
+
};
|
|
90
|
+
const rootCtx = trace.setSpanContext(context.active(), rootSc);
|
|
91
|
+
const ordered = [...group].sort(
|
|
92
|
+
(a, b) => a.kind === "run" ? -1 : b.kind === "run" ? 1 : a.startedAt - b.startedAt
|
|
93
|
+
);
|
|
94
|
+
let runSpan;
|
|
95
|
+
for (const sw of ordered) {
|
|
96
|
+
const parentCtx = sw.kind === "run" || !runSpan ? rootCtx : trace.setSpan(context.active(), runSpan);
|
|
97
|
+
const span = tracer.startSpan(sw.name, { kind: KIND_MAP[sw.kind], startTime: sw.startedAt }, parentCtx);
|
|
98
|
+
span.setAttributes(toOtelAttributes(sw));
|
|
99
|
+
span.end(sw.endedAt ?? sw.startedAt);
|
|
100
|
+
if (sw.kind === "run") runSpan = span;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
await provider.forceFlush();
|
|
104
|
+
},
|
|
105
|
+
async shutdown() {
|
|
106
|
+
await provider.shutdown();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function replayerExporter(opts) {
|
|
111
|
+
const replayer = createSpanReplayer(opts);
|
|
112
|
+
return { export: (spans) => replayer.replay(spans) };
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
KIND_MAP,
|
|
116
|
+
createSpanReplayer,
|
|
117
|
+
deriveRootSpanId,
|
|
118
|
+
deriveTraceId,
|
|
119
|
+
replayerExporter,
|
|
120
|
+
toOtelAttributes
|
|
121
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nightowlsdev/telemetry-core",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/cueplusplus/corale.git",
|
|
12
|
+
"directory": "packages/telemetry-core"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/cueplusplus/corale#readme",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"require": "./dist/index.cjs"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"main": "./dist/index.cjs",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@opentelemetry/api": "^1.9.0",
|
|
31
|
+
"@opentelemetry/core": "^2.0.1",
|
|
32
|
+
"@opentelemetry/sdk-trace-base": "^2.0.1",
|
|
33
|
+
"@opentelemetry/resources": "^2.0.1",
|
|
34
|
+
"@opentelemetry/semantic-conventions": "^1.36.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@nightowlsdev/core": "0.3.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^24.12.4",
|
|
41
|
+
"tsup": "8.5.1",
|
|
42
|
+
"typescript": "6.0.3",
|
|
43
|
+
"vitest": "^3.2.0",
|
|
44
|
+
"@nightowlsdev/core": "0.3.0",
|
|
45
|
+
"@nightowlsdev/tsconfig": "0.0.0",
|
|
46
|
+
"@nightowlsdev/eslint-config": "0.0.0"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"lint": "eslint src"
|
|
53
|
+
}
|
|
54
|
+
}
|