@ap3x/observe-otel 0.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 +21 -0
- package/README.md +25 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +154 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AP3X
|
|
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,25 @@
|
|
|
1
|
+
# @ap3x/observe-otel
|
|
2
|
+
|
|
3
|
+
Exports `@ap3x/observe` events to any OpenTelemetry-compatible backend.
|
|
4
|
+
|
|
5
|
+
Part of [AP3X](https://github.com/AP3X-Dev/AP3X) — a TypeScript multi-agent framework.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @ap3x/observe-otel
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What's inside
|
|
14
|
+
|
|
15
|
+
- **`OtelExporter`** — implements the `@ap3x/observe` `Exporter` interface over `@opentelemetry/api`.
|
|
16
|
+
|
|
17
|
+
Bring your own OpenTelemetry SDK and configure it as usual; this package only emits.
|
|
18
|
+
|
|
19
|
+
## Documentation
|
|
20
|
+
|
|
21
|
+
See the [AP3X repository](https://github.com/AP3X-Dev/AP3X) for architecture notes, the full package map, and examples.
|
|
22
|
+
|
|
23
|
+
## License
|
|
24
|
+
|
|
25
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Exporter, TraceEvent } from "@ap3x/observe";
|
|
2
|
+
import type { Tracer } from "@opentelemetry/api";
|
|
3
|
+
export interface OtelExporterOptions {
|
|
4
|
+
tracer?: Tracer;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Reconstructs OTel spans from AP3X's point-event trace stream. AP3X emits a flat
|
|
8
|
+
* sequence of start/end/point events per span id; OTel wants intervals. Paired
|
|
9
|
+
* `*_start`/`*_end` events become real spans, keyed by spanId+stem+disambiguator:
|
|
10
|
+
* `tool_execution` uses its `toolCallId`; `member_run`/`struct_run` (which carry no
|
|
11
|
+
* per-invocation id) use the agent/struct name instead. Each key holds a FIFO queue
|
|
12
|
+
* of open spans rather than a single slot, so same-key concurrent starts (e.g.
|
|
13
|
+
* `SubagentRegistry.gather()` fan-out) each get their own span instead of the second
|
|
14
|
+
* start clobbering the first. Everything else is attached as a span event on the
|
|
15
|
+
* innermost open span.
|
|
16
|
+
*
|
|
17
|
+
* ponytail: parent linkage is recorded as the `ap3x.parent_span_id` attribute rather
|
|
18
|
+
* than wired through OTel's Context/trace.setSpan machinery. The injectable `tracer`
|
|
19
|
+
* here is typed against @opentelemetry/api's `Tracer`/`Span` interfaces only (no SDK
|
|
20
|
+
* dependency), and tests inject a hand-rolled fake tracer whose `startSpan` doesn't
|
|
21
|
+
* participate in real Context propagation — attribute-based linkage is what a
|
|
22
|
+
* downstream OTel backend can reconstruct real parent/child spans from, without this
|
|
23
|
+
* package needing a live Context API. Upgrade path: thread `context.active()` +
|
|
24
|
+
* `trace.setSpan` once this exporter runs against a real SDK-backed tracer.
|
|
25
|
+
*/
|
|
26
|
+
export declare class OtelExporter implements Exporter {
|
|
27
|
+
#private;
|
|
28
|
+
constructor(options?: OtelExporterOptions);
|
|
29
|
+
get dropped(): number;
|
|
30
|
+
onEvent(e: TraceEvent): void;
|
|
31
|
+
flush(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAoB,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAGnE,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAyFD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,YAAa,YAAW,QAAQ;;gBAM/B,OAAO,CAAC,EAAE,mBAAmB;IAIzC,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,OAAO,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI;IAiBtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAmE7B"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
3
|
+
function stemOf(name) {
|
|
4
|
+
return name.replace(/_(start|end)$/, "");
|
|
5
|
+
}
|
|
6
|
+
function isStart(name) {
|
|
7
|
+
return name.endsWith("_start");
|
|
8
|
+
}
|
|
9
|
+
function isEnd(name) {
|
|
10
|
+
return name.endsWith("_end");
|
|
11
|
+
}
|
|
12
|
+
var PAIRED_STEMS = /* @__PURE__ */ new Set(["agent", "turn", "tool_execution", "member_run", "struct_run"]);
|
|
13
|
+
function toolCallIdOf(data) {
|
|
14
|
+
if (data && typeof data === "object" && "toolCallId" in data) {
|
|
15
|
+
const id = data.toolCallId;
|
|
16
|
+
if (typeof id === "string") return id;
|
|
17
|
+
}
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
function disambiguatorOf(e, stem) {
|
|
21
|
+
if (stem === "member_run") return e.agent?.name ?? "";
|
|
22
|
+
if (stem === "struct_run") {
|
|
23
|
+
const data = e.data;
|
|
24
|
+
if (data && typeof data === "object" && "struct" in data) {
|
|
25
|
+
const struct = data.struct;
|
|
26
|
+
if (typeof struct === "string") return struct;
|
|
27
|
+
}
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
return toolCallIdOf(e.data);
|
|
31
|
+
}
|
|
32
|
+
function isErrorEvent(data) {
|
|
33
|
+
return Boolean(
|
|
34
|
+
data && typeof data === "object" && data.isError === true
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
function flatAttributes(data) {
|
|
38
|
+
if (!data || typeof data !== "object") return {};
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const [key, value] of Object.entries(data)) {
|
|
41
|
+
if (value === null || value === void 0) continue;
|
|
42
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
43
|
+
out[key] = value;
|
|
44
|
+
} else {
|
|
45
|
+
try {
|
|
46
|
+
out[key] = JSON.stringify(value);
|
|
47
|
+
} catch {
|
|
48
|
+
out[key] = String(value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
function startAttributes(e) {
|
|
55
|
+
return {
|
|
56
|
+
"ap3x.trace_id": e.traceId,
|
|
57
|
+
"ap3x.span_id": e.spanId,
|
|
58
|
+
...e.parentSpanId ? { "ap3x.parent_span_id": e.parentSpanId } : {},
|
|
59
|
+
"ap3x.seq": e.seq,
|
|
60
|
+
"ap3x.source": e.source,
|
|
61
|
+
...e.agent?.name ? { "agent.name": e.agent.name } : {},
|
|
62
|
+
...e.agent?.model ? { "agent.model": e.agent.model } : {}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
var OtelExporter = class {
|
|
66
|
+
#tracer;
|
|
67
|
+
// ponytail: no invocation id in the stream — identical concurrent twins may swap durations; exact match needs an id on the events.
|
|
68
|
+
#open = /* @__PURE__ */ new Map();
|
|
69
|
+
#dropped = 0;
|
|
70
|
+
constructor(options) {
|
|
71
|
+
this.#tracer = options?.tracer ?? trace.getTracer("@ap3x/observe-otel");
|
|
72
|
+
}
|
|
73
|
+
get dropped() {
|
|
74
|
+
return this.#dropped;
|
|
75
|
+
}
|
|
76
|
+
onEvent(e) {
|
|
77
|
+
try {
|
|
78
|
+
const stem = stemOf(e.name);
|
|
79
|
+
if (isStart(e.name) && PAIRED_STEMS.has(stem)) {
|
|
80
|
+
this.#handleStart(e, stem);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (isEnd(e.name) && PAIRED_STEMS.has(stem)) {
|
|
84
|
+
this.#handleEnd(e, stem);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.#handlePoint(e);
|
|
88
|
+
} catch {
|
|
89
|
+
this.#dropped += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async flush() {
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
for (const queue of this.#open.values()) {
|
|
95
|
+
for (const { span } of queue) span.end(now);
|
|
96
|
+
}
|
|
97
|
+
this.#open.clear();
|
|
98
|
+
}
|
|
99
|
+
#keyOf(spanId, stem, disambiguator) {
|
|
100
|
+
return `${spanId}:${stem}:${disambiguator}`;
|
|
101
|
+
}
|
|
102
|
+
#handleStart(e, stem) {
|
|
103
|
+
const key = this.#keyOf(e.spanId, stem, disambiguatorOf(e, stem));
|
|
104
|
+
const span = this.#tracer.startSpan(stem, { startTime: e.ts, attributes: startAttributes(e) });
|
|
105
|
+
const queue = this.#open.get(key);
|
|
106
|
+
if (queue) queue.push({ span, spanId: e.spanId, stem });
|
|
107
|
+
else this.#open.set(key, [{ span, spanId: e.spanId, stem }]);
|
|
108
|
+
}
|
|
109
|
+
#handleEnd(e, stem) {
|
|
110
|
+
const key = this.#keyOf(e.spanId, stem, disambiguatorOf(e, stem));
|
|
111
|
+
const queue = this.#open.get(key);
|
|
112
|
+
const open = queue?.shift();
|
|
113
|
+
if (!open) return;
|
|
114
|
+
open.span.setAttributes({
|
|
115
|
+
"ap3x.end_seq": e.seq,
|
|
116
|
+
...e.usage ? {
|
|
117
|
+
"gen_ai.usage.input_tokens": e.usage.input,
|
|
118
|
+
"gen_ai.usage.output_tokens": e.usage.output
|
|
119
|
+
} : {}
|
|
120
|
+
});
|
|
121
|
+
if (isErrorEvent(e.data)) open.span.setStatus({ code: SpanStatusCode.ERROR });
|
|
122
|
+
open.span.end(e.ts);
|
|
123
|
+
if (queue && queue.length === 0) this.#open.delete(key);
|
|
124
|
+
}
|
|
125
|
+
#handlePoint(e) {
|
|
126
|
+
const target = this.#innermostOpenSpan(e.spanId);
|
|
127
|
+
if (target) {
|
|
128
|
+
target.addEvent(e.name, flatAttributes(e.data), e.ts);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const span = this.#tracer.startSpan(e.name, {
|
|
132
|
+
startTime: e.ts,
|
|
133
|
+
attributes: startAttributes(e)
|
|
134
|
+
});
|
|
135
|
+
span.end(e.ts);
|
|
136
|
+
}
|
|
137
|
+
/** Most-recently-opened span whose key belongs to this spanId. */
|
|
138
|
+
#innermostOpenSpan(spanId) {
|
|
139
|
+
const prefix = `${spanId}:`;
|
|
140
|
+
const entries = [...this.#open.entries()];
|
|
141
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
142
|
+
const entry = entries[i];
|
|
143
|
+
if (entry?.[0].startsWith(prefix)) {
|
|
144
|
+
const queue = entry[1];
|
|
145
|
+
const last = queue[queue.length - 1];
|
|
146
|
+
if (last) return last.span;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
export {
|
|
153
|
+
OtelExporter
|
|
154
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ap3x/observe-otel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenTelemetry exporter for @ap3x/observe",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/AP3X-Dev/AP3X.git",
|
|
8
|
+
"directory": "packages/observe-otel"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": ["dist"],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup src/index.ts --format esm --clean --tsconfig tsconfig.build.json && tsc -p tsconfig.build.json",
|
|
26
|
+
"prepublishOnly": "node ../../scripts/publish-gate.mjs",
|
|
27
|
+
"test": "vitest run"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@ap3x/observe": "0.1.0",
|
|
31
|
+
"@opentelemetry/api": "^1.9.0"
|
|
32
|
+
}
|
|
33
|
+
}
|