pi-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 +183 -0
- package/README.md +66 -0
- package/dist/attrs.d.ts +67 -0
- package/dist/attrs.js +124 -0
- package/dist/attrs.js.map +1 -0
- package/dist/commands/otel.d.ts +2 -0
- package/dist/commands/otel.js +438 -0
- package/dist/commands/otel.js.map +1 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +132 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +274 -0
- package/dist/index.js.map +1 -0
- package/dist/otel/logs.d.ts +14 -0
- package/dist/otel/logs.js +103 -0
- package/dist/otel/logs.js.map +1 -0
- package/dist/otel/metrics.d.ts +11 -0
- package/dist/otel/metrics.js +50 -0
- package/dist/otel/metrics.js.map +1 -0
- package/dist/otel/sdk.d.ts +14 -0
- package/dist/otel/sdk.js +162 -0
- package/dist/otel/sdk.js.map +1 -0
- package/dist/spans.d.ts +74 -0
- package/dist/spans.js +504 -0
- package/dist/spans.js.map +1 -0
- package/package.json +76 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-otel — OpenTelemetry traces for pi-coding-agent.
|
|
3
|
+
*
|
|
4
|
+
* Wires pi lifecycle events into an OTel span tree:
|
|
5
|
+
* pi.interaction (per user prompt)
|
|
6
|
+
* ├─ pi.llm_request
|
|
7
|
+
* └─ pi.tool.<name>
|
|
8
|
+
*
|
|
9
|
+
* See `_plans/SPEC.md` for the full design.
|
|
10
|
+
*
|
|
11
|
+
* The `/otel` command (Aspire launcher) is registered below via
|
|
12
|
+
* `registerOtelCommand`. We also expose `pi.events` channels
|
|
13
|
+
* (`pi-otel:status`, `pi-otel:trace-active`) for future consumers.
|
|
14
|
+
*
|
|
15
|
+
* ## pi-otel:log — extensibility API for other pi packages
|
|
16
|
+
*
|
|
17
|
+
* Any pi extension can route structured log records through pi-otel by emitting:
|
|
18
|
+
*
|
|
19
|
+
* pi.events.emit("pi-otel:log", {
|
|
20
|
+
* eventName: "my-package.something", // lands as event.name attribute
|
|
21
|
+
* severity: "info", // "debug" | "info" | "warn" | "error"
|
|
22
|
+
* body: "human-readable message",
|
|
23
|
+
* attributes: { "key": "value" }, // optional; string | number | boolean values
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* No-op if signals.logs is disabled or the OTel SDK is not yet initialized.
|
|
27
|
+
* pi-otel uses this channel internally for its own lifecycle events.
|
|
28
|
+
*/
|
|
29
|
+
import { basename } from "node:path";
|
|
30
|
+
import { trace } from "@opentelemetry/api";
|
|
31
|
+
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
32
|
+
import { ATTR_FINISH_REASONS, ATTR_HTTP_STATUS_CODE, ATTR_PI_CWD, ATTR_PI_SESSION_ID, ATTR_REQUEST_MODEL, ATTR_RESPONSE_ID, ATTR_RESPONSE_MODEL, ATTR_SYSTEM, applyUsageAttrs, GEN_AI_SYSTEM_PI, } from "./attrs.js";
|
|
33
|
+
import { registerOtelCommand } from "./commands/otel.js";
|
|
34
|
+
import { resolveConfig } from "./config.js";
|
|
35
|
+
import { emitLifecycleLog } from "./otel/logs.js";
|
|
36
|
+
import { initSdk, probeEndpoint, shutdownSdk } from "./otel/sdk.js";
|
|
37
|
+
import { SpanTracker } from "./spans.js";
|
|
38
|
+
const TRACER_NAME = "pi-otel";
|
|
39
|
+
const TRACER_VERSION = "0.1.0";
|
|
40
|
+
const SEVERITY_MAP = {
|
|
41
|
+
debug: SeverityNumber.DEBUG,
|
|
42
|
+
info: SeverityNumber.INFO,
|
|
43
|
+
warn: SeverityNumber.WARN,
|
|
44
|
+
error: SeverityNumber.ERROR,
|
|
45
|
+
};
|
|
46
|
+
export default function (pi) {
|
|
47
|
+
registerOtelCommand(pi, () => ctx0?.cwd);
|
|
48
|
+
// pi-otel:log — any pi extension can emit structured log records through
|
|
49
|
+
// pi-otel. No-op when signals.logs is disabled (LoggerProvider not registered).
|
|
50
|
+
pi.events.on("pi-otel:log", (data) => {
|
|
51
|
+
if (!data || typeof data !== "object")
|
|
52
|
+
return;
|
|
53
|
+
const { eventName = "pi-otel.log", severity = "info", body = "", attributes = {}, } = data;
|
|
54
|
+
emitLifecycleLog(eventName, SEVERITY_MAP[severity] ?? SeverityNumber.INFO, body, attributes);
|
|
55
|
+
});
|
|
56
|
+
let ctx0;
|
|
57
|
+
let tracker = null;
|
|
58
|
+
let sessionIdRef;
|
|
59
|
+
let sessionStartLogged = false;
|
|
60
|
+
const notify = (msg, severity = "info") => {
|
|
61
|
+
try {
|
|
62
|
+
ctx0?.ui?.notify?.(msg, severity);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// best-effort
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
function wireSdk(cfg, opts = {}) {
|
|
69
|
+
initSdk(cfg, notify, opts);
|
|
70
|
+
const tracer = trace.getTracer(TRACER_NAME, TRACER_VERSION);
|
|
71
|
+
tracker = new SpanTracker({
|
|
72
|
+
tracer,
|
|
73
|
+
captureContent: cfg.captureContent,
|
|
74
|
+
cwd: cfg.cwd,
|
|
75
|
+
sessionId: () => sessionIdRef,
|
|
76
|
+
});
|
|
77
|
+
pi.events.emit("pi-otel:status", {
|
|
78
|
+
state: "ready",
|
|
79
|
+
endpoint: cfg.endpoint,
|
|
80
|
+
});
|
|
81
|
+
// Fire once: wiring can happen at session_start OR later via dashboard-ready.
|
|
82
|
+
if (!sessionStartLogged) {
|
|
83
|
+
sessionStartLogged = true;
|
|
84
|
+
pi.events.emit("pi-otel:log", {
|
|
85
|
+
eventName: "pi.session.start",
|
|
86
|
+
severity: "info",
|
|
87
|
+
body: `pi session ${sessionIdRef ?? "(ephemeral)"} started`,
|
|
88
|
+
attributes: {
|
|
89
|
+
[ATTR_SYSTEM]: GEN_AI_SYSTEM_PI,
|
|
90
|
+
[ATTR_PI_CWD]: cfg.cwd,
|
|
91
|
+
"service.name": cfg.serviceName,
|
|
92
|
+
...(sessionIdRef ? { [ATTR_PI_SESSION_ID]: sessionIdRef } : {}),
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
98
|
+
ctx0 = ctx;
|
|
99
|
+
const cfg = resolveConfig(ctx.cwd);
|
|
100
|
+
if (!cfg.enabled) {
|
|
101
|
+
tracker = null;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Best-effort session id from the session manager.
|
|
105
|
+
try {
|
|
106
|
+
const file = ctx.sessionManager?.getSessionFile?.();
|
|
107
|
+
if (file)
|
|
108
|
+
sessionIdRef = basename(file, ".jsonl");
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// ignore
|
|
112
|
+
}
|
|
113
|
+
// Defer SDK init until the endpoint is reachable — otherwise the metric
|
|
114
|
+
// reader / log processor begin retrying against a dead endpoint and the
|
|
115
|
+
// resulting errors get buffered and flushed once it comes online.
|
|
116
|
+
if (await probeEndpoint(cfg.endpoint)) {
|
|
117
|
+
wireSdk(cfg);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
notify(`pi-otel: OTLP endpoint ${cfg.endpoint} not reachable — run /otel start to launch a dashboard, or /otel connect <endpoint> to wire elsewhere.`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
const logError = (eventName, body, attrs = {}) => pi.events.emit("pi-otel:log", {
|
|
124
|
+
eventName,
|
|
125
|
+
severity: "error",
|
|
126
|
+
body,
|
|
127
|
+
attributes: attrs,
|
|
128
|
+
});
|
|
129
|
+
pi.on("before_agent_start", async (event, _ctx) => {
|
|
130
|
+
tracker?.startInteraction(event?.prompt);
|
|
131
|
+
const tid = tracker?.activeTraceId();
|
|
132
|
+
if (tid)
|
|
133
|
+
pi.events.emit("pi-otel:trace-active", { traceId: tid });
|
|
134
|
+
});
|
|
135
|
+
pi.on("turn_start", async (event, _ctx) => {
|
|
136
|
+
const idx = event?.turnIndex;
|
|
137
|
+
tracker?.startTurn(typeof idx === "number" ? idx : undefined);
|
|
138
|
+
});
|
|
139
|
+
pi.on("turn_end", async (_event, _ctx) => {
|
|
140
|
+
tracker?.endTurn();
|
|
141
|
+
});
|
|
142
|
+
pi.on("message_start", async (event, _ctx) => {
|
|
143
|
+
const msg = event?.message;
|
|
144
|
+
if (!msg)
|
|
145
|
+
return;
|
|
146
|
+
if (msg.role === "user") {
|
|
147
|
+
tracker?.noteUserMessage(msg.content);
|
|
148
|
+
}
|
|
149
|
+
else if (msg.role === "toolResult") {
|
|
150
|
+
tracker?.noteToolResultMessage({
|
|
151
|
+
toolCallId: msg.toolCallId,
|
|
152
|
+
toolName: msg.toolName,
|
|
153
|
+
content: msg.content,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
pi.on("before_provider_request", async (event, _ctx) => {
|
|
158
|
+
// event.payload shape varies per provider; try to lift a model field.
|
|
159
|
+
const payload = event?.payload;
|
|
160
|
+
const model = payload?.model ?? payload?.modelId ?? payload?.modelName ?? undefined;
|
|
161
|
+
tracker?.startLlmRequest(typeof model === "string" ? model : undefined);
|
|
162
|
+
if (typeof model === "string") {
|
|
163
|
+
tracker?.setLlmAttrs({ [ATTR_REQUEST_MODEL]: model });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
pi.on("after_provider_response", async (event, _ctx) => {
|
|
167
|
+
const status = event?.status;
|
|
168
|
+
const headers = event?.headers ?? {};
|
|
169
|
+
const attrs = {};
|
|
170
|
+
if (typeof status === "number")
|
|
171
|
+
attrs[ATTR_HTTP_STATUS_CODE] = status;
|
|
172
|
+
// Common response-id headers across providers.
|
|
173
|
+
const respId = headers["x-request-id"] ??
|
|
174
|
+
headers["request-id"] ??
|
|
175
|
+
headers["anthropic-request-id"] ??
|
|
176
|
+
headers["openai-response-id"];
|
|
177
|
+
if (typeof respId === "string")
|
|
178
|
+
attrs[ATTR_RESPONSE_ID] = respId;
|
|
179
|
+
tracker?.setLlmAttrs(attrs);
|
|
180
|
+
// Note: end is deferred to message_end so we can attach usage/cost.
|
|
181
|
+
});
|
|
182
|
+
pi.on("message_end", async (event, _ctx) => {
|
|
183
|
+
const msg = event?.message;
|
|
184
|
+
if (!msg || msg.role !== "assistant")
|
|
185
|
+
return;
|
|
186
|
+
const attrs = {};
|
|
187
|
+
if (typeof msg.model === "string")
|
|
188
|
+
attrs[ATTR_RESPONSE_MODEL] = msg.model;
|
|
189
|
+
const finish = msg.finishReason ?? msg.stopReason ?? msg.finish_reason;
|
|
190
|
+
if (typeof finish === "string")
|
|
191
|
+
attrs[ATTR_FINISH_REASONS] = [finish];
|
|
192
|
+
applyUsageAttrs(attrs, msg.usage);
|
|
193
|
+
tracker?.setLlmAttrs(attrs);
|
|
194
|
+
tracker?.noteAssistantMessage(msg);
|
|
195
|
+
tracker?.endLlmRequest();
|
|
196
|
+
if (finish === "error") {
|
|
197
|
+
logError("pi.llm_request.error", msg.errorMessage ?? `LLM request failed (${finish})`, {
|
|
198
|
+
...(typeof msg.model === "string"
|
|
199
|
+
? { [ATTR_RESPONSE_MODEL]: msg.model }
|
|
200
|
+
: {}),
|
|
201
|
+
[ATTR_FINISH_REASONS]: finish,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
pi.on("tool_execution_start", async (event, _ctx) => {
|
|
206
|
+
const e = event;
|
|
207
|
+
if (!e?.toolCallId || !e?.toolName)
|
|
208
|
+
return;
|
|
209
|
+
tracker?.startTool(e.toolCallId, e.toolName, e.args);
|
|
210
|
+
});
|
|
211
|
+
pi.on("tool_execution_end", async (event, _ctx) => {
|
|
212
|
+
const e = event;
|
|
213
|
+
if (!e?.toolCallId)
|
|
214
|
+
return;
|
|
215
|
+
tracker?.endTool(e.toolCallId, { isError: !!e.isError, result: e.result });
|
|
216
|
+
if (e.isError) {
|
|
217
|
+
logError("pi.tool.error", `tool ${e.toolName} failed`, {
|
|
218
|
+
"gen_ai.tool.name": e.toolName,
|
|
219
|
+
"gen_ai.tool.call.id": e.toolCallId,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
pi.on("agent_end", async (_event, _ctx) => {
|
|
224
|
+
tracker?.endInteraction();
|
|
225
|
+
});
|
|
226
|
+
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
227
|
+
// Defensive: close any in-flight interaction before flushing.
|
|
228
|
+
tracker?.endInteraction();
|
|
229
|
+
pi.events.emit("pi-otel:log", {
|
|
230
|
+
eventName: "pi.session.end",
|
|
231
|
+
severity: "info",
|
|
232
|
+
body: `pi session ${sessionIdRef ?? "(ephemeral)"} ended`,
|
|
233
|
+
attributes: {
|
|
234
|
+
[ATTR_SYSTEM]: GEN_AI_SYSTEM_PI,
|
|
235
|
+
...(sessionIdRef ? { [ATTR_PI_SESSION_ID]: sessionIdRef } : {}),
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
await shutdownSdk();
|
|
239
|
+
pi.events.emit("pi-otel:status", { state: "shutdown" });
|
|
240
|
+
tracker = null;
|
|
241
|
+
});
|
|
242
|
+
// Anchor exported for the launcher extension. The launcher can call
|
|
243
|
+
// `pi.events.emit("pi-otel:request-status", null)` and we reply with state.
|
|
244
|
+
pi.events.on("pi-otel:request-status", () => {
|
|
245
|
+
pi.events.emit("pi-otel:status", {
|
|
246
|
+
state: tracker ? "ready" : "disabled",
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
// Re-init the SDK when the dashboard becomes available mid-session, or when
|
|
250
|
+
// `/otel connect` rewires us to an external collector. Without this, the
|
|
251
|
+
// exporter keeps retrying against the dead endpoint it was wired to at
|
|
252
|
+
// session_start. Payload `endpoint`/`protocol` override resolved config.
|
|
253
|
+
pi.events.on("pi-otel:dashboard-ready", async (payload) => {
|
|
254
|
+
if (!ctx0)
|
|
255
|
+
return;
|
|
256
|
+
const cfg = resolveConfig(ctx0.cwd);
|
|
257
|
+
if (!cfg.enabled)
|
|
258
|
+
return;
|
|
259
|
+
const override = (payload ?? {});
|
|
260
|
+
if (typeof override.endpoint === "string" && override.endpoint) {
|
|
261
|
+
cfg.endpoint = override.endpoint;
|
|
262
|
+
}
|
|
263
|
+
if (typeof override.protocol === "string" && override.protocol) {
|
|
264
|
+
cfg.protocol = override.protocol;
|
|
265
|
+
}
|
|
266
|
+
await shutdownSdk();
|
|
267
|
+
// Caller (e.g. /otel start) already notified success; don't clobber it.
|
|
268
|
+
wireSdk(cfg, { silentSuccess: true });
|
|
269
|
+
});
|
|
270
|
+
// TODO(SPEC §9 q1): inject TRACEPARENT into bash subprocesses once pi
|
|
271
|
+
// exposes a subprocess-env hook for the bash tool.
|
|
272
|
+
// TODO(SPEC §7 P3): redaction + truncation for captureContent != "full".
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LogRecord emission helpers + OTel diag→OTLP bridge.
|
|
3
|
+
*
|
|
4
|
+
* Pi-otel's OWN internal events do NOT use the global `diag` — they go
|
|
5
|
+
* through the `notify` callback wired from index.ts (ctx.ui.notify), because
|
|
6
|
+
* failing OTLP machinery cannot reliably report its own failures through
|
|
7
|
+
* itself.
|
|
8
|
+
*/
|
|
9
|
+
import type { DiagLogger } from "@opentelemetry/api";
|
|
10
|
+
import { type LogAttributes, type Logger, SeverityNumber } from "@opentelemetry/api-logs";
|
|
11
|
+
export declare function getLogger(): Logger;
|
|
12
|
+
export declare function resetLogHandles(): void;
|
|
13
|
+
export declare function emitLifecycleLog(eventName: string, severity: SeverityNumber, body: string, attrs?: LogAttributes): void;
|
|
14
|
+
export declare function buildBridgeDiagLogger(): DiagLogger;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LogRecord emission helpers + OTel diag→OTLP bridge.
|
|
3
|
+
*
|
|
4
|
+
* Pi-otel's OWN internal events do NOT use the global `diag` — they go
|
|
5
|
+
* through the `notify` callback wired from index.ts (ctx.ui.notify), because
|
|
6
|
+
* failing OTLP machinery cannot reliably report its own failures through
|
|
7
|
+
* itself.
|
|
8
|
+
*/
|
|
9
|
+
import { logs, SeverityNumber, } from "@opentelemetry/api-logs";
|
|
10
|
+
const LOGGER_NAME = "pi-otel";
|
|
11
|
+
const LOGGER_VERSION = "0.1.0";
|
|
12
|
+
const BRIDGE_LOGGER_NAME = "@opentelemetry/diag";
|
|
13
|
+
let logger = null;
|
|
14
|
+
let bridgeLogger = null;
|
|
15
|
+
export function getLogger() {
|
|
16
|
+
if (!logger) {
|
|
17
|
+
logger = logs.getLogger(LOGGER_NAME, LOGGER_VERSION);
|
|
18
|
+
}
|
|
19
|
+
return logger;
|
|
20
|
+
}
|
|
21
|
+
function getBridgeLogger() {
|
|
22
|
+
if (!bridgeLogger) {
|
|
23
|
+
bridgeLogger = logs.getLogger(BRIDGE_LOGGER_NAME, LOGGER_VERSION);
|
|
24
|
+
}
|
|
25
|
+
return bridgeLogger;
|
|
26
|
+
}
|
|
27
|
+
export function resetLogHandles() {
|
|
28
|
+
logger = null;
|
|
29
|
+
bridgeLogger = null;
|
|
30
|
+
}
|
|
31
|
+
function emitLogRecord(log, severity, body, attributes) {
|
|
32
|
+
try {
|
|
33
|
+
log.emit({
|
|
34
|
+
severityNumber: severity,
|
|
35
|
+
severityText: SeverityNumber[severity],
|
|
36
|
+
body,
|
|
37
|
+
attributes,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// best-effort
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function emitLifecycleLog(eventName, severity, body, attrs = {}) {
|
|
45
|
+
emitLogRecord(getLogger(), severity, body, {
|
|
46
|
+
"event.name": eventName,
|
|
47
|
+
...attrs,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function stringifyArg(a) {
|
|
51
|
+
if (typeof a === "string")
|
|
52
|
+
return a;
|
|
53
|
+
if (a instanceof Error)
|
|
54
|
+
return a.stack ?? `${a.name}: ${a.message}`;
|
|
55
|
+
// grpc-js callErrorFromStatus() uses Object.assign(new Error(), status), copying
|
|
56
|
+
// code/details/metadata as own properties. In some runtimes instanceof fails across
|
|
57
|
+
// module boundaries — fall back to duck-typing so we emit the stack, not JSON.
|
|
58
|
+
if (a &&
|
|
59
|
+
typeof a === "object" &&
|
|
60
|
+
typeof a.stack === "string") {
|
|
61
|
+
return a.stack;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return JSON.stringify(a);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return String(a);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Drop per-export ticks from the OTLP exporter delegate — they fire every
|
|
71
|
+
// batch interval and drown signal in Aspire Structured Logs.
|
|
72
|
+
const BRIDGE_DROP = /^(?:items to be sent|OTLPExportDelegate|Export\()/i;
|
|
73
|
+
// Guards against re-entrant calls: logs.getLogger() itself calls diag.warn(),
|
|
74
|
+
// which would recurse infinitely if bridgeLogger is not yet cached.
|
|
75
|
+
let bridgeEmitting = false;
|
|
76
|
+
function emitBridge(severity, message, args) {
|
|
77
|
+
if (bridgeEmitting)
|
|
78
|
+
return;
|
|
79
|
+
// OTel JS internals occasionally pass an Error as the first arg even though
|
|
80
|
+
// DiagLogger types it as string. Normalize so body is always a human-readable
|
|
81
|
+
// string (Aspire renders body as the Message column).
|
|
82
|
+
const text = typeof message === "string" ? message : stringifyArg(message);
|
|
83
|
+
if (BRIDGE_DROP.test(text))
|
|
84
|
+
return;
|
|
85
|
+
const attributes = args.length > 0 ? { "diag.args": args.map(stringifyArg) } : {};
|
|
86
|
+
bridgeEmitting = true;
|
|
87
|
+
try {
|
|
88
|
+
emitLogRecord(getBridgeLogger(), severity, text, attributes);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
bridgeEmitting = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function buildBridgeDiagLogger() {
|
|
95
|
+
return {
|
|
96
|
+
verbose: (message, ...args) => emitBridge(SeverityNumber.DEBUG, message, args),
|
|
97
|
+
debug: (message, ...args) => emitBridge(SeverityNumber.DEBUG, message, args),
|
|
98
|
+
info: (message, ...args) => emitBridge(SeverityNumber.INFO, message, args),
|
|
99
|
+
warn: (message, ...args) => emitBridge(SeverityNumber.WARN, message, args),
|
|
100
|
+
error: (message, ...args) => emitBridge(SeverityNumber.ERROR, message, args),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=logs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logs.js","sourceRoot":"","sources":["../../src/otel/logs.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenAI client histograms — sigil-aligned per OTel semconv. Lazy so this
|
|
3
|
+
* module is safe to import when metrics are disabled (global MeterProvider
|
|
4
|
+
* is then no-op).
|
|
5
|
+
*/
|
|
6
|
+
import { type Counter, type Histogram } from "@opentelemetry/api";
|
|
7
|
+
export declare const getDurationHistogram: () => Histogram<import("@opentelemetry/api").Attributes>;
|
|
8
|
+
export declare const getTokenHistogram: () => Histogram<import("@opentelemetry/api").Attributes>;
|
|
9
|
+
export declare const getToolCallsHistogram: () => Histogram<import("@opentelemetry/api").Attributes>;
|
|
10
|
+
export declare const getToolCallsCounter: () => Counter<import("@opentelemetry/api").Attributes>;
|
|
11
|
+
export declare function resetMetricHandles(): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenAI client histograms — sigil-aligned per OTel semconv. Lazy so this
|
|
3
|
+
* module is safe to import when metrics are disabled (global MeterProvider
|
|
4
|
+
* is then no-op).
|
|
5
|
+
*/
|
|
6
|
+
import { metrics, } from "@opentelemetry/api";
|
|
7
|
+
const METER_NAME = "pi-otel";
|
|
8
|
+
const METER_VERSION = "0.1.0";
|
|
9
|
+
const cache = new Map();
|
|
10
|
+
function getHistogram(name, opts) {
|
|
11
|
+
let h = cache.get(name);
|
|
12
|
+
if (!h) {
|
|
13
|
+
h = metrics.getMeter(METER_NAME, METER_VERSION).createHistogram(name, opts);
|
|
14
|
+
cache.set(name, h);
|
|
15
|
+
}
|
|
16
|
+
return h;
|
|
17
|
+
}
|
|
18
|
+
function getCounter(name, opts) {
|
|
19
|
+
let c = cache.get(name);
|
|
20
|
+
if (!c) {
|
|
21
|
+
c = metrics.getMeter(METER_NAME, METER_VERSION).createCounter(name, opts);
|
|
22
|
+
cache.set(name, c);
|
|
23
|
+
}
|
|
24
|
+
return c;
|
|
25
|
+
}
|
|
26
|
+
export const getDurationHistogram = () => getHistogram("gen_ai.client.operation.duration", {
|
|
27
|
+
description: "Duration of GenAI client operations",
|
|
28
|
+
unit: "s",
|
|
29
|
+
});
|
|
30
|
+
export const getTokenHistogram = () => getHistogram("gen_ai.client.token.usage", {
|
|
31
|
+
description: "Number of tokens used in GenAI client operations",
|
|
32
|
+
unit: "{token}",
|
|
33
|
+
});
|
|
34
|
+
// Step-1 integer buckets up to 32. Default OTel boundaries start at 5, so
|
|
35
|
+
// per-op counts of 0/1/2 all land in the first bucket and percentile readers
|
|
36
|
+
// (e.g. Aspire) report the bucket upper bound instead of the actual value.
|
|
37
|
+
const TOOL_CALL_BUCKETS = Array.from({ length: 33 }, (_, i) => i);
|
|
38
|
+
export const getToolCallsHistogram = () => getHistogram("gen_ai.client.tool_calls_per_operation", {
|
|
39
|
+
description: "Number of tool calls per GenAI client operation",
|
|
40
|
+
unit: "{call}",
|
|
41
|
+
advice: { explicitBucketBoundaries: TOOL_CALL_BUCKETS },
|
|
42
|
+
});
|
|
43
|
+
export const getToolCallsCounter = () => getCounter("gen_ai.client.tool.calls", {
|
|
44
|
+
description: "Total number of tool calls invoked by the agent",
|
|
45
|
+
unit: "{call}",
|
|
46
|
+
});
|
|
47
|
+
export function resetMetricHandles() {
|
|
48
|
+
cache.clear();
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/otel/metrics.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAIL,OAAO,GACR,MAAM,oBAAoB,CAAC;AAE5B,MAAM,UAAU,GAAG,SAAS,CAAC;AAC7B,MAAM,aAAa,GAAG,OAAO,CAAC;AAE9B,MAAM,KAAK,GAAG,IAAI,GAAG,EAA+B,CAAC;AAErD,SAAS,YAAY,CAAC,IAAY,EAAE,IAAmB;IACrD,IAAI,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAA0B,CAAC;IACjD,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC5E,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,UAAU,CAAC,IAAY,EAAE,IAAmB;IACnD,IAAI,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAwB,CAAC;IAC/C,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC1E,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,EAAE,CACvC,YAAY,CAAC,kCAAkC,EAAE;IAC/C,WAAW,EAAE,qCAAqC;IAClD,IAAI,EAAE,GAAG;CACV,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,EAAE,CACpC,YAAY,CAAC,2BAA2B,EAAE;IACxC,WAAW,EAAE,kDAAkD;IAC/D,IAAI,EAAE,SAAS;CAChB,CAAC,CAAC;AAEL,0EAA0E;AAC1E,6EAA6E;AAC7E,2EAA2E;AAC3E,MAAM,iBAAiB,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;AAElE,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,EAAE,CACxC,YAAY,CAAC,wCAAwC,EAAE;IACrD,WAAW,EAAE,iDAAiD;IAC9D,IAAI,EAAE,QAAQ;IACd,MAAM,EAAE,EAAE,wBAAwB,EAAE,iBAAiB,EAAE;CACxD,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,EAAE,CACtC,UAAU,CAAC,0BAA0B,EAAE;IACrC,WAAW,EAAE,iDAAiD;IAC9D,IAAI,EAAE,QAAQ;CACf,CAAC,CAAC;AAEL,MAAM,UAAU,kBAAkB;IAChC,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTel SDK bootstrap. One global SDK per process — pi loads us per session
|
|
3
|
+
* but the SDK is shared across sessions in the same process.
|
|
4
|
+
*/
|
|
5
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
6
|
+
import type { OtelConfig } from "../config.js";
|
|
7
|
+
export type NotifySeverity = "info" | "warning" | "error";
|
|
8
|
+
export type Notify = (msg: string, severity?: NotifySeverity) => void;
|
|
9
|
+
export declare function probeTcp(host: string, port: number, timeoutMs?: number): Promise<boolean>;
|
|
10
|
+
export declare function probeEndpoint(endpoint: string, timeoutMs?: number): Promise<boolean>;
|
|
11
|
+
export declare function initSdk(cfg: OtelConfig, notify?: Notify, opts?: {
|
|
12
|
+
silentSuccess?: boolean;
|
|
13
|
+
}): NodeSDK | null;
|
|
14
|
+
export declare function shutdownSdk(): Promise<void>;
|
package/dist/otel/sdk.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTel SDK bootstrap. One global SDK per process — pi loads us per session
|
|
3
|
+
* but the SDK is shared across sessions in the same process.
|
|
4
|
+
*/
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { createConnection } from "node:net";
|
|
7
|
+
import { diag, metrics, trace } from "@opentelemetry/api";
|
|
8
|
+
import { logs } from "@opentelemetry/api-logs";
|
|
9
|
+
import { OTLPLogExporter as LogGrpcExporter } from "@opentelemetry/exporter-logs-otlp-grpc";
|
|
10
|
+
import { OTLPLogExporter as LogHttpExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
11
|
+
import { OTLPLogExporter as LogProtoExporter } from "@opentelemetry/exporter-logs-otlp-proto";
|
|
12
|
+
import { OTLPMetricExporter as MetricGrpcExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
|
|
13
|
+
import { OTLPMetricExporter as MetricHttpExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
14
|
+
import { OTLPMetricExporter as MetricProtoExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
|
15
|
+
import { OTLPTraceExporter as GrpcExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
|
|
16
|
+
import { OTLPTraceExporter as HttpExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
17
|
+
import { OTLPTraceExporter as ProtoExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
18
|
+
import { Resource } from "@opentelemetry/resources";
|
|
19
|
+
import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
20
|
+
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
21
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
22
|
+
import { BatchSpanProcessor, ParentBasedSampler, TraceIdRatioBasedSampler, } from "@opentelemetry/sdk-trace-base";
|
|
23
|
+
import { ATTR_SERVICE_INSTANCE_ID, ATTR_SERVICE_NAME, } from "@opentelemetry/semantic-conventions/incubating";
|
|
24
|
+
import { ATTR_PI_CWD } from "../attrs.js";
|
|
25
|
+
import { buildBridgeDiagLogger, resetLogHandles } from "./logs.js";
|
|
26
|
+
import { resetMetricHandles } from "./metrics.js";
|
|
27
|
+
let sdk = null;
|
|
28
|
+
let initOnce = false;
|
|
29
|
+
export function probeTcp(host, port, timeoutMs = 300) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const sock = createConnection({ port, host });
|
|
32
|
+
const done = (ok) => {
|
|
33
|
+
sock.destroy();
|
|
34
|
+
resolve(ok);
|
|
35
|
+
};
|
|
36
|
+
sock.setTimeout(timeoutMs);
|
|
37
|
+
sock.once("connect", () => done(true));
|
|
38
|
+
sock.once("timeout", () => done(false));
|
|
39
|
+
sock.once("error", () => done(false));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Used at session_start to avoid wiring exporters at a dead endpoint —
|
|
43
|
+
// otherwise the metric reader / log processor begin retrying immediately and
|
|
44
|
+
// those failures get buffered and flushed once the endpoint comes online.
|
|
45
|
+
export function probeEndpoint(endpoint, timeoutMs = 300) {
|
|
46
|
+
let u;
|
|
47
|
+
try {
|
|
48
|
+
u = new URL(endpoint);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return Promise.resolve(false);
|
|
52
|
+
}
|
|
53
|
+
// OTLP endpoints always carry an explicit port; refuse to fall back to
|
|
54
|
+
// 80/443, which could silently green-light an unrelated service.
|
|
55
|
+
if (!u.port)
|
|
56
|
+
return Promise.resolve(false);
|
|
57
|
+
return probeTcp(u.hostname || "127.0.0.1", Number(u.port), timeoutMs);
|
|
58
|
+
}
|
|
59
|
+
function pickByProtocol(cfg, ctors) {
|
|
60
|
+
const opts = { url: cfg.endpoint, headers: cfg.headers };
|
|
61
|
+
if (cfg.protocol === "http/protobuf")
|
|
62
|
+
return new ctors.proto(opts);
|
|
63
|
+
if (cfg.protocol === "http/json")
|
|
64
|
+
return new ctors.http(opts);
|
|
65
|
+
return new ctors.grpc(opts);
|
|
66
|
+
}
|
|
67
|
+
export function initSdk(cfg, notify, opts = {}) {
|
|
68
|
+
if (!cfg.enabled || !cfg.signals.traces)
|
|
69
|
+
return null;
|
|
70
|
+
if (initOnce)
|
|
71
|
+
return sdk;
|
|
72
|
+
initOnce = true;
|
|
73
|
+
const instanceId = `${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
74
|
+
const resource = new Resource({
|
|
75
|
+
...cfg.resourceAttributes,
|
|
76
|
+
[ATTR_SERVICE_NAME]: cfg.serviceName,
|
|
77
|
+
[ATTR_SERVICE_INSTANCE_ID]: instanceId,
|
|
78
|
+
[ATTR_PI_CWD]: cfg.cwd,
|
|
79
|
+
});
|
|
80
|
+
const traceExporter = pickByProtocol(cfg, {
|
|
81
|
+
grpc: GrpcExporter,
|
|
82
|
+
proto: ProtoExporter,
|
|
83
|
+
http: HttpExporter,
|
|
84
|
+
});
|
|
85
|
+
const spanProcessor = new BatchSpanProcessor(traceExporter);
|
|
86
|
+
const sampler = cfg.sampleRatio < 1.0
|
|
87
|
+
? new ParentBasedSampler({
|
|
88
|
+
root: new TraceIdRatioBasedSampler(cfg.sampleRatio),
|
|
89
|
+
})
|
|
90
|
+
: undefined;
|
|
91
|
+
const sdkOpts = {
|
|
92
|
+
resource,
|
|
93
|
+
spanProcessor,
|
|
94
|
+
...(sampler ? { sampler } : {}),
|
|
95
|
+
};
|
|
96
|
+
if (cfg.signals.metrics) {
|
|
97
|
+
const metricExporter = pickByProtocol(cfg, {
|
|
98
|
+
grpc: MetricGrpcExporter,
|
|
99
|
+
proto: MetricProtoExporter,
|
|
100
|
+
http: MetricHttpExporter,
|
|
101
|
+
});
|
|
102
|
+
sdkOpts.metricReader = new PeriodicExportingMetricReader({
|
|
103
|
+
exporter: metricExporter,
|
|
104
|
+
exportIntervalMillis: 10_000,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (cfg.signals.logs) {
|
|
108
|
+
const logExporter = pickByProtocol(cfg, {
|
|
109
|
+
grpc: LogGrpcExporter,
|
|
110
|
+
proto: LogProtoExporter,
|
|
111
|
+
http: LogHttpExporter,
|
|
112
|
+
});
|
|
113
|
+
sdkOpts.logRecordProcessors = [new BatchLogRecordProcessor(logExporter)];
|
|
114
|
+
}
|
|
115
|
+
sdk = new NodeSDK(sdkOpts);
|
|
116
|
+
try {
|
|
117
|
+
sdk.start();
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const e = err;
|
|
121
|
+
notify?.(`pi-otel: SDK start failed — ${e.message}`, "error");
|
|
122
|
+
sdk = null;
|
|
123
|
+
initOnce = false;
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
// Install bridge AFTER sdk.start() — LoggerProvider must exist first.
|
|
127
|
+
if (cfg.signals.logs) {
|
|
128
|
+
diag.setLogger(buildBridgeDiagLogger(), {
|
|
129
|
+
logLevel: cfg.logLevel,
|
|
130
|
+
suppressOverrideMessage: true,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (!opts.silentSuccess) {
|
|
134
|
+
notify?.(`pi-otel: OTLP wired to ${cfg.endpoint} (${cfg.protocol})`, "info");
|
|
135
|
+
}
|
|
136
|
+
return sdk;
|
|
137
|
+
}
|
|
138
|
+
export async function shutdownSdk() {
|
|
139
|
+
if (!sdk)
|
|
140
|
+
return;
|
|
141
|
+
try {
|
|
142
|
+
await sdk.shutdown();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// swallow — silent-drop policy (SPEC §7)
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
// Reset state first so a subsequent initSdk() can proceed even if the
|
|
149
|
+
// cleanup calls below throw. The global Tracer/Logger/Meter APIs refuse
|
|
150
|
+
// to replace an already-registered provider — without disabling all
|
|
151
|
+
// three, a re-init silently keeps the dead providers.
|
|
152
|
+
sdk = null;
|
|
153
|
+
initOnce = false;
|
|
154
|
+
trace.disable();
|
|
155
|
+
metrics.disable();
|
|
156
|
+
logs.disable();
|
|
157
|
+
diag.disable();
|
|
158
|
+
resetMetricHandles();
|
|
159
|
+
resetLogHandles();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=sdk.js.map
|