506d0942cf
Extended the OTLP export bootstrap beyond traces so omp emits the full OpenTelemetry signal set from a single session. - Registered a LoggerProvider + BatchLogRecordProcessor and a MeterProvider + PeriodicExportingMetricReader when their OTLP endpoints (or the shared endpoint) are set, each gated independently by OTEL_*_EXPORTER=none, OTEL_SDK_DISABLED, and http/protobuf protocol checks. - Bridged the centralized logger through a new registerLogSink API so every log event also becomes an OTLP log record with severity, attributes, and active span context for log-trace correlation (min level via OTEL_LOG_LEVEL). - Recorded gen_ai.client.token.usage and pi.omp.agent.* metrics from the agent run summary and per-chat usage hooks, and emitted a structured run summary log event. - Added an out-of-process logs+metrics probe and gating tests covering the per-signal kill switches. Fixes #4604
128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
/**
|
|
* Positive-path probe for the OTLP log + metric exporters, run as a subprocess
|
|
* by telemetry-export.test.ts. Keeping it out-of-process means the global
|
|
* LoggerProvider / MeterProvider singletons that initTelemetryExport() registers
|
|
* never leak into the test runner.
|
|
*
|
|
* Stands up a loopback OTLP/proto receiver, points the standard env vars at it,
|
|
* registers the providers, drives a log record through the bridged
|
|
* `@oh-my-pi/pi-utils` logger and metric instruments through the agent
|
|
* telemetry hooks, flushes, and exits 0 only if the receiver got a non-empty
|
|
* protobuf POST at both /v1/logs and /v1/metrics.
|
|
*/
|
|
|
|
import type { AgentRunCoverage, AgentRunSummary, ChatUsageEvent } from "@oh-my-pi/pi-agent-core";
|
|
import { emptyAgentRunCoverage, emptyAgentRunSummary } from "@oh-my-pi/pi-agent-core";
|
|
import {
|
|
createTelemetryExportConfig,
|
|
flushTelemetryExport,
|
|
initTelemetryExport,
|
|
isTelemetryExportEnabled,
|
|
} from "@oh-my-pi/pi-coding-agent/telemetry-export";
|
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
|
|
const seen = new Set<string>();
|
|
|
|
const server = Bun.serve({
|
|
port: 0,
|
|
async fetch(req) {
|
|
const path = new URL(req.url).pathname;
|
|
if (req.method === "POST" && req.headers.get("content-type") === "application/x-protobuf") {
|
|
const body = await req.arrayBuffer();
|
|
if (body.byteLength > 0) {
|
|
if (path.endsWith("/v1/logs")) seen.add("logs");
|
|
if (path.endsWith("/v1/metrics")) seen.add("metrics");
|
|
}
|
|
}
|
|
return new Response('{"partialSuccess":{}}', {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
},
|
|
});
|
|
|
|
const base = `http://localhost:${server.port}`;
|
|
process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = `${base}/v1/logs`;
|
|
process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = `${base}/v1/metrics`;
|
|
process.env.OTEL_SERVICE_NAME = "oh-my-pi-signals-probe";
|
|
// Force a short metric export interval so the periodic reader flushes fast.
|
|
process.env.OTEL_METRIC_EXPORT_INTERVAL = "500";
|
|
|
|
await initTelemetryExport();
|
|
if (!isTelemetryExportEnabled()) {
|
|
console.error("PROBE: providers did not register");
|
|
await server.stop(true);
|
|
process.exit(2);
|
|
}
|
|
|
|
const config = createTelemetryExportConfig(undefined);
|
|
if (!config) {
|
|
console.error("PROBE: export config not produced");
|
|
await server.stop(true);
|
|
process.exit(2);
|
|
}
|
|
|
|
// Bridged utility logger -> OTel log record.
|
|
logger.error("probe error", { code: "probe" });
|
|
|
|
// Metric instruments via the agent telemetry hooks.
|
|
const usage: ChatUsageEvent = {
|
|
span: undefined as never,
|
|
agent: { id: "main", name: "Main" },
|
|
conversationId: "probe-session",
|
|
stepNumber: 0,
|
|
model: "claude-haiku-4-5",
|
|
provider: "anthropic",
|
|
serviceTier: undefined,
|
|
usage: {
|
|
inputTokens: 1000,
|
|
outputTokens: 200,
|
|
totalTokens: 1200,
|
|
cachedInputTokens: 0,
|
|
cacheWriteTokens: 0,
|
|
reasoningOutputTokens: 0,
|
|
},
|
|
cost: { usd: 0.01 },
|
|
attributes: undefined,
|
|
headers: undefined,
|
|
};
|
|
config.onChatUsage?.(usage);
|
|
|
|
const summary: AgentRunSummary = {
|
|
...emptyAgentRunSummary(),
|
|
chats: { total: 1, byStopReason: { end_turn: 1 }, totalLatencyMs: 1500 },
|
|
tools: {
|
|
total: 1,
|
|
ok: 1,
|
|
error: 0,
|
|
skipped: 0,
|
|
blocked: 0,
|
|
timeout: 0,
|
|
aborted: 0,
|
|
totalLatencyMs: 42,
|
|
byName: {
|
|
read: { total: 1, ok: 1, error: 0, skipped: 0, blocked: 0, timeout: 0, aborted: 0, totalLatencyMs: 42 },
|
|
},
|
|
},
|
|
stepCount: 1,
|
|
};
|
|
const coverage: AgentRunCoverage = {
|
|
...emptyAgentRunCoverage(),
|
|
toolsAvailable: ["read", "write"],
|
|
toolsInvoked: ["read"],
|
|
toolsUnused: ["write"],
|
|
modelsUsed: ["claude-haiku-4-5"],
|
|
providersUsed: ["anthropic"],
|
|
};
|
|
config.onRunEnd?.(summary, coverage);
|
|
|
|
await flushTelemetryExport();
|
|
// The metric reader exports on its own interval; wait one cycle then flush.
|
|
await Bun.sleep(700);
|
|
await flushTelemetryExport();
|
|
await server.stop(true);
|
|
|
|
const ok = seen.has("logs") && seen.has("metrics");
|
|
console.log(ok ? "PROBE: RECEIVED" : `PROBE: MISSING ${["logs", "metrics"].filter(s => !seen.has(s)).join(",")}`);
|
|
process.exit(ok ? 0 : 1);
|