@vymalo/opencode-otel 0.12.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 +91 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +224 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/instruments.d.ts +29 -0
- package/dist/instruments.js +102 -0
- package/dist/instruments.js.map +1 -0
- package/dist/lib.d.ts +8 -0
- package/dist/lib.js +8 -0
- package/dist/lib.js.map +1 -0
- package/dist/logging.d.ts +16 -0
- package/dist/logging.js +72 -0
- package/dist/logging.js.map +1 -0
- package/dist/opencode.d.ts +23 -0
- package/dist/opencode.js +141 -0
- package/dist/opencode.js.map +1 -0
- package/dist/propagation.d.ts +22 -0
- package/dist/propagation.js +55 -0
- package/dist/propagation.js.map +1 -0
- package/dist/providers.d.ts +55 -0
- package/dist/providers.js +179 -0
- package/dist/providers.js.map +1 -0
- package/dist/recorder.d.ts +85 -0
- package/dist/recorder.js +546 -0
- package/dist/recorder.js.map +1 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
import { type EnvSource } from "./config.js";
|
|
3
|
+
import { type Logger } from "./logging.js";
|
|
4
|
+
import { type ExporterFactories } from "./providers.js";
|
|
5
|
+
export interface OtelPluginFactoryOptions {
|
|
6
|
+
logger?: Logger;
|
|
7
|
+
/** Injected environment; defaults to `process.env`. */
|
|
8
|
+
env?: EnvSource;
|
|
9
|
+
/** Substitute exporters (tests use in-memory ones). */
|
|
10
|
+
exporters?: ExporterFactories;
|
|
11
|
+
/** Injectable clock; defaults to `Date.now`. */
|
|
12
|
+
now?: () => number;
|
|
13
|
+
/** Skip `beforeExit`/`SIGINT`/`SIGTERM` registration (tests). */
|
|
14
|
+
registerProcessHandlers?: boolean;
|
|
15
|
+
/** Override the resolved host metadata (hostname, version) for tests. */
|
|
16
|
+
hostInfo?: {
|
|
17
|
+
hostname?: string;
|
|
18
|
+
version?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare function createOtelPlugin(factoryOptions?: OtelPluginFactoryOptions): Plugin;
|
|
22
|
+
export declare const OpencodeOtelPlugin: Plugin;
|
|
23
|
+
export default OpencodeOtelPlugin;
|
package/dist/opencode.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { resolveOtelConfig } from "./config.js";
|
|
3
|
+
import { createJsonConsoleLogger, DEFAULT_LOG_LEVEL, fromOpenCodeLogLevel, LOG_LEVEL_PRIORITY } from "./logging.js";
|
|
4
|
+
import { installTracePropagation } from "./propagation.js";
|
|
5
|
+
import { buildResource, createProviders, describeError } from "./providers.js";
|
|
6
|
+
import { TelemetryRecorder } from "./recorder.js";
|
|
7
|
+
const PLUGIN_SERVICE_NAME = "opencode-otel-plugin";
|
|
8
|
+
/**
|
|
9
|
+
* Pipe plugin logs through OpenCode's `client.app.log` so they show up in the
|
|
10
|
+
* host's structured log stream, with the JSON console as a reliable fallback.
|
|
11
|
+
* Mirrors the rest of the suite.
|
|
12
|
+
*
|
|
13
|
+
* Note this is the plugin's *own* diagnostic logging — entirely separate from
|
|
14
|
+
* the OTLP logs signal it exports.
|
|
15
|
+
*/
|
|
16
|
+
function createOpenCodeLogger(client, getMinLevel) {
|
|
17
|
+
const fallback = createJsonConsoleLogger("debug");
|
|
18
|
+
const consoleAll = /^(1|true|yes|on)$/i.test(process.env.VYMALO_PLUGIN_CONSOLE_LOG ?? "");
|
|
19
|
+
const write = (level, event, fields) => {
|
|
20
|
+
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[getMinLevel()]) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (consoleAll || level === "warn" || level === "error") {
|
|
24
|
+
fallback[level](event, fields);
|
|
25
|
+
}
|
|
26
|
+
const hostLevel = level === "trace" ? "debug" : level;
|
|
27
|
+
void client.app
|
|
28
|
+
.log({
|
|
29
|
+
body: { service: PLUGIN_SERVICE_NAME, level: hostLevel, message: event, extra: fields }
|
|
30
|
+
})
|
|
31
|
+
.catch(() => {
|
|
32
|
+
/* best-effort */
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
trace: (event, fields) => write("trace", event, fields),
|
|
37
|
+
debug: (event, fields) => write("debug", event, fields),
|
|
38
|
+
info: (event, fields) => write("info", event, fields),
|
|
39
|
+
warn: (event, fields) => write("warn", event, fields),
|
|
40
|
+
error: (event, fields) => write("error", event, fields)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Drain buffered telemetry on process exit. The plugin API has no dispose hook,
|
|
45
|
+
* so without this a short CLI invocation loses everything still in a batch
|
|
46
|
+
* processor. Handlers are registered once and never keep the loop alive.
|
|
47
|
+
*/
|
|
48
|
+
function registerExitHandlers(providers, logger) {
|
|
49
|
+
let done = false;
|
|
50
|
+
const drain = () => {
|
|
51
|
+
if (done) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
done = true;
|
|
55
|
+
void providers.shutdown().catch((error) => {
|
|
56
|
+
logger.warn("otel_shutdown_failed", { error: describeError(error) });
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
process.once("beforeExit", drain);
|
|
60
|
+
process.once("SIGINT", drain);
|
|
61
|
+
process.once("SIGTERM", drain);
|
|
62
|
+
}
|
|
63
|
+
export function createOtelPlugin(factoryOptions = {}) {
|
|
64
|
+
return async (input, pluginOptions) => {
|
|
65
|
+
let currentLogLevel = DEFAULT_LOG_LEVEL;
|
|
66
|
+
const logger = factoryOptions.logger ?? createOpenCodeLogger(input.client, () => currentLogLevel);
|
|
67
|
+
const config = resolveOtelConfig(pluginOptions, factoryOptions.env ?? process.env);
|
|
68
|
+
if (!config.active) {
|
|
69
|
+
// No endpoint and no explicit exporter means the plugin was installed but
|
|
70
|
+
// never configured — cost nothing rather than half-initializing.
|
|
71
|
+
logger.info("otel_plugin_inactive", {
|
|
72
|
+
enabled: config.enabled,
|
|
73
|
+
reason: config.enabled ? "no_exporter_configured" : "disabled"
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
config: async (hostConfig) => {
|
|
77
|
+
currentLogLevel = fromOpenCodeLogLevel(hostConfig.logLevel) ?? DEFAULT_LOG_LEVEL;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const resource = buildResource(config, {
|
|
82
|
+
version: factoryOptions.hostInfo?.version,
|
|
83
|
+
hostname: factoryOptions.hostInfo?.hostname ?? safeHostname(),
|
|
84
|
+
projectName: input.project?.id,
|
|
85
|
+
directory: input.directory,
|
|
86
|
+
worktree: input.worktree
|
|
87
|
+
});
|
|
88
|
+
const providers = createProviders(config, resource, logger, factoryOptions.exporters);
|
|
89
|
+
const recorder = new TelemetryRecorder({
|
|
90
|
+
providers,
|
|
91
|
+
config,
|
|
92
|
+
logger,
|
|
93
|
+
now: factoryOptions.now
|
|
94
|
+
});
|
|
95
|
+
if (factoryOptions.registerProcessHandlers !== false) {
|
|
96
|
+
registerExitHandlers(providers, logger);
|
|
97
|
+
}
|
|
98
|
+
logger.info("otel_plugin_enabled", {
|
|
99
|
+
serviceName: config.serviceName,
|
|
100
|
+
exporters: config.exporters,
|
|
101
|
+
endpoint: config.endpoint,
|
|
102
|
+
includeSessionId: config.includeSessionId,
|
|
103
|
+
filteredTools: [...config.filteredTools]
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
config: async (hostConfig) => {
|
|
107
|
+
currentLogLevel = fromOpenCodeLogLevel(hostConfig.logLevel) ?? DEFAULT_LOG_LEVEL;
|
|
108
|
+
if (config.propagateTraceContext) {
|
|
109
|
+
const wrapped = installTracePropagation(hostConfig, {
|
|
110
|
+
getContext: () => recorder.currentChatContext(),
|
|
111
|
+
logger
|
|
112
|
+
});
|
|
113
|
+
logger.debug("otel_trace_propagation_ready", { providerCount: wrapped });
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
event: async ({ event }) => {
|
|
117
|
+
recorder.onEvent(event);
|
|
118
|
+
},
|
|
119
|
+
"chat.message": async (chatInput, chatOutput) => {
|
|
120
|
+
recorder.onChatMessage(chatInput, chatOutput);
|
|
121
|
+
},
|
|
122
|
+
"tool.execute.before": async (toolInput) => {
|
|
123
|
+
recorder.onToolBefore(toolInput);
|
|
124
|
+
},
|
|
125
|
+
"tool.execute.after": async (toolInput, toolOutput) => {
|
|
126
|
+
recorder.onToolAfter(toolInput, toolOutput);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function safeHostname() {
|
|
132
|
+
try {
|
|
133
|
+
return hostname() || undefined;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export const OpencodeOtelPlugin = createOtelPlugin();
|
|
140
|
+
export default OpencodeOtelPlugin;
|
|
141
|
+
//# sourceMappingURL=opencode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode.js","sourceRoot":"","sources":["../src/opencode.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Context } from "@opentelemetry/api";
|
|
2
|
+
import type { Logger } from "./logging.js";
|
|
3
|
+
export interface ProviderConfigLike {
|
|
4
|
+
options?: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
export interface PropagationConfigInput {
|
|
7
|
+
provider?: Record<string, ProviderConfigLike | undefined>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Wrap every provider's `options.fetch` so outgoing model requests carry W3C
|
|
11
|
+
* trace context, joining an OpenCode session and its gateway-side spans into
|
|
12
|
+
* one trace.
|
|
13
|
+
*
|
|
14
|
+
* This is the same interception seam `@vymalo/opencode-ratelimit` uses — and it
|
|
15
|
+
* composes the same way: the existing fetch is captured at install time and
|
|
16
|
+
* delegated to, so stacking the two plugins in either order works.
|
|
17
|
+
*/
|
|
18
|
+
export declare function installTracePropagation(input: PropagationConfigInput, deps: {
|
|
19
|
+
getContext: () => Context | undefined;
|
|
20
|
+
logger: Logger;
|
|
21
|
+
fetchImpl?: typeof fetch;
|
|
22
|
+
}): number;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { defaultTextMapSetter } from "@opentelemetry/api";
|
|
2
|
+
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
3
|
+
const propagator = new W3CTraceContextPropagator();
|
|
4
|
+
/**
|
|
5
|
+
* Wrap every provider's `options.fetch` so outgoing model requests carry W3C
|
|
6
|
+
* trace context, joining an OpenCode session and its gateway-side spans into
|
|
7
|
+
* one trace.
|
|
8
|
+
*
|
|
9
|
+
* This is the same interception seam `@vymalo/opencode-ratelimit` uses — and it
|
|
10
|
+
* composes the same way: the existing fetch is captured at install time and
|
|
11
|
+
* delegated to, so stacking the two plugins in either order works.
|
|
12
|
+
*/
|
|
13
|
+
export function installTracePropagation(input, deps) {
|
|
14
|
+
const providers = input.provider;
|
|
15
|
+
if (!providers) {
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
let wrapped = 0;
|
|
19
|
+
for (const [providerId, providerConfig] of Object.entries(providers)) {
|
|
20
|
+
if (!providerConfig) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const options = (providerConfig.options ??= {});
|
|
24
|
+
const delegate = typeof options.fetch === "function"
|
|
25
|
+
? options.fetch
|
|
26
|
+
: (deps.fetchImpl ?? globalThis.fetch);
|
|
27
|
+
if (typeof delegate !== "function") {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
options.fetch = async (input_, init) => {
|
|
31
|
+
const context = deps.getContext();
|
|
32
|
+
if (!context) {
|
|
33
|
+
return delegate(input_, init);
|
|
34
|
+
}
|
|
35
|
+
const carrier = {};
|
|
36
|
+
propagator.inject(context, carrier, defaultTextMapSetter);
|
|
37
|
+
if (Object.keys(carrier).length === 0) {
|
|
38
|
+
return delegate(input_, init);
|
|
39
|
+
}
|
|
40
|
+
const headers = new Headers(init?.headers ?? {});
|
|
41
|
+
// Never clobber an upstream traceparent — if something already set one,
|
|
42
|
+
// it knows more about the request than we do.
|
|
43
|
+
for (const [key, value] of Object.entries(carrier)) {
|
|
44
|
+
if (!headers.has(key)) {
|
|
45
|
+
headers.set(key, value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return delegate(input_, { ...init, headers });
|
|
49
|
+
};
|
|
50
|
+
wrapped += 1;
|
|
51
|
+
deps.logger.trace("otel_trace_propagation_installed", { providerId });
|
|
52
|
+
}
|
|
53
|
+
return wrapped;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=propagation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"propagation.js","sourceRoot":"","sources":["../src/propagation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAYhE,MAAM,UAAU,GAAG,IAAI,yBAAyB,EAAE,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CACrC,KAA6B,EAC7B,IAAyF;IAEzF,MAAM,SAAS,GAAG,KAAK,CAAC,QAAQ,CAAC;IACjC,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,CAAC,UAAU,EAAE,cAAc,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACrE,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,CAAC,cAAc,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC;QAChD,MAAM,QAAQ,GACZ,OAAO,OAAO,CAAC,KAAK,KAAK,UAAU;YACjC,CAAC,CAAE,OAAO,CAAC,KAAsB;YACjC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE,CAAC;YACnC,SAAS;QACX,CAAC;QAED,OAAO,CAAC,KAAK,GAAG,KAAK,EACnB,MAAmC,EACnC,IAAkC,EACf,EAAE;YACrB,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YAClC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,OAAO,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YAChC,CAAC;YACD,MAAM,OAAO,GAA2B,EAAE,CAAC;YAC3C,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,oBAAoB,CAAC,CAAC;YAC1D,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACtC,OAAO,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YAChC,CAAC;YACD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC;YACjD,wEAAwE;YACxE,8CAA8C;YAC9C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;gBACnD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;oBACtB,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;gBAC1B,CAAC;YACH,CAAC;YACD,OAAO,QAAQ,CAAC,MAAM,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAChD,CAAC,CAAC;QACF,OAAO,IAAI,CAAC,CAAC;QACb,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,kCAAkC,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Logger as OtelLogger } from "@opentelemetry/api-logs";
|
|
2
|
+
import { type Resource } from "@opentelemetry/resources";
|
|
3
|
+
import { type LogRecordExporter } from "@opentelemetry/sdk-logs";
|
|
4
|
+
import { type PushMetricExporter } from "@opentelemetry/sdk-metrics";
|
|
5
|
+
import { type SpanExporter } from "@opentelemetry/sdk-trace";
|
|
6
|
+
import type { Tracer } from "@opentelemetry/api";
|
|
7
|
+
import type { Meter } from "@opentelemetry/api";
|
|
8
|
+
import type { Logger } from "./logging.js";
|
|
9
|
+
import type { ResolvedOtelConfig } from "./types.js";
|
|
10
|
+
/**
|
|
11
|
+
* The three provider handles the recorder needs, plus lifecycle. `undefined`
|
|
12
|
+
* for a signal whose exporter is `none` — the recorder no-ops on it rather
|
|
13
|
+
* than branching on config everywhere.
|
|
14
|
+
*/
|
|
15
|
+
export interface TelemetryProviders {
|
|
16
|
+
tracer?: Tracer;
|
|
17
|
+
meter?: Meter;
|
|
18
|
+
otelLogger?: OtelLogger;
|
|
19
|
+
/** Push everything buffered. Called on `session.idle` and on process exit. */
|
|
20
|
+
forceFlush(): Promise<void>;
|
|
21
|
+
/** Flush and tear down. */
|
|
22
|
+
shutdown(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Exporter factories, injectable so tests can substitute in-memory exporters
|
|
26
|
+
* without reaching the network or constructing real OTLP clients.
|
|
27
|
+
*/
|
|
28
|
+
export interface ExporterFactories {
|
|
29
|
+
trace?: (config: ResolvedOtelConfig) => SpanExporter | undefined;
|
|
30
|
+
metric?: (config: ResolvedOtelConfig) => PushMetricExporter | undefined;
|
|
31
|
+
log?: (config: ResolvedOtelConfig) => LogRecordExporter | undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the resource every signal is stamped with.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately identifies the **machine and the project**, never the developer:
|
|
37
|
+
* no git author email, no account id. An operator who wants per-person
|
|
38
|
+
* attribution adds it explicitly via `resourceAttributes` /
|
|
39
|
+
* `OTEL_RESOURCE_ATTRIBUTES`, which keeps that choice visible in config.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildResource(config: ResolvedOtelConfig, context: {
|
|
42
|
+
version?: string;
|
|
43
|
+
hostname?: string;
|
|
44
|
+
projectName?: string;
|
|
45
|
+
directory?: string;
|
|
46
|
+
worktree?: string;
|
|
47
|
+
branch?: string;
|
|
48
|
+
}): Resource;
|
|
49
|
+
/**
|
|
50
|
+
* Construct the enabled providers. Each signal is independent: a failure to
|
|
51
|
+
* build one leaves the others running, because partial telemetry is strictly
|
|
52
|
+
* better than an exception escaping into the host's plugin loader.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createProviders(config: ResolvedOtelConfig, resource: Resource, logger: Logger, factories?: ExporterFactories): TelemetryProviders;
|
|
55
|
+
export declare function describeError(error: unknown): string;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
|
|
2
|
+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
|
3
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
4
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
5
|
+
import { BatchLogRecordProcessor, ConsoleLogRecordExporter, LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
6
|
+
import { AggregationTemporality, ConsoleMetricExporter, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
7
|
+
import { BatchSpanProcessor, ConsoleSpanExporter, TracerProvider } from "@opentelemetry/sdk-trace";
|
|
8
|
+
import { signalUrl } from "./config.js";
|
|
9
|
+
const INSTRUMENTATION_SCOPE = "@vymalo/opencode-otel";
|
|
10
|
+
function otlpArgs(config, signal) {
|
|
11
|
+
const url = signalUrl(config, signal);
|
|
12
|
+
return url ? { url, headers: config.headers } : undefined;
|
|
13
|
+
}
|
|
14
|
+
const defaultFactories = {
|
|
15
|
+
trace: (config) => {
|
|
16
|
+
if (config.exporters.traces === "console") {
|
|
17
|
+
return new ConsoleSpanExporter();
|
|
18
|
+
}
|
|
19
|
+
const args = otlpArgs(config, "traces");
|
|
20
|
+
return args ? new OTLPTraceExporter(args) : undefined;
|
|
21
|
+
},
|
|
22
|
+
metric: (config) => {
|
|
23
|
+
if (config.exporters.metrics === "console") {
|
|
24
|
+
return new ConsoleMetricExporter();
|
|
25
|
+
}
|
|
26
|
+
const args = otlpArgs(config, "metrics");
|
|
27
|
+
return args
|
|
28
|
+
? new OTLPMetricExporter({
|
|
29
|
+
...args,
|
|
30
|
+
temporalityPreference: config.metricTemporality === "cumulative"
|
|
31
|
+
? AggregationTemporality.CUMULATIVE
|
|
32
|
+
: AggregationTemporality.DELTA
|
|
33
|
+
})
|
|
34
|
+
: undefined;
|
|
35
|
+
},
|
|
36
|
+
log: (config) => {
|
|
37
|
+
if (config.exporters.logs === "console") {
|
|
38
|
+
return new ConsoleLogRecordExporter();
|
|
39
|
+
}
|
|
40
|
+
const args = otlpArgs(config, "logs");
|
|
41
|
+
return args ? new OTLPLogExporter(args) : undefined;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Build the resource every signal is stamped with.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately identifies the **machine and the project**, never the developer:
|
|
48
|
+
* no git author email, no account id. An operator who wants per-person
|
|
49
|
+
* attribution adds it explicitly via `resourceAttributes` /
|
|
50
|
+
* `OTEL_RESOURCE_ATTRIBUTES`, which keeps that choice visible in config.
|
|
51
|
+
*/
|
|
52
|
+
export function buildResource(config, context) {
|
|
53
|
+
const attributes = {
|
|
54
|
+
"service.name": config.serviceName,
|
|
55
|
+
"telemetry.sdk.language": "nodejs"
|
|
56
|
+
};
|
|
57
|
+
if (context.version) {
|
|
58
|
+
attributes["service.version"] = context.version;
|
|
59
|
+
}
|
|
60
|
+
if (config.environment) {
|
|
61
|
+
attributes["deployment.environment.name"] = config.environment;
|
|
62
|
+
}
|
|
63
|
+
if (context.hostname) {
|
|
64
|
+
attributes["host.name"] = context.hostname;
|
|
65
|
+
}
|
|
66
|
+
if (context.projectName) {
|
|
67
|
+
attributes["opencode.project.name"] = context.projectName;
|
|
68
|
+
}
|
|
69
|
+
if (context.directory) {
|
|
70
|
+
attributes["opencode.directory"] = context.directory;
|
|
71
|
+
}
|
|
72
|
+
if (context.worktree) {
|
|
73
|
+
attributes["opencode.worktree"] = context.worktree;
|
|
74
|
+
}
|
|
75
|
+
if (context.branch) {
|
|
76
|
+
attributes["vcs.repository.ref.name"] = context.branch;
|
|
77
|
+
}
|
|
78
|
+
// Operator-supplied attributes win — they are the escape hatch, and silently
|
|
79
|
+
// ignoring them would make the escape hatch useless.
|
|
80
|
+
return resourceFromAttributes({ ...attributes, ...config.resourceAttributes });
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Construct the enabled providers. Each signal is independent: a failure to
|
|
84
|
+
* build one leaves the others running, because partial telemetry is strictly
|
|
85
|
+
* better than an exception escaping into the host's plugin loader.
|
|
86
|
+
*/
|
|
87
|
+
export function createProviders(config, resource, logger, factories = {}) {
|
|
88
|
+
const make = { ...defaultFactories, ...factories };
|
|
89
|
+
const flushers = [];
|
|
90
|
+
const shutdowns = [];
|
|
91
|
+
let tracer;
|
|
92
|
+
let meter;
|
|
93
|
+
let otelLogger;
|
|
94
|
+
if (config.exporters.traces !== "none") {
|
|
95
|
+
try {
|
|
96
|
+
const exporter = make.trace(config);
|
|
97
|
+
if (exporter) {
|
|
98
|
+
const provider = new TracerProvider({
|
|
99
|
+
resource,
|
|
100
|
+
spanProcessors: [
|
|
101
|
+
new BatchSpanProcessor({
|
|
102
|
+
exporter,
|
|
103
|
+
scheduledDelayMillis: config.traceExportIntervalMs
|
|
104
|
+
})
|
|
105
|
+
]
|
|
106
|
+
});
|
|
107
|
+
tracer = provider.getTracer(INSTRUMENTATION_SCOPE);
|
|
108
|
+
flushers.push(() => provider.forceFlush());
|
|
109
|
+
shutdowns.push(() => provider.shutdown());
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
logger.warn("otel_traces_init_failed", { error: describeError(error) });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (config.exporters.metrics !== "none") {
|
|
117
|
+
try {
|
|
118
|
+
const exporter = make.metric(config);
|
|
119
|
+
if (exporter) {
|
|
120
|
+
const provider = new MeterProvider({
|
|
121
|
+
resource,
|
|
122
|
+
readers: [
|
|
123
|
+
new PeriodicExportingMetricReader({
|
|
124
|
+
exporter,
|
|
125
|
+
exportIntervalMillis: config.metricExportIntervalMs
|
|
126
|
+
})
|
|
127
|
+
]
|
|
128
|
+
});
|
|
129
|
+
meter = provider.getMeter(INSTRUMENTATION_SCOPE);
|
|
130
|
+
flushers.push(() => provider.forceFlush());
|
|
131
|
+
shutdowns.push(() => provider.shutdown());
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
logger.warn("otel_metrics_init_failed", { error: describeError(error) });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (config.exporters.logs !== "none") {
|
|
139
|
+
try {
|
|
140
|
+
const exporter = make.log(config);
|
|
141
|
+
if (exporter) {
|
|
142
|
+
const provider = new LoggerProvider({
|
|
143
|
+
resource,
|
|
144
|
+
processors: [
|
|
145
|
+
new BatchLogRecordProcessor({
|
|
146
|
+
exporter,
|
|
147
|
+
scheduledDelayMillis: config.logExportIntervalMs
|
|
148
|
+
})
|
|
149
|
+
]
|
|
150
|
+
});
|
|
151
|
+
otelLogger = provider.getLogger(INSTRUMENTATION_SCOPE);
|
|
152
|
+
flushers.push(() => provider.forceFlush());
|
|
153
|
+
shutdowns.push(() => provider.shutdown());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
logger.warn("otel_logs_init_failed", { error: describeError(error) });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const runAll = async (tasks) => {
|
|
161
|
+
// `allSettled`, not `all`: one unreachable collector must not stop the
|
|
162
|
+
// other signals from draining.
|
|
163
|
+
await Promise.allSettled(tasks.map((task) => task()));
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
tracer,
|
|
167
|
+
meter,
|
|
168
|
+
otelLogger,
|
|
169
|
+
forceFlush: () => runAll(flushers),
|
|
170
|
+
shutdown: () => runAll(shutdowns)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export function describeError(error) {
|
|
174
|
+
if (error instanceof Error) {
|
|
175
|
+
return error.message;
|
|
176
|
+
}
|
|
177
|
+
return String(error);
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=providers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.js","sourceRoot":"","sources":["../src/providers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type Context } from "@opentelemetry/api";
|
|
2
|
+
import type { Hooks } from "@opencode-ai/plugin";
|
|
3
|
+
import type { Logger } from "./logging.js";
|
|
4
|
+
import type { TelemetryProviders } from "./providers.js";
|
|
5
|
+
import type { ResolvedOtelConfig } from "./types.js";
|
|
6
|
+
type OpencodeEvent = Parameters<NonNullable<Hooks["event"]>>[0]["event"];
|
|
7
|
+
type ChatMessageInput = Parameters<NonNullable<Hooks["chat.message"]>>[0];
|
|
8
|
+
type ChatMessageOutput = Parameters<NonNullable<Hooks["chat.message"]>>[1];
|
|
9
|
+
type ToolBeforeInput = Parameters<NonNullable<Hooks["tool.execute.before"]>>[0];
|
|
10
|
+
type ToolAfterInput = Parameters<NonNullable<Hooks["tool.execute.after"]>>[0];
|
|
11
|
+
export interface RecorderDeps {
|
|
12
|
+
providers: TelemetryProviders;
|
|
13
|
+
config: ResolvedOtelConfig;
|
|
14
|
+
logger: Logger;
|
|
15
|
+
/** Injectable clock; defaults to `Date.now`. */
|
|
16
|
+
now?: () => number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Translates the OpenCode event stream and hook callbacks into OTel signals.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately holds no content: lengths, counts, durations and outcomes only.
|
|
22
|
+
* See `plans/otel.md` → "No content capture in v1".
|
|
23
|
+
*/
|
|
24
|
+
export declare class TelemetryRecorder {
|
|
25
|
+
private readonly deps;
|
|
26
|
+
private readonly instruments?;
|
|
27
|
+
private readonly now;
|
|
28
|
+
private readonly sessions;
|
|
29
|
+
private readonly chats;
|
|
30
|
+
private readonly tools;
|
|
31
|
+
/** Terminal tool outcomes already recorded, so the hook and the part update cannot double-count. */
|
|
32
|
+
private readonly finishedTools;
|
|
33
|
+
/** Assistant messages already finalized — `message.updated` fires repeatedly with cumulative totals. */
|
|
34
|
+
private readonly finalizedMessages;
|
|
35
|
+
/** Pending permission prompts, so `permission.replied` can name the tool it resolved. */
|
|
36
|
+
private readonly permissions;
|
|
37
|
+
/**
|
|
38
|
+
* Last-seen cumulative diff per `sessionID\0file`. `session.diff` reports the
|
|
39
|
+
* session's whole diff each time, so only the delta may be counted.
|
|
40
|
+
*/
|
|
41
|
+
private readonly diffs;
|
|
42
|
+
constructor(deps: RecorderDeps);
|
|
43
|
+
/**
|
|
44
|
+
* Session id as a *metric* attribute — omitted unless `includeSessionId`,
|
|
45
|
+
* because it is unbounded cardinality and metric backends bill per series.
|
|
46
|
+
* Logs and spans always carry it.
|
|
47
|
+
*/
|
|
48
|
+
private metricSession;
|
|
49
|
+
private emit;
|
|
50
|
+
private session;
|
|
51
|
+
onEvent(event: OpencodeEvent): void;
|
|
52
|
+
private dispatch;
|
|
53
|
+
private onSessionCreated;
|
|
54
|
+
private onSessionStatus;
|
|
55
|
+
private settleActiveTime;
|
|
56
|
+
private onSessionIdle;
|
|
57
|
+
private onCompacted;
|
|
58
|
+
private onSessionError;
|
|
59
|
+
private onSessionDiff;
|
|
60
|
+
private onMessageUpdated;
|
|
61
|
+
private finalizeMessage;
|
|
62
|
+
private onPartUpdated;
|
|
63
|
+
private onPermissionReplied;
|
|
64
|
+
private onCommandExecuted;
|
|
65
|
+
onChatMessage(input: ChatMessageInput, output: ChatMessageOutput): void;
|
|
66
|
+
onToolBefore(input: ToolBeforeInput): void;
|
|
67
|
+
onToolAfter(input: ToolAfterInput, output: {
|
|
68
|
+
output?: string;
|
|
69
|
+
}): void;
|
|
70
|
+
/**
|
|
71
|
+
* Record a tool's terminal outcome exactly once. Both `tool.execute.after`
|
|
72
|
+
* and the tool part reaching a terminal state report it, and which arrives
|
|
73
|
+
* depends on whether the tool succeeded — first writer wins, so a failing
|
|
74
|
+
* tool (no `after` hook) is still counted.
|
|
75
|
+
*/
|
|
76
|
+
private finishTool;
|
|
77
|
+
/**
|
|
78
|
+
* The context of the single in-flight chat, or `undefined` when zero or more
|
|
79
|
+
* than one is running. Ambiguity yields no trace context rather than a wrong
|
|
80
|
+
* parent — a missing link is recoverable, a fabricated one is not.
|
|
81
|
+
*/
|
|
82
|
+
currentChatContext(): Context | undefined;
|
|
83
|
+
shutdown(): Promise<void>;
|
|
84
|
+
}
|
|
85
|
+
export {};
|