@vymalo/opencode-core-otel 0.14.1
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 +40 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.js +237 -0
- package/dist/config.js.map +1 -0
- package/dist/deferred.d.ts +22 -0
- package/dist/deferred.js +29 -0
- package/dist/deferred.js.map +1 -0
- package/dist/export-logging.d.ts +28 -0
- package/dist/export-logging.js +74 -0
- package/dist/export-logging.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/instruments.d.ts +29 -0
- package/dist/instruments.js +103 -0
- package/dist/instruments.js.map +1 -0
- package/dist/lib.d.ts +11 -0
- package/dist/lib.js +12 -0
- package/dist/lib.js.map +1 -0
- package/dist/logging.d.ts +16 -0
- package/dist/logging.js +69 -0
- package/dist/logging.js.map +1 -0
- package/dist/propagation.d.ts +22 -0
- package/dist/propagation.js +57 -0
- package/dist/propagation.js.map +1 -0
- package/dist/providers.d.ts +66 -0
- package/dist/providers.js +242 -0
- package/dist/providers.js.map +1 -0
- package/dist/recorder.d.ts +166 -0
- package/dist/recorder.js +795 -0
- package/dist/recorder.js.map +1 -0
- package/dist/token-source.d.ts +59 -0
- package/dist/token-source.js +125 -0
- package/dist/token-source.js.map +1 -0
- package/dist/types.d.ts +104 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/vcs.d.ts +63 -0
- package/dist/vcs.js +188 -0
- package/dist/vcs.js.map +1 -0
- package/package.json +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vymalo contributors
|
|
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,40 @@
|
|
|
1
|
+
# @vymalo/opencode-core-otel
|
|
2
|
+
|
|
3
|
+
Shared OpenTelemetry engine for the @vymalo OpenCode plugin suite. Holds the
|
|
4
|
+
exporter wiring, resource building, the telemetry recorder, and the
|
|
5
|
+
token-source seam in **one place** so [`@vymalo/opencode-otel`](../opencode-otel)
|
|
6
|
+
(and any future OTEL-aware plugin) can share it instead of forking it.
|
|
7
|
+
|
|
8
|
+
> Extracted from `@vymalo/opencode-otel`.
|
|
9
|
+
|
|
10
|
+
## What it provides
|
|
11
|
+
|
|
12
|
+
- `config.ts` — `resolveOtelConfig` and the env/command parsing it depends on.
|
|
13
|
+
- `providers.ts` — `createProviders` / `buildResource`, the OTLP exporter and
|
|
14
|
+
SDK provider wiring for traces, metrics and logs.
|
|
15
|
+
- `recorder.ts` — `TelemetryRecorder`, the OpenCode-hook-to-OTEL-signal mapper.
|
|
16
|
+
- `instruments.ts` — the counters/histograms the recorder writes through.
|
|
17
|
+
- `token-source.ts` — bearer-token injection for authenticated OTLP exporters.
|
|
18
|
+
- `export-logging.ts` — failure-logging wrapper around exporters.
|
|
19
|
+
- `propagation.ts` — W3C trace-context propagation into provider `fetch`.
|
|
20
|
+
- `vcs.ts` — git remote/branch detection for resource attributes.
|
|
21
|
+
- `deferred.ts` — bounded-wait resource attributes for late-arriving events.
|
|
22
|
+
- `logging.ts` — structured logger with level filtering.
|
|
23
|
+
- `types.ts` — the shared config/option types.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { resolveOtelConfig, createProviders, TelemetryRecorder } from "@vymalo/opencode-core-otel";
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For the full surface import from `@vymalo/opencode-core-otel/lib`.
|
|
32
|
+
|
|
33
|
+
## Development
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpm --filter @vymalo/opencode-core-otel build
|
|
37
|
+
pnpm --filter @vymalo/opencode-core-otel typecheck
|
|
38
|
+
pnpm --filter @vymalo/opencode-core-otel test
|
|
39
|
+
pnpm --filter @vymalo/opencode-core-otel coverage
|
|
40
|
+
```
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ResolvedOtelConfig, SignalName } from "./types.js";
|
|
2
|
+
export declare const SIGNALS: readonly SignalName[];
|
|
3
|
+
/** Env source, injectable so tests never touch `process.env`. */
|
|
4
|
+
export type EnvSource = Record<string, string | undefined>;
|
|
5
|
+
/**
|
|
6
|
+
* Parse the OTel `key=value,key2=value2` list format used by both
|
|
7
|
+
* `OTEL_EXPORTER_OTLP_HEADERS` and `OTEL_RESOURCE_ATTRIBUTES`. Values are
|
|
8
|
+
* percent-decoded per spec; an undecodable value is kept verbatim rather than
|
|
9
|
+
* dropping the whole pair.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseKeyValueList(raw: string | undefined): Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* Normalise a command to argv. An array is taken verbatim — that is the form to
|
|
14
|
+
* use when a path contains spaces. A string is split on whitespace, with **no
|
|
15
|
+
* shell**: no quoting, no substitution, no injection surface.
|
|
16
|
+
*
|
|
17
|
+
* The split is decided by the type of the value actually supplied, not by the
|
|
18
|
+
* type of some other candidate. Keying it on the wrong source silently produced
|
|
19
|
+
* a single argv element like `"governance-auth token"` whenever an environment
|
|
20
|
+
* override sat on top of an array in config — which `execFile` then failed to
|
|
21
|
+
* spawn, so every export went out unauthenticated.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseCommand(raw: unknown): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Resolve plugin options against the environment.
|
|
26
|
+
*
|
|
27
|
+
* **Precedence: environment overrides plugin options.** Options are the base
|
|
28
|
+
* layer so a `.well-known/opencode` document can ship a working default for a
|
|
29
|
+
* whole organization; `OTEL_*` variables override per machine so a developer
|
|
30
|
+
* can point at a local collector without editing served config. See
|
|
31
|
+
* `plans/otel.md`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveOtelConfig(raw: unknown, env?: EnvSource): ResolvedOtelConfig;
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the OTLP URL for one signal. A per-signal endpoint is used verbatim
|
|
36
|
+
* (spec: it already includes the path); the base endpoint gets the signal path
|
|
37
|
+
* appended.
|
|
38
|
+
*/
|
|
39
|
+
export declare function signalUrl(config: ResolvedOtelConfig, signal: SignalName): string | undefined;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { DEFAULT_REFRESH_MS } from "./token-source.js";
|
|
2
|
+
export const SIGNALS = [
|
|
3
|
+
"traces",
|
|
4
|
+
"metrics",
|
|
5
|
+
"logs"
|
|
6
|
+
];
|
|
7
|
+
const DEFAULTS = {
|
|
8
|
+
serviceName: "opencode",
|
|
9
|
+
metricExportIntervalMs: 6e4,
|
|
10
|
+
logExportIntervalMs: 5e3,
|
|
11
|
+
traceExportIntervalMs: 5e3,
|
|
12
|
+
metricTemporality: "delta"
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The `OTEL_*` variable names read per signal. The first entry of each tuple is
|
|
16
|
+
* the OTel-spec name; the second (where present) is the Claude-Code-compatible
|
|
17
|
+
* alias, accepted so an operator can move a working env block across tools.
|
|
18
|
+
*/
|
|
19
|
+
const SIGNAL_ENV = {
|
|
20
|
+
traces: {
|
|
21
|
+
exporter: "OTEL_TRACES_EXPORTER",
|
|
22
|
+
endpoint: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
|
|
23
|
+
interval: ["OTEL_BSP_SCHEDULE_DELAY", "OTEL_TRACES_EXPORT_INTERVAL"]
|
|
24
|
+
},
|
|
25
|
+
metrics: {
|
|
26
|
+
exporter: "OTEL_METRICS_EXPORTER",
|
|
27
|
+
endpoint: "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
|
|
28
|
+
interval: ["OTEL_METRIC_EXPORT_INTERVAL"]
|
|
29
|
+
},
|
|
30
|
+
logs: {
|
|
31
|
+
exporter: "OTEL_LOGS_EXPORTER",
|
|
32
|
+
endpoint: "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
|
|
33
|
+
interval: ["OTEL_BLRP_SCHEDULE_DELAY", "OTEL_LOGS_EXPORT_INTERVAL"]
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function parseBool(value) {
|
|
37
|
+
if (value === undefined) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
if (/^(1|true|yes|on)$/i.test(value)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (/^(0|false|no|off)$/i.test(value)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
function parsePositiveInt(value) {
|
|
49
|
+
if (value === undefined) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const n = Number.parseInt(value.trim(), 10);
|
|
53
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse the OTel `key=value,key2=value2` list format used by both
|
|
57
|
+
* `OTEL_EXPORTER_OTLP_HEADERS` and `OTEL_RESOURCE_ATTRIBUTES`. Values are
|
|
58
|
+
* percent-decoded per spec; an undecodable value is kept verbatim rather than
|
|
59
|
+
* dropping the whole pair.
|
|
60
|
+
*/
|
|
61
|
+
export function parseKeyValueList(raw) {
|
|
62
|
+
if (!raw) {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const pair of raw.split(",")) {
|
|
67
|
+
const eq = pair.indexOf("=");
|
|
68
|
+
if (eq <= 0) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const key = pair.slice(0, eq).trim();
|
|
72
|
+
const rawValue = pair.slice(eq + 1).trim();
|
|
73
|
+
if (!key) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
out[key] = decodeURIComponent(rawValue);
|
|
78
|
+
} catch {
|
|
79
|
+
out[key] = rawValue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function parseExporter(value) {
|
|
85
|
+
if (value === undefined) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
// The spec allows a comma-separated list; we honour the first recognised
|
|
89
|
+
// entry rather than fanning out to multiple exporters.
|
|
90
|
+
for (const candidate of value.split(",").map((v) => v.trim().toLowerCase())) {
|
|
91
|
+
if (candidate === "otlp" || candidate === "console" || candidate === "none") {
|
|
92
|
+
return candidate;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
function firstDefined(env, keys) {
|
|
98
|
+
for (const key of keys) {
|
|
99
|
+
const value = env[key];
|
|
100
|
+
if (value !== undefined && value !== "") {
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
function stringRecord(raw) {
|
|
107
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
const out = {};
|
|
111
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
112
|
+
if (typeof value === "string") {
|
|
113
|
+
out[key] = value;
|
|
114
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
115
|
+
out[key] = String(value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
function stringList(raw) {
|
|
121
|
+
if (Array.isArray(raw)) {
|
|
122
|
+
return raw.filter((v) => typeof v === "string" && v.trim() !== "");
|
|
123
|
+
}
|
|
124
|
+
if (typeof raw === "string") {
|
|
125
|
+
return raw.split(",").map((v) => v.trim()).filter((v) => v !== "");
|
|
126
|
+
}
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Normalise a command to argv. An array is taken verbatim — that is the form to
|
|
131
|
+
* use when a path contains spaces. A string is split on whitespace, with **no
|
|
132
|
+
* shell**: no quoting, no substitution, no injection surface.
|
|
133
|
+
*
|
|
134
|
+
* The split is decided by the type of the value actually supplied, not by the
|
|
135
|
+
* type of some other candidate. Keying it on the wrong source silently produced
|
|
136
|
+
* a single argv element like `"governance-auth token"` whenever an environment
|
|
137
|
+
* override sat on top of an array in config — which `execFile` then failed to
|
|
138
|
+
* spawn, so every export went out unauthenticated.
|
|
139
|
+
*/
|
|
140
|
+
export function parseCommand(raw) {
|
|
141
|
+
if (Array.isArray(raw)) {
|
|
142
|
+
return raw.filter((part) => typeof part === "string" && part.trim() !== "");
|
|
143
|
+
}
|
|
144
|
+
if (typeof raw === "string") {
|
|
145
|
+
return raw.split(/\s+/).filter((part) => part !== "");
|
|
146
|
+
}
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Resolve plugin options against the environment.
|
|
151
|
+
*
|
|
152
|
+
* **Precedence: environment overrides plugin options.** Options are the base
|
|
153
|
+
* layer so a `.well-known/opencode` document can ship a working default for a
|
|
154
|
+
* whole organization; `OTEL_*` variables override per machine so a developer
|
|
155
|
+
* can point at a local collector without editing served config. See
|
|
156
|
+
* `plans/otel.md`.
|
|
157
|
+
*/
|
|
158
|
+
export function resolveOtelConfig(raw, env = process.env) {
|
|
159
|
+
const opts = raw ?? {};
|
|
160
|
+
const enabled = parseBool(env.OPENCODE_OTEL_ENABLED) ?? opts.enabled !== false;
|
|
161
|
+
const endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT || opts.endpoint || undefined;
|
|
162
|
+
const endpoints = {};
|
|
163
|
+
for (const signal of SIGNALS) {
|
|
164
|
+
const value = env[SIGNAL_ENV[signal].endpoint] || opts.endpoints?.[signal];
|
|
165
|
+
if (value) {
|
|
166
|
+
endpoints[signal] = value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Env headers merge over option headers key-by-key rather than replacing the
|
|
170
|
+
// map, so a machine can override just its auth token without restating the
|
|
171
|
+
// whole set the served config provided.
|
|
172
|
+
const headers = {
|
|
173
|
+
...stringRecord(opts.headers),
|
|
174
|
+
...parseKeyValueList(env.OTEL_EXPORTER_OTLP_HEADERS)
|
|
175
|
+
};
|
|
176
|
+
// An endpoint anywhere is what makes OTLP the implicit default; with none
|
|
177
|
+
// configured the plugin stays completely inert.
|
|
178
|
+
const hasEndpoint = Boolean(endpoint) || SIGNALS.some((s) => Boolean(endpoints[s]));
|
|
179
|
+
const fallback = hasEndpoint ? "otlp" : "none";
|
|
180
|
+
const exporters = {};
|
|
181
|
+
for (const signal of SIGNALS) {
|
|
182
|
+
exporters[signal] = parseExporter(env[SIGNAL_ENV[signal].exporter]) ?? opts.exporters?.[signal] ?? fallback;
|
|
183
|
+
}
|
|
184
|
+
const resourceAttributes = {
|
|
185
|
+
...stringRecord(opts.resourceAttributes),
|
|
186
|
+
...parseKeyValueList(env.OTEL_RESOURCE_ATTRIBUTES)
|
|
187
|
+
};
|
|
188
|
+
const temporality = env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE?.trim().toLowerCase() === "cumulative" ? "cumulative" : env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE?.trim().toLowerCase() === "delta" ? "delta" : opts.metricTemporality ?? DEFAULTS.metricTemporality;
|
|
189
|
+
const filteredTools = new Set(env.OTEL_OPENCODE_FILTERED_TOOLS !== undefined ? stringList(env.OTEL_OPENCODE_FILTERED_TOOLS) : stringList(opts.filteredTools));
|
|
190
|
+
// Resolve the source first, then normalise based on *its* own type — see
|
|
191
|
+
// `parseCommand` for why keying the split on anything else is a trap.
|
|
192
|
+
const tokenCommand = parseCommand(env.OPENCODE_OTEL_TOKEN_COMMAND ?? opts.tokenCommand);
|
|
193
|
+
const enabledSignals = SIGNALS.filter((s) => exporters[s] !== "none");
|
|
194
|
+
return {
|
|
195
|
+
enabled,
|
|
196
|
+
active: enabled && enabledSignals.length > 0,
|
|
197
|
+
endpoint,
|
|
198
|
+
endpoints,
|
|
199
|
+
headers,
|
|
200
|
+
tokenCommand,
|
|
201
|
+
tokenHeader: env.OPENCODE_OTEL_TOKEN_HEADER || opts.tokenHeader || "Authorization",
|
|
202
|
+
tokenPrefix: env.OPENCODE_OTEL_TOKEN_PREFIX ?? opts.tokenPrefix ?? "Bearer ",
|
|
203
|
+
tokenRefreshMs: parsePositiveInt(env.OPENCODE_OTEL_TOKEN_REFRESH_MS) ?? (opts.tokenRefreshMs && opts.tokenRefreshMs > 0 ? opts.tokenRefreshMs : DEFAULT_REFRESH_MS),
|
|
204
|
+
tokenTimeoutMs: parsePositiveInt(env.OPENCODE_OTEL_TOKEN_TIMEOUT_MS) ?? (opts.tokenTimeoutMs && opts.tokenTimeoutMs > 0 ? opts.tokenTimeoutMs : 1e4),
|
|
205
|
+
exporters,
|
|
206
|
+
serviceName: env.OTEL_SERVICE_NAME || opts.serviceName || DEFAULTS.serviceName,
|
|
207
|
+
environment: env.OPENCODE_OTEL_ENVIRONMENT || opts.environment || undefined,
|
|
208
|
+
resourceAttributes,
|
|
209
|
+
collectVcs: parseBool(env.OPENCODE_OTEL_COLLECT_VCS) ?? opts.collectVcs !== false,
|
|
210
|
+
metricExportIntervalMs: parsePositiveInt(firstDefined(env, SIGNAL_ENV.metrics.interval)) ?? (opts.metricExportIntervalMs && opts.metricExportIntervalMs > 0 ? opts.metricExportIntervalMs : DEFAULTS.metricExportIntervalMs),
|
|
211
|
+
logExportIntervalMs: parsePositiveInt(firstDefined(env, SIGNAL_ENV.logs.interval)) ?? (opts.logExportIntervalMs && opts.logExportIntervalMs > 0 ? opts.logExportIntervalMs : DEFAULTS.logExportIntervalMs),
|
|
212
|
+
traceExportIntervalMs: parsePositiveInt(firstDefined(env, SIGNAL_ENV.traces.interval)) ?? (opts.traceExportIntervalMs && opts.traceExportIntervalMs > 0 ? opts.traceExportIntervalMs : DEFAULTS.traceExportIntervalMs),
|
|
213
|
+
metricTemporality: temporality,
|
|
214
|
+
includeSessionId: parseBool(env.OTEL_METRICS_INCLUDE_SESSION_ID) ?? opts.includeSessionId === true,
|
|
215
|
+
filteredTools,
|
|
216
|
+
propagateTraceContext: parseBool(env.OPENCODE_OTEL_PROPAGATE_TRACE_CONTEXT) ?? (opts.propagateTraceContext !== false && exporters.traces !== "none")
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Resolve the OTLP URL for one signal. A per-signal endpoint is used verbatim
|
|
221
|
+
* (spec: it already includes the path); the base endpoint gets the signal path
|
|
222
|
+
* appended.
|
|
223
|
+
*/
|
|
224
|
+
export function signalUrl(config, signal) {
|
|
225
|
+
const explicit = config.endpoints[signal];
|
|
226
|
+
if (explicit) {
|
|
227
|
+
return explicit;
|
|
228
|
+
}
|
|
229
|
+
if (!config.endpoint) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
const base = config.endpoint.replace(/\/+$/, "");
|
|
233
|
+
const path = signal === "traces" ? "v1/traces" : signal === "metrics" ? "v1/metrics" : "v1/logs";
|
|
234
|
+
return `${base}/${path}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"mappings":"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","names":[],"sources":["../src/config.ts"],"version":3,"file":"config.js","sourceRoot":""}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A resource attribute whose value only becomes known after the plugin has
|
|
3
|
+
* already started — OpenCode reports the host version and the git branch as
|
|
4
|
+
* *events*, not as plugin input, but an OTel `Resource` is fixed at provider
|
|
5
|
+
* construction.
|
|
6
|
+
*
|
|
7
|
+
* The OTel resource API accepts promise-valued attributes and exporters await
|
|
8
|
+
* them before the first export, so a deferred value is the supported way to
|
|
9
|
+
* bridge that gap. The timeout is not optional: a promise that never settles
|
|
10
|
+
* would block every export forever, so an attribute that has not arrived in
|
|
11
|
+
* time resolves to `undefined` and is dropped from the resource.
|
|
12
|
+
*/
|
|
13
|
+
export interface DeferredAttribute {
|
|
14
|
+
/** Hand this to `resourceFromAttributes`. */
|
|
15
|
+
readonly value: Promise<string | undefined>;
|
|
16
|
+
/** Settle it with the real value. Later calls are ignored. */
|
|
17
|
+
settle(value: string | undefined): void;
|
|
18
|
+
/** Give up and settle as absent. Idempotent. */
|
|
19
|
+
abandon(): void;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_DEFERRED_TIMEOUT_MS = 2e3;
|
|
22
|
+
export declare function deferredAttribute(timeoutMs?: number, schedule?: typeof setTimeout): DeferredAttribute;
|
package/dist/deferred.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const DEFAULT_DEFERRED_TIMEOUT_MS = 2e3;
|
|
2
|
+
export function deferredAttribute(timeoutMs = DEFAULT_DEFERRED_TIMEOUT_MS, schedule = setTimeout) {
|
|
3
|
+
let resolve = () => {};
|
|
4
|
+
let settled = false;
|
|
5
|
+
const value = new Promise((r) => {
|
|
6
|
+
resolve = r;
|
|
7
|
+
});
|
|
8
|
+
const finish = (next) => {
|
|
9
|
+
if (settled) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
settled = true;
|
|
13
|
+
resolve(next);
|
|
14
|
+
};
|
|
15
|
+
const timer = schedule(() => finish(undefined), timeoutMs);
|
|
16
|
+
// Never hold a short CLI invocation open just to wait for an attribute.
|
|
17
|
+
timer.unref?.();
|
|
18
|
+
return {
|
|
19
|
+
value,
|
|
20
|
+
settle: (next) => {
|
|
21
|
+
if (next !== undefined && next !== "") {
|
|
22
|
+
finish(next);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
abandon: () => finish(undefined)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=deferred.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"mappings":"AAqBA,OAAO,MAAM,8BAA8B;AAE3C,OAAO,SAAS,kBACd,YAAoB,6BACpB,WAA8B,YACX;CACnB,IAAI,gBAAqD,CAAC;CAC1D,IAAI,UAAU;CACd,MAAM,QAAQ,IAAI,SAA6B,MAAM;EACnD,UAAU;CACZ,CAAC;CAED,MAAM,UAAU,SAAmC;EACjD,IAAI,SAAS;GACX;EACF;EACA,UAAU;EACV,QAAQ,IAAI;CACd;CAEA,MAAM,QAAQ,eAAe,OAAO,SAAS,GAAG,SAAS;;CAEzD,AAAC,MAAiC,QAAQ;CAE1C,OAAO;EACL;EACA,SAAS,SAAS;GAChB,IAAI,SAAS,aAAa,SAAS,IAAI;IACrC,OAAO,IAAI;GACb;EACF;EACA,eAAe,OAAO,SAAS;CACjC;AACF","names":[],"sources":["../src/deferred.ts"],"version":3,"file":"deferred.js","sourceRoot":""}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ExportResult } from "@opentelemetry/core";
|
|
2
|
+
import type { Logger } from "./logging.js";
|
|
3
|
+
import type { SignalName } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* The shape all three OTel exporters share: `export(items, resultCallback)`.
|
|
6
|
+
* Typed structurally rather than against `SpanExporter | PushMetricExporter |
|
|
7
|
+
* LogRecordExporter`, whose item types are unrelated.
|
|
8
|
+
*/
|
|
9
|
+
export interface ExporterLike {
|
|
10
|
+
export(items: never, resultCallback: (result: ExportResult) => void): void;
|
|
11
|
+
shutdown(): Promise<void>;
|
|
12
|
+
forceFlush?(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Wrap an exporter so a failed export reaches the host log stream.
|
|
16
|
+
*
|
|
17
|
+
* Without this, a rejected OTLP request — an expired credential, a wrong
|
|
18
|
+
* audience, an unreachable collector — is reported only through the OTel SDK's
|
|
19
|
+
* own `diag` channel, which nothing here subscribes to. The visible symptom is
|
|
20
|
+
* that telemetry silently stops while the session looks entirely healthy.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately not `diag.setLogger`: that is process-global and would hijack
|
|
23
|
+
* the channel for the host and every other plugin. Decorating our own exporters
|
|
24
|
+
* observes exactly the failures we caused and nothing else.
|
|
25
|
+
*/
|
|
26
|
+
export declare function withFailureLogging<T extends ExporterLike>(exporter: T, signal: SignalName, logger: Logger, options?: {
|
|
27
|
+
onFailure?: () => void;
|
|
28
|
+
}): T;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ExportResultCode } from "@opentelemetry/core";
|
|
2
|
+
/**
|
|
3
|
+
* Wrap an exporter so a failed export reaches the host log stream.
|
|
4
|
+
*
|
|
5
|
+
* Without this, a rejected OTLP request — an expired credential, a wrong
|
|
6
|
+
* audience, an unreachable collector — is reported only through the OTel SDK's
|
|
7
|
+
* own `diag` channel, which nothing here subscribes to. The visible symptom is
|
|
8
|
+
* that telemetry silently stops while the session looks entirely healthy.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately not `diag.setLogger`: that is process-global and would hijack
|
|
11
|
+
* the channel for the host and every other plugin. Decorating our own exporters
|
|
12
|
+
* observes exactly the failures we caused and nothing else.
|
|
13
|
+
*/
|
|
14
|
+
export function withFailureLogging(exporter, signal, logger, options = {}) {
|
|
15
|
+
let consecutiveFailures = 0;
|
|
16
|
+
const wrapped = {
|
|
17
|
+
export(items, resultCallback) {
|
|
18
|
+
exporter.export(items, (result) => {
|
|
19
|
+
if (result.code === ExportResultCode.SUCCESS) {
|
|
20
|
+
if (consecutiveFailures > 0) {
|
|
21
|
+
logger.info("otel_export_recovered", {
|
|
22
|
+
signal,
|
|
23
|
+
afterFailures: consecutiveFailures
|
|
24
|
+
});
|
|
25
|
+
consecutiveFailures = 0;
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
consecutiveFailures += 1;
|
|
29
|
+
logger.warn("otel_export_failed", {
|
|
30
|
+
signal,
|
|
31
|
+
consecutiveFailures,
|
|
32
|
+
// `error` may carry an HTTP status; the message is the actionable
|
|
33
|
+
// part (e.g. "authentication didn't succeed" from an OIDC gate).
|
|
34
|
+
error: describeExportError(result.error),
|
|
35
|
+
status: readStatus(result.error)
|
|
36
|
+
});
|
|
37
|
+
options.onFailure?.();
|
|
38
|
+
}
|
|
39
|
+
resultCallback(result);
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
shutdown: () => exporter.shutdown(),
|
|
43
|
+
...exporter.forceFlush ? { forceFlush: () => exporter.forceFlush?.() } : {}
|
|
44
|
+
};
|
|
45
|
+
// Preserve the prototype and any extra members the concrete exporter exposes.
|
|
46
|
+
return Object.assign(Object.create(Object.getPrototypeOf(exporter)), exporter, wrapped);
|
|
47
|
+
}
|
|
48
|
+
function describeExportError(error) {
|
|
49
|
+
if (!error) {
|
|
50
|
+
return "unknown";
|
|
51
|
+
}
|
|
52
|
+
if (error instanceof Error) {
|
|
53
|
+
return error.message;
|
|
54
|
+
}
|
|
55
|
+
return String(error);
|
|
56
|
+
}
|
|
57
|
+
/** OTLP exporter errors carry the HTTP status as `code` or `statusCode`. */
|
|
58
|
+
function readStatus(error) {
|
|
59
|
+
if (!error || typeof error !== "object") {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const candidate = error;
|
|
63
|
+
for (const value of [candidate.statusCode, candidate.code]) {
|
|
64
|
+
if (typeof value === "number") {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
if (typeof value === "string" && /^\d{3}$/.test(value)) {
|
|
68
|
+
return Number(value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=export-logging.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"mappings":"AAAA,SAA4B,wBAAwB;;;;;;;;;;;;;AA4BpD,OAAO,SAAS,mBACd,UACA,QACA,QACA,UAAsC,CAAC,GACpC;CACH,IAAI,sBAAsB;CAE1B,MAAM,UAAwB;EAC5B,OAAO,OAAO,gBAAgB;GAC5B,SAAS,OAAO,QAAQ,WAAW;IACjC,IAAI,OAAO,SAAS,iBAAiB,SAAS;KAC5C,IAAI,sBAAsB,GAAG;MAC3B,OAAO,KAAK,yBAAyB;OAAE;OAAQ,eAAe;MAAoB,CAAC;MACnF,sBAAsB;KACxB;IACF,OAAO;KACL,uBAAuB;KACvB,OAAO,KAAK,sBAAsB;MAChC;MACA;;;MAGA,OAAO,oBAAoB,OAAO,KAAK;MACvC,QAAQ,WAAW,OAAO,KAAK;KACjC,CAAC;KACD,QAAQ,YAAY;IACtB;IACA,eAAe,MAAM;GACvB,CAAC;EACH;EACA,gBAAgB,SAAS,SAAS;EAClC,GAAI,SAAS,aAAa,EAAE,kBAAkB,SAAS,aAAa,EAAmB,IAAI,CAAC;CAC9F;;CAGA,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,eAAe,QAAQ,CAAC,GAAG,UAAU,OAAO;AACxF;AAEA,SAAS,oBAAoB,OAAwB;CACnD,IAAI,CAAC,OAAO;EACV,OAAO;CACT;CACA,IAAI,iBAAiB,OAAO;EAC1B,OAAO,MAAM;CACf;CACA,OAAO,OAAO,KAAK;AACrB;;AAGA,SAAS,WAAW,OAAoC;CACtD,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU;EACvC,OAAO;CACT;CACA,MAAM,YAAY;CAClB,KAAK,MAAM,SAAS,CAAC,UAAU,YAAY,UAAU,IAAI,GAAG;EAC1D,IAAI,OAAO,UAAU,UAAU;GAC7B,OAAO;EACT;EACA,IAAI,OAAO,UAAU,YAAY,UAAU,KAAK,KAAK,GAAG;GACtD,OAAO,OAAO,KAAK;EACrB;CACF;CACA,OAAO;AACT","names":[],"sources":["../src/export-logging.ts"],"version":3,"file":"export-logging.js","sourceRoot":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./lib.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"mappings":"AAAA,cAAc","names":[],"sources":["../src/index.ts"],"version":3,"file":"index.js","sourceRoot":""}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Counter, Histogram, Meter } from "@opentelemetry/api";
|
|
2
|
+
/**
|
|
3
|
+
* Every metric this plugin emits, created once per plugin instance.
|
|
4
|
+
*
|
|
5
|
+
* Naming follows the GenAI semantic conventions where one exists
|
|
6
|
+
* (`gen_ai.client.*`) and `opencode.*` otherwise — deliberately matching the
|
|
7
|
+
* vocabulary `opencode-otel-plugin` already established, so both plugins' data
|
|
8
|
+
* lands on the same dashboards. See `plans/otel.md`.
|
|
9
|
+
*/
|
|
10
|
+
export interface Instruments {
|
|
11
|
+
/** Real USD, straight from `AssistantMessage.cost` — no price table involved. */
|
|
12
|
+
cost: Counter;
|
|
13
|
+
/** All five token types, discriminated by `gen_ai.token.type`. */
|
|
14
|
+
tokens: Histogram;
|
|
15
|
+
/** Assistant-message wall time, seconds. */
|
|
16
|
+
duration: Histogram;
|
|
17
|
+
sessions: Counter;
|
|
18
|
+
requests: Counter;
|
|
19
|
+
compactions: Counter;
|
|
20
|
+
/** Wall time a session spent `busy`, seconds. */
|
|
21
|
+
activeTime: Counter;
|
|
22
|
+
linesOfCode: Counter;
|
|
23
|
+
toolInvocations: Counter;
|
|
24
|
+
permissionDecisions: Counter;
|
|
25
|
+
commands: Counter;
|
|
26
|
+
}
|
|
27
|
+
export declare function createInstruments(meter: Meter): Instruments;
|
|
28
|
+
/** Best-effort language for a path, by extension. `undefined` when unknown. */
|
|
29
|
+
export declare function detectLanguage(file: string): string | undefined;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export function createInstruments(meter) {
|
|
2
|
+
return {
|
|
3
|
+
cost: meter.createCounter("opencode.cost.usage", {
|
|
4
|
+
description: "Cost of model usage, as reported by the OpenCode host",
|
|
5
|
+
unit: "USD"
|
|
6
|
+
}),
|
|
7
|
+
tokens: meter.createHistogram("gen_ai.client.token.usage", {
|
|
8
|
+
description: "Tokens consumed per assistant message, by token type",
|
|
9
|
+
unit: "{token}"
|
|
10
|
+
}),
|
|
11
|
+
duration: meter.createHistogram("gen_ai.client.operation.duration", {
|
|
12
|
+
description: "Duration of a model operation",
|
|
13
|
+
unit: "s"
|
|
14
|
+
}),
|
|
15
|
+
sessions: meter.createCounter("opencode.session.count", {
|
|
16
|
+
description: "Sessions started",
|
|
17
|
+
unit: "{session}"
|
|
18
|
+
}),
|
|
19
|
+
requests: meter.createCounter("opencode.session.request.count", {
|
|
20
|
+
description: "Assistant messages completed",
|
|
21
|
+
unit: "{request}"
|
|
22
|
+
}),
|
|
23
|
+
compactions: meter.createCounter("opencode.session.compaction.count", {
|
|
24
|
+
description: "Context compactions performed",
|
|
25
|
+
unit: "{compaction}"
|
|
26
|
+
}),
|
|
27
|
+
activeTime: meter.createCounter("opencode.active_time.total", {
|
|
28
|
+
description: "Wall time sessions spent busy",
|
|
29
|
+
unit: "s"
|
|
30
|
+
}),
|
|
31
|
+
linesOfCode: meter.createCounter("opencode.lines_of_code.count", {
|
|
32
|
+
description: "Lines added or removed across session diffs",
|
|
33
|
+
unit: "{line}"
|
|
34
|
+
}),
|
|
35
|
+
toolInvocations: meter.createCounter("opencode.tool.invocations", {
|
|
36
|
+
description: "Tool executions",
|
|
37
|
+
unit: "{invocation}"
|
|
38
|
+
}),
|
|
39
|
+
permissionDecisions: meter.createCounter("opencode.permission.decision.count", {
|
|
40
|
+
description: "Permission prompts resolved",
|
|
41
|
+
unit: "{decision}"
|
|
42
|
+
}),
|
|
43
|
+
commands: meter.createCounter("opencode.command.executed.count", {
|
|
44
|
+
description: "Slash commands executed",
|
|
45
|
+
unit: "{command}"
|
|
46
|
+
})
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const EXTENSION_LANGUAGES = {
|
|
50
|
+
ts: "typescript",
|
|
51
|
+
tsx: "typescript",
|
|
52
|
+
mts: "typescript",
|
|
53
|
+
cts: "typescript",
|
|
54
|
+
js: "javascript",
|
|
55
|
+
jsx: "javascript",
|
|
56
|
+
mjs: "javascript",
|
|
57
|
+
cjs: "javascript",
|
|
58
|
+
py: "python",
|
|
59
|
+
rs: "rust",
|
|
60
|
+
go: "go",
|
|
61
|
+
java: "java",
|
|
62
|
+
kt: "kotlin",
|
|
63
|
+
kts: "kotlin",
|
|
64
|
+
swift: "swift",
|
|
65
|
+
dart: "dart",
|
|
66
|
+
rb: "ruby",
|
|
67
|
+
php: "php",
|
|
68
|
+
cs: "csharp",
|
|
69
|
+
c: "c",
|
|
70
|
+
h: "c",
|
|
71
|
+
cc: "cpp",
|
|
72
|
+
cpp: "cpp",
|
|
73
|
+
hpp: "cpp",
|
|
74
|
+
m: "objective-c",
|
|
75
|
+
mm: "objective-c",
|
|
76
|
+
scala: "scala",
|
|
77
|
+
ex: "elixir",
|
|
78
|
+
exs: "elixir",
|
|
79
|
+
sh: "shell",
|
|
80
|
+
bash: "shell",
|
|
81
|
+
zsh: "shell",
|
|
82
|
+
sql: "sql",
|
|
83
|
+
html: "html",
|
|
84
|
+
css: "css",
|
|
85
|
+
scss: "scss",
|
|
86
|
+
json: "json",
|
|
87
|
+
yaml: "yaml",
|
|
88
|
+
yml: "yaml",
|
|
89
|
+
toml: "toml",
|
|
90
|
+
md: "markdown",
|
|
91
|
+
tf: "terraform"
|
|
92
|
+
};
|
|
93
|
+
/** Best-effort language for a path, by extension. `undefined` when unknown. */
|
|
94
|
+
export function detectLanguage(file) {
|
|
95
|
+
const base = file.slice(file.lastIndexOf("/") + 1);
|
|
96
|
+
const dot = base.lastIndexOf(".");
|
|
97
|
+
if (dot <= 0) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
return EXTENSION_LANGUAGES[base.slice(dot + 1).toLowerCase()];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=instruments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"mappings":"AA4BA,OAAO,SAAS,kBAAkB,OAA2B;CAC3D,OAAO;EACL,MAAM,MAAM,cAAc,uBAAuB;GAC/C,aAAa;GACb,MAAM;EACR,CAAC;EACD,QAAQ,MAAM,gBAAgB,6BAA6B;GACzD,aAAa;GACb,MAAM;EACR,CAAC;EACD,UAAU,MAAM,gBAAgB,oCAAoC;GAClE,aAAa;GACb,MAAM;EACR,CAAC;EACD,UAAU,MAAM,cAAc,0BAA0B;GACtD,aAAa;GACb,MAAM;EACR,CAAC;EACD,UAAU,MAAM,cAAc,kCAAkC;GAC9D,aAAa;GACb,MAAM;EACR,CAAC;EACD,aAAa,MAAM,cAAc,qCAAqC;GACpE,aAAa;GACb,MAAM;EACR,CAAC;EACD,YAAY,MAAM,cAAc,8BAA8B;GAC5D,aAAa;GACb,MAAM;EACR,CAAC;EACD,aAAa,MAAM,cAAc,gCAAgC;GAC/D,aAAa;GACb,MAAM;EACR,CAAC;EACD,iBAAiB,MAAM,cAAc,6BAA6B;GAChE,aAAa;GACb,MAAM;EACR,CAAC;EACD,qBAAqB,MAAM,cAAc,sCAAsC;GAC7E,aAAa;GACb,MAAM;EACR,CAAC;EACD,UAAU,MAAM,cAAc,mCAAmC;GAC/D,aAAa;GACb,MAAM;EACR,CAAC;CACH;AACF;AAEA,MAAM,sBAA8C;CAClD,IAAI;CACJ,KAAK;CACL,KAAK;CACL,KAAK;CACL,IAAI;CACJ,KAAK;CACL,KAAK;CACL,KAAK;CACL,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,MAAM;CACN,IAAI;CACJ,KAAK;CACL,OAAO;CACP,MAAM;CACN,IAAI;CACJ,KAAK;CACL,IAAI;CACJ,GAAG;CACH,GAAG;CACH,IAAI;CACJ,KAAK;CACL,KAAK;CACL,GAAG;CACH,IAAI;CACJ,OAAO;CACP,IAAI;CACJ,KAAK;CACL,IAAI;CACJ,MAAM;CACN,KAAK;CACL,KAAK;CACL,MAAM;CACN,KAAK;CACL,MAAM;CACN,MAAM;CACN,MAAM;CACN,KAAK;CACL,MAAM;CACN,IAAI;CACJ,IAAI;AACN;;AAGA,OAAO,SAAS,eAAe,MAAkC;CAC/D,MAAM,OAAO,KAAK,MAAM,KAAK,YAAY,GAAG,IAAI,CAAC;CACjD,MAAM,MAAM,KAAK,YAAY,GAAG;CAChC,IAAI,OAAO,GAAG;EACZ,OAAO;CACT;CACA,OAAO,oBAAoB,KAAK,MAAM,MAAM,CAAC,CAAC,CAAC,YAAY;AAC7D","names":[],"sources":["../src/instruments.ts"],"version":3,"file":"instruments.js","sourceRoot":""}
|