-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathindex.ts
More file actions
179 lines (164 loc) · 6.04 KB
/
index.ts
File metadata and controls
179 lines (164 loc) · 6.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import type { Plugin } from "@opencode-ai/plugin"
import { SeverityNumber } from "@opentelemetry/api-logs"
import { logs } from "@opentelemetry/api-logs"
import pkg from "../package.json" with { type: "json" }
import type {
EventSessionCreated,
EventSessionIdle,
EventSessionError,
EventMessageUpdated,
EventMessagePartUpdated,
EventPermissionUpdated,
EventPermissionReplied,
EventSessionDiff,
EventCommandExecuted,
} from "@opencode-ai/sdk"
import { LEVELS, type Level, type HandlerContext } from "./types.ts"
import { loadConfig, resolveLogLevel } from "./config.ts"
import { probeEndpoint } from "./probe.ts"
import { setupOtel, createInstruments } from "./otel.ts"
import { handleSessionCreated, handleSessionIdle, handleSessionError } from "./handlers/session.ts"
import { handleMessageUpdated, handleMessagePartUpdated } from "./handlers/message.ts"
import { handlePermissionUpdated, handlePermissionReplied } from "./handlers/permission.ts"
import { handleSessionDiff, handleCommandExecuted } from "./handlers/activity.ts"
const PLUGIN_VERSION: string = (pkg as { version?: string }).version ?? "unknown"
/**
* OpenCode plugin that exports session telemetry via OpenTelemetry (OTLP/gRPC).
* Instruments metrics (sessions, tokens, cost, lines of code, commits, tool durations)
* and structured log events. All instrumentation is gated on `OPENCODE_ENABLE_TELEMETRY`.
*/
export const OtelPlugin: Plugin = async ({ project, client }) => {
const config = loadConfig()
let minLevel: Level = "info"
const log: HandlerContext["log"] = async (level, message, extra) => {
if (LEVELS[level] < LEVELS[minLevel]) return
await client.app.log({ body: { service: "opencode-plugin-otel", level, message, extra } })
}
if (!config.enabled) {
await log("info", "telemetry disabled (set OPENCODE_ENABLE_TELEMETRY to enable)")
return {}
}
await log("info", "starting up", {
version: PLUGIN_VERSION,
endpoint: config.endpoint,
metricsInterval: config.metricsInterval,
logsInterval: config.logsInterval,
metricPrefix: config.metricPrefix,
})
const probe = await probeEndpoint(config.endpoint)
if (probe.ok) {
await log("info", "OTLP endpoint reachable", { endpoint: config.endpoint, ms: probe.ms })
} else {
await log("warn", "OTLP endpoint unreachable — exports may fail", {
endpoint: config.endpoint,
error: probe.error,
})
}
const { meterProvider, loggerProvider } = setupOtel(
config.endpoint,
config.metricsInterval,
config.logsInterval,
PLUGIN_VERSION,
)
await log("info", "OTel SDK initialized")
const instruments = createInstruments(config.metricPrefix)
const logger = logs.getLogger("com.opencode")
const pendingToolSpans = new Map()
const pendingPermissions = new Map()
const commonAttrs = { "project.id": project.id } as const
const ctx: HandlerContext = {
logger,
log,
instruments,
commonAttrs,
pendingToolSpans,
pendingPermissions,
}
async function shutdown() {
await Promise.allSettled([meterProvider.shutdown(), loggerProvider.shutdown()])
}
process.on("SIGTERM", () => { shutdown().then(() => process.exit(0)).catch(() => process.exit(1)) })
process.on("SIGINT", () => { shutdown().then(() => process.exit(0)).catch(() => process.exit(1)) })
process.on("beforeExit", () => { shutdown().catch(() => {}) })
const safe = <T extends unknown[]>(
name: string,
fn: (...args: T) => Promise<void> | void,
): ((...args: T) => Promise<void>) =>
async (...args: T) => {
try {
await fn(...args)
} catch (err) {
await log("error", `otel: unhandled error in ${name}`, {
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
})
}
}
return {
config: async (cfg) => {
if (cfg.logLevel) {
const next = resolveLogLevel(cfg.logLevel, minLevel)
if (next !== minLevel) {
minLevel = next
await log("debug", `log level set to "${minLevel}"`)
} else if (cfg.logLevel.toLowerCase() !== minLevel) {
await log("warn", `unknown log level "${cfg.logLevel}", keeping "${minLevel}"`)
}
}
},
"chat.message": safe("chat.message", async (input, output) => {
const promptLength = output.parts.reduce(
(acc, p) => (p.type === "text" ? acc + p.text.length : acc),
0,
)
logger.emit({
severityNumber: SeverityNumber.INFO,
severityText: "INFO",
timestamp: Date.now(),
observedTimestamp: Date.now(),
body: "user_prompt",
attributes: {
"event.name": "user_prompt",
"session.id": input.sessionID,
agent: input.agent ?? "unknown",
prompt_length: promptLength,
model: input.model
? `${input.model.providerID}/${input.model.modelID}`
: "unknown",
...commonAttrs,
},
})
}),
event: safe("event", async ({ event }) => {
switch (event.type) {
case "session.created":
await handleSessionCreated(event as EventSessionCreated, ctx)
break
case "session.idle":
handleSessionIdle(event as EventSessionIdle, ctx)
break
case "session.error":
handleSessionError(event as EventSessionError, ctx)
break
case "session.diff":
handleSessionDiff(event as EventSessionDiff, ctx)
break
case "command.executed":
handleCommandExecuted(event as EventCommandExecuted, ctx)
break
case "permission.updated":
handlePermissionUpdated(event as EventPermissionUpdated, ctx)
break
case "permission.replied":
handlePermissionReplied(event as EventPermissionReplied, ctx)
break
case "message.updated":
await handleMessageUpdated(event as EventMessageUpdated, ctx)
break
case "message.part.updated":
await handleMessagePartUpdated(event as EventMessagePartUpdated, ctx)
break
}
}),
}
}