@adhd/apigen-plugin-tracing 0.0.0-stage → 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/CHANGELOG.md +19 -0
- package/README.md +112 -2
- package/index.d.ts +3 -0
- package/index.js +1 -0
- package/index.mjs +98 -0
- package/lib/plugin.d.ts +80 -0
- package/package.json +31 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @adhd/apigen-plugin-tracing
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Initial release. A `Layer` capability that emits one OTel span per dispatched operation
|
|
8
|
+
(operation id, transport, outcome, duration) to the `@adhd/sox-telemetry` JSONL sink, correlated
|
|
9
|
+
by `trace_id`. Declares an empty `target` capability for interface parity (`--type tracing` is a
|
|
10
|
+
valid no-op).
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- The span's `apigen.transport` is now supplied by the engine from the operation plan's own
|
|
15
|
+
transport rather than inferred, so every transport — `http`, `grpc`, `mcp`, `cli` — reports
|
|
16
|
+
its own transport, never `undefined`. Reserved span keys (`apigen.op`, `apigen.transport`,
|
|
17
|
+
`trace_id`) can no longer be shadowed by an `envelopeAttrs` key of the same name; the reserved keys
|
|
18
|
+
always win. Streaming operations are quarantined to a unary span (see README): per-chunk records
|
|
19
|
+
with a chunk count are no longer emitted under the current `Next` contract.
|
package/README.md
CHANGED
|
@@ -1,3 +1,113 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @adhd/apigen-plugin-tracing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Layer plugin: one OTel span per dispatched operation, written as durable JSONL trace records.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
`@adhd/apigen-plugin-tracing` instruments **every operation dispatched through an apigen transport**
|
|
8
|
+
— `http`, `grpc`, `mcp`, and `cli` — because all transports converge on a single composed invoker.
|
|
9
|
+
For each dispatched operation it emits a span carrying the operation id, the transport, the outcome,
|
|
10
|
+
and the duration, correlated by a `trace_id` that ties every span of one logical request together.
|
|
11
|
+
|
|
12
|
+
Records are written through [`@adhd/sox-telemetry`](https://www.npmjs.com/package/@adhd/sox-telemetry)
|
|
13
|
+
to the process's durable JSONL sink (`<dir>/<service>.<role>-<date>.jsonl`), so a second process — or
|
|
14
|
+
the backlog graph — can reconstruct one request across transports.
|
|
15
|
+
|
|
16
|
+
It is implemented as a **`Layer` capability** (one layer instruments all three transports). A
|
|
17
|
+
`target` capability is declared for interface parity but emits nothing: `--type tracing` resolves to a
|
|
18
|
+
valid no-op rather than an error. This mirrors `apigen-plugin-logger`.
|
|
19
|
+
|
|
20
|
+
Tracing composes cleanly alongside the logger plugin — same seam, no coupling:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
usePlugins: [tracingPlugin, loggerPlugin, batchPlugin];
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm add @adhd/apigen-plugin-tracing
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### CLI — bare slug
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
apigen run --source ./api.ts --type mcp --use tracing
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Also usable through the v2 multi-use option:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
apigen run --source ./api.ts --type mcp --use @adhd/apigen-plugin-tracing
|
|
44
|
+
# `--type tracing` is a valid no-op target (emits no files, throws no error)
|
|
45
|
+
apigen run --source ./api.ts --type tracing
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Programmatic
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { tracingPlugin, makeTracingPlugin, TraceHandle } from '@adhd/apigen-plugin-tracing';
|
|
52
|
+
|
|
53
|
+
// default plugin — span prefix `apigen` (zero-config)
|
|
54
|
+
run({ usePlugins: [tracingPlugin] });
|
|
55
|
+
|
|
56
|
+
// `makeTracingPlugin(...)` is the configured entry point — use it to set the span prefix and
|
|
57
|
+
// copy declared envelope headers onto every span.
|
|
58
|
+
|
|
59
|
+
// configured
|
|
60
|
+
run({
|
|
61
|
+
usePlugins: [
|
|
62
|
+
makeTracingPlugin({
|
|
63
|
+
serviceName: 'checkout',
|
|
64
|
+
envelopeAttrs: ['x-request-id'],
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Annotating the live span from domain code
|
|
71
|
+
|
|
72
|
+
The layer seeds a `TraceHandle` into `call.ctx`; read it back and add attributes to the live span:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { TraceHandle } from '@adhd/apigen-plugin-tracing';
|
|
76
|
+
|
|
77
|
+
export async function placeOrder(call, ...args) {
|
|
78
|
+
const trace = call.ctx.get(TraceHandle);
|
|
79
|
+
trace?.annotate({ 'order.region': region });
|
|
80
|
+
// …
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`annotate` is a no-op when the operation is not currently inside a unary span (e.g. before the
|
|
85
|
+
downstream resolved, or for a streaming call, which is logged rather than spanned).
|
|
86
|
+
|
|
87
|
+
## Records
|
|
88
|
+
|
|
89
|
+
| Event | Level | Carries |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `<serviceName>.<op>.start` | info | `apigen.op`, `apigen.transport`, `trace_id` |
|
|
92
|
+
| `<serviceName>.<op>.finish` | info | the above + `duration_ms` |
|
|
93
|
+
| `<serviceName>.<op>.error` | error | the above + `error` (the thrown message) |
|
|
94
|
+
| `apigen.op.error` | error | `apigen.op`, `apigen.transport`, `trace_id`, `span`, `duration_ms`, `err` |
|
|
95
|
+
|
|
96
|
+
Streaming operations are **quarantined**: under the current `Next` contract
|
|
97
|
+
(`() => Promise<LayerResult>`), an `AsyncIterable` can only be a *resolved* value, never an
|
|
98
|
+
unresolved one, so the layer spans a streaming op only up to the point the stream is obtained — as a
|
|
99
|
+
unary span. Per-chunk `.{start,finish,error}` records with a chunk count are **not** emitted. The
|
|
100
|
+
`traceStream` helper remains in the package (directly unit-tested) behind a quarantine banner for a
|
|
101
|
+
future reopen-span follow-up; `traceUnary` is the only layer path.
|
|
102
|
+
|
|
103
|
+
## Options
|
|
104
|
+
|
|
105
|
+
| Option | Type | Default | Description |
|
|
106
|
+
|---|---|---|---|
|
|
107
|
+
| `serviceName` | `string` | `'apigen'` | Span / record name prefix. |
|
|
108
|
+
| `envelopeAttrs` | `string[]` | `[]` | Extra `call.envelope` keys copied verbatim onto each span. |
|
|
109
|
+
|
|
110
|
+
## Part of the apigen toolchain
|
|
111
|
+
|
|
112
|
+
See [`@adhd/apigen-cli`](https://www.npmjs.com/package/@adhd/apigen-cli) for the full
|
|
113
|
+
TypeScript-to-API system and the list of available plugins.
|
package/index.d.ts
ADDED
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});let e=require("@adhd/sox-telemetry");var t=class{constructor(e,t,n){this.traceId=e,this.spanName=t,this.startedAt=n}annotate(e){let t=n.get(this);t!==void 0&&t.setAttributes(r(e))}},n=new WeakMap;function r(e){let t={};for(let[n,r]of Object.entries(e))(typeof r==`string`||typeof r==`number`||typeof r==`boolean`)&&(t[n]=r);return t}function i(e,t){let n={};for(let r of t){let t=e[r];t!==void 0&&(n[r]=t)}return n}function a(e){return e instanceof Error?e.message:typeof e==`string`?e:typeof e==`object`&&e&&`message`in e&&typeof e.message==`string`?e.message:String(e)}function o(t,i,o,s){return(0,e.withSpan)(i,r(o),async r=>{n.set(s,r);try{return await t}catch(t){throw e.log.error(`apigen.op.error`,{...o,span:i,duration_ms:Date.now()-s.startedAt,err:a(t)}),t}finally{n.delete(s)}})}function s(n={}){let r=n.serviceName??`apigen`,a=n.envelopeAttrs??[];return(n,s)=>{let c=`${r}.${n.operation.id}`,l=(0,e.currentTraceId)()??(0,e.newTraceId)(),u=new t(l,c,Date.now());n.ctx.set(t,u);let d={...i(n.envelope,a),"apigen.op":n.operation.id,"apigen.transport":n.transport,trace_id:l};return(0,e.withTrace)(l,()=>o(Promise.resolve(s()),c,d,u))}}var c={id:`tracing`,description:`Layer plugin: emits one OTel span per dispatched operation (op id, transport, outcome, duration) to the sox-telemetry JSONL sink, correlated by trace_id.`,language:`ts`,optionsSchema:{type:`object`,properties:{serviceName:{type:`string`,description:`Span / record name prefix. Default: apigen.`},envelopeAttrs:{type:`array`,items:{type:`string`},description:`Extra envelope keys copied verbatim onto each span. Default: [].`}},additionalProperties:!1},capabilities:{target:{name:`tracing`,generate(){return[]}},layer:{layer:s()}}};function l(e={}){return{...c,capabilities:{...c.capabilities,layer:{layer:s(e)}}}}exports.TraceHandle=t,exports.default=c,exports.tracingPlugin=c,exports.makeTracingPlugin=l;
|
package/index.mjs
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { currentTraceId as e, log as t, newTraceId as n, withSpan as r, withTrace as i } from "@adhd/sox-telemetry";
|
|
2
|
+
//#region src/lib/plugin.ts
|
|
3
|
+
var a = class {
|
|
4
|
+
constructor(e, t, n) {
|
|
5
|
+
this.traceId = e, this.spanName = t, this.startedAt = n;
|
|
6
|
+
}
|
|
7
|
+
annotate(e) {
|
|
8
|
+
let t = o.get(this);
|
|
9
|
+
t !== void 0 && t.setAttributes(s(e));
|
|
10
|
+
}
|
|
11
|
+
}, o = /* @__PURE__ */ new WeakMap();
|
|
12
|
+
function s(e) {
|
|
13
|
+
let t = {};
|
|
14
|
+
for (let [n, r] of Object.entries(e)) (typeof r == "string" || typeof r == "number" || typeof r == "boolean") && (t[n] = r);
|
|
15
|
+
return t;
|
|
16
|
+
}
|
|
17
|
+
function c(e, t) {
|
|
18
|
+
let n = {};
|
|
19
|
+
for (let r of t) {
|
|
20
|
+
let t = e[r];
|
|
21
|
+
t !== void 0 && (n[r] = t);
|
|
22
|
+
}
|
|
23
|
+
return n;
|
|
24
|
+
}
|
|
25
|
+
function l(e) {
|
|
26
|
+
return e instanceof Error ? e.message : typeof e == "string" ? e : typeof e == "object" && e && "message" in e && typeof e.message == "string" ? e.message : String(e);
|
|
27
|
+
}
|
|
28
|
+
function u(e, n, i, a) {
|
|
29
|
+
return r(n, s(i), async (r) => {
|
|
30
|
+
o.set(a, r);
|
|
31
|
+
try {
|
|
32
|
+
return await e;
|
|
33
|
+
} catch (e) {
|
|
34
|
+
throw t.error("apigen.op.error", {
|
|
35
|
+
...i,
|
|
36
|
+
span: n,
|
|
37
|
+
duration_ms: Date.now() - a.startedAt,
|
|
38
|
+
err: l(e)
|
|
39
|
+
}), e;
|
|
40
|
+
} finally {
|
|
41
|
+
o.delete(a);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function d(t = {}) {
|
|
46
|
+
let r = t.serviceName ?? "apigen", o = t.envelopeAttrs ?? [];
|
|
47
|
+
return (t, s) => {
|
|
48
|
+
let l = `${r}.${t.operation.id}`, d = e() ?? n(), f = new a(d, l, Date.now());
|
|
49
|
+
t.ctx.set(a, f);
|
|
50
|
+
let p = {
|
|
51
|
+
...c(t.envelope, o),
|
|
52
|
+
"apigen.op": t.operation.id,
|
|
53
|
+
"apigen.transport": t.transport,
|
|
54
|
+
trace_id: d
|
|
55
|
+
};
|
|
56
|
+
return i(d, () => u(Promise.resolve(s()), l, p, f));
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
var f = {
|
|
60
|
+
id: "tracing",
|
|
61
|
+
description: "Layer plugin: emits one OTel span per dispatched operation (op id, transport, outcome, duration) to the sox-telemetry JSONL sink, correlated by trace_id.",
|
|
62
|
+
language: "ts",
|
|
63
|
+
optionsSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
serviceName: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "Span / record name prefix. Default: apigen."
|
|
69
|
+
},
|
|
70
|
+
envelopeAttrs: {
|
|
71
|
+
type: "array",
|
|
72
|
+
items: { type: "string" },
|
|
73
|
+
description: "Extra envelope keys copied verbatim onto each span. Default: []."
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
additionalProperties: !1
|
|
77
|
+
},
|
|
78
|
+
capabilities: {
|
|
79
|
+
target: {
|
|
80
|
+
name: "tracing",
|
|
81
|
+
generate() {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
layer: { layer: d() }
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
function p(e = {}) {
|
|
89
|
+
return {
|
|
90
|
+
...f,
|
|
91
|
+
capabilities: {
|
|
92
|
+
...f.capabilities,
|
|
93
|
+
layer: { layer: d(e) }
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { a as TraceHandle, f as default, f as tracingPlugin, p as makeTracingPlugin };
|
package/lib/plugin.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Plugin, Call, Next, Result, Chunk } from '@adhd/apigen-core-client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for {@link makeTracingPlugin}.
|
|
5
|
+
*
|
|
6
|
+
* Layer plugins receive no opts at call time — `LayerCapability.layer` is invoked with only
|
|
7
|
+
* `(call, next)` — so configuration is a factory, exactly as `makeLoggerPlugin` does it.
|
|
8
|
+
*/
|
|
9
|
+
export interface TracingOptions {
|
|
10
|
+
/** Span / record name prefix. Default: `'apigen'`. */
|
|
11
|
+
serviceName?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Extra attribute keys copied verbatim from `call.envelope` onto each span. Default: `[]`
|
|
14
|
+
* (only the built-in attrs `apigen.op`, `apigen.transport`, `trace_id` are emitted).
|
|
15
|
+
*/
|
|
16
|
+
envelopeAttrs?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Per-call trace handle, seeded into `call.ctx`. The class is the ctx token: domain code reads
|
|
20
|
+
* it back with `call.ctx.get(TraceHandle)`.
|
|
21
|
+
*/
|
|
22
|
+
export declare class TraceHandle {
|
|
23
|
+
/** Correlation id shared by every span of one logical request. */
|
|
24
|
+
readonly traceId: string;
|
|
25
|
+
/** Span name — `${serviceName}.${operation.id}`. */
|
|
26
|
+
readonly spanName: string;
|
|
27
|
+
/** Epoch millis at which the span opened. */
|
|
28
|
+
readonly startedAt: number;
|
|
29
|
+
constructor(
|
|
30
|
+
/** Correlation id shared by every span of one logical request. */
|
|
31
|
+
traceId: string,
|
|
32
|
+
/** Span name — `${serviceName}.${operation.id}`. */
|
|
33
|
+
spanName: string,
|
|
34
|
+
/** Epoch millis at which the span opened. */
|
|
35
|
+
startedAt: number);
|
|
36
|
+
/**
|
|
37
|
+
* Add attributes to the live span from domain code — opt-in. A no-op when the operation is
|
|
38
|
+
* not currently inside a unary span (e.g. before the downstream resolved, or for a streaming
|
|
39
|
+
* call, which is logged rather than spanned).
|
|
40
|
+
*/
|
|
41
|
+
annotate(fields: Record<string, unknown>): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* QUARANTINED — unreachable under the current `Next` contract; see CONTRACT-FIX §4.
|
|
45
|
+
* Exported only so the streaming branch can be exercised by a direct unit test; no layer
|
|
46
|
+
* path reaches it (a `Next` resolves to a `LayerResult`, so this detector can never be
|
|
47
|
+
* true for the unresolved `next()` promise). Retained — never deleted — pending a `Next`
|
|
48
|
+
* that can yield an iterable before resolution.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isAsyncIterable(value: unknown): value is AsyncIterable<Chunk>;
|
|
51
|
+
/**
|
|
52
|
+
* QUARANTINED — unreachable under the current `Next` contract; see CONTRACT-FIX §4.
|
|
53
|
+
* The layer no longer branches on `next()` (an `AsyncIterable` can only be a *resolved*
|
|
54
|
+
* value, never the unresolved promise), so this is retained with its body intact and
|
|
55
|
+
* exported solely for a direct unit test. Do NOT re-wire it into the layer by awaiting
|
|
56
|
+
* `next()` first — that would destroy the span-before-body invariant guarded by
|
|
57
|
+
* `plugin.spec.ts` (the hang-visibility test).
|
|
58
|
+
*
|
|
59
|
+
* Streaming branch — `withSpan` cannot await an iterable, so the `.start` record is emitted
|
|
60
|
+
* synchronously on entry and an async-generator wrapper yields each chunk unchanged, then
|
|
61
|
+
* emits `.finish` with the chunk count (or `.error` and re-throws).
|
|
62
|
+
*/
|
|
63
|
+
export declare function traceStream(downstream: AsyncIterable<Chunk>, spanName: string, attrs: Record<string, unknown>): AsyncIterable<Chunk>;
|
|
64
|
+
/**
|
|
65
|
+
* Build the tracing layer for a given configuration. The returned layer calls `next()` exactly
|
|
66
|
+
* once and returns its resolved value (or iterable) unchanged, wrapping it in a span.
|
|
67
|
+
*/
|
|
68
|
+
export declare function makeTraceLayer(opts?: TracingOptions): (call: Call, next: Next) => Promise<Result> | AsyncIterable<Chunk>;
|
|
69
|
+
/**
|
|
70
|
+
* Tracing plugin.
|
|
71
|
+
*
|
|
72
|
+
* Implemented as a **Layer** capability — a single layer instruments every transport (http, grpc,
|
|
73
|
+
* mcp, cli) because they all dispatch through one composed invoker. The `target` capability is
|
|
74
|
+
* declared but emits nothing (`generate()` returns `[]`), so `--type tracing` resolves to a valid
|
|
75
|
+
* no-op rather than an error — the same precedent as `apigen-plugin-logger`.
|
|
76
|
+
*/
|
|
77
|
+
export declare const tracingPlugin: Plugin<TracingOptions>;
|
|
78
|
+
/** Configured factory — rebuilds the layer with `opts` (logger precedent). */
|
|
79
|
+
export declare function makeTracingPlugin(opts?: TracingOptions): Plugin<TracingOptions>;
|
|
80
|
+
export default tracingPlugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhd/apigen-plugin-tracing",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"dependencies": {
|
|
5
|
+
"@adhd/apigen-core-client": "^0.3.3",
|
|
6
|
+
"@adhd/sox-telemetry": "^0.3.2"
|
|
7
|
+
},
|
|
8
|
+
"main": "./index.js",
|
|
9
|
+
"module": "./index.mjs",
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"description": "Tracing plugin for apigen servers",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"tracing",
|
|
17
|
+
"telemetry",
|
|
18
|
+
"otel",
|
|
19
|
+
"apigen",
|
|
20
|
+
"plugin",
|
|
21
|
+
"typescript"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/PseudoSky/adhd.git",
|
|
27
|
+
"directory": "packages/apigen/apigen-plugin-tracing"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/PseudoSky/adhd/tree/main/packages/apigen/apigen-plugin-tracing#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/PseudoSky/adhd/issues"
|
|
32
|
+
}
|
|
33
|
+
}
|