feat(Opencode): add OpenTelemetry instrumentation plugin for Opencode#48
Open
whoIam0987 wants to merge 39 commits into
Open
feat(Opencode): add OpenTelemetry instrumentation plugin for Opencode#48whoIam0987 wants to merge 39 commits into
whoIam0987 wants to merge 39 commits into
Conversation
…vocations handling P0 fix — process.exit concurrency race (src/index.ts): - Extract interceptedExit() as a named function that checks exitIntercepted - SIGTERM/SIGINT handlers now call interceptedExit() instead of calling shutdown() directly, so the exitIntercepted guard protects all entry points - Prevents concurrent SIGTERM+process.exit or SIGTERM+SIGINT from calling originalExit multiple times or running shutdown() twice P1 fix — handleSessionError double-processing activeInvocations (src/handlers/session.ts): - sweepSession() now accepts an optional errorMessage parameter - In the error path, sweepSession() adds 'session.error' events to both agentSpan and entrySpan before ending them (previously done in handleSessionError) - handleSessionError no longer manually handles activeInvocations after calling sweepSession() — fixes double setStatus(ERROR)/end() on spans Update session-traces.test.ts to verify session.error events are added by sweepSession (now 1 event on both agent and entry spans).
…nd attribute Add SPAN_KIND_ATTR constant to util.ts (exported): - Checks LOONGSUITE_SEMCONV_DIALECT_NAME=ALIBABA_GROUP env var - Checks if OTEL_EXPORTER_OTLP_ENDPOINT contains 'sunfire' - When either is true: uses 'gen_ai.span_kind_name' (ALIBABA_GROUP dialect) - Otherwise: uses 'gen_ai.span.kind' (OTel standard default) Replace the single hardcoded 'gen_ai.span.kind' in genAiSpanAttrs() with SPAN_KIND_ATTR so all spans (ENTRY/AGENT/STEP/LLM/TOOL) use the correct attribute name for the configured dialect. Update tests to import and use SPAN_KIND_ATTR so they pass regardless of the environment's dialect setting.
The legacy endpoint env var OPENCODE_OTLP_ENDPOINT should also trigger the Sunfire/ALIBABA_GROUP semconv dialect auto-detection, not just OTEL_EXPORTER_OTLP_ENDPOINT.
…t protocol The OTLPLogExporter from exporter-logs-otlp-grpc was receiving an HTTP endpoint (http://host:port/v1/logs) which gRPC cannot use — it silently fails or ignores the URL. All three exporters now use HTTP/Protobuf: - Logs: exporter-logs-otlp-grpc → exporter-logs-otlp-http - Metrics: exporter-metrics-otlp-proto (unchanged, already HTTP) - Traces: exporter-trace-otlp-proto (unchanged, already HTTP)
bun.lock and package-lock.json contained registry.anpm.alibaba-inc.com URLs from the Alibaba internal npm mirror. Regenerated both files using the public registry.npmjs.org so the package can be installed by external users without internal network access. Also picks up the latest package versions (@opencode-ai/sdk 1.4.6, etc.) and removes the grpc exporter packages replaced in the previous commit.
Root cause: config.otlpHeaders was parsed from env but never forwarded to the OTLP exporters, so authenticated backends silently rejected all telemetry exports. config.ts: - Add parseOtlpHeaders(raw) — parses 'k=v,k=v' header string to object otel.ts: - setupOtel() gains optional headers parameter - headers forwarded to OTLPMetricExporter, OTLPLogExporter, OTLPTraceExporter index.ts: - Call parseOtlpHeaders(config.otlpHeaders) and pass to setupOtel() - Legacy: propagate OPENCODE_OTLP_HEADERS → OTEL_EXPORTER_OTLP_HEADERS when the standard var is unset, for SDK-level header resolution
service.name was hardcoded to 'opencode', ignoring the standard OTEL_SERVICE_NAME env var. Now uses OTEL_SERVICE_NAME if set, falling back to 'opencode'. OTEL_RESOURCE_ATTRIBUTES can still override it.
… through call chain - config.ts: loadFileConfig() reads ~/.config/opencode/otel-plugin.json as fallback for endpoint, serviceName, headers, semconvDialect (works even without shell env) - buildResource(version, serviceName?) accepts explicit serviceName - setupOtel(..., serviceName?) passes it to buildResource - index.ts: passes config.serviceName to setupOtel - enabled = true when file.endpoint is set (no env needed)
…ripts install.sh: - Step 5: write $XDG_CONFIG_HOME/opencode/otel-plugin.json with endpoint, serviceName, headers, semconvDialect (only non-empty fields) uninstall.sh: - Step 4: delete otel-plugin.json if it exists
install.sh:
- Step 2: npm pack in package root → generate tgz
- Step 3: move tgz to ~/.config/opencode/, write file dep in package.json
- Step 4: register plugin name in opencode.json
- Steps 5-7: Sunfire detection, otel-plugin.json, env block (unchanged)
- opencode/bun auto-installs local tgz on next startup
uninstall.sh:
- Step 3: remove loongsuite-*.tgz from ~/.config/opencode/
remove @loongsuite/... entry from package.json dependencies
install.sh:
- Step 2: npm install in source dir to ensure node_modules present
- Step 3: write ~/.config/opencode/plugins/opentelemetry-instrumentation-opencode.ts
containing: import { OtelPlugin } from '<pkg>/src/index.ts' + export default
Cleans up stale loose files from previous install methods
- Step 4: remove pkg name from opencode.json plugin array if present
(local file plugins do not need to be declared there)
- Steps 5-7: Sunfire detection, otel-plugin.json, env block (unchanged)
uninstall.sh:
- Step 3: delete plugin entry .ts file
Also cleans up stale tgz files from previous install method
…in.json src/config.ts: - Remove FileConfig type and loadFileConfig() function - Remove node:fs, node:path, node:os imports - Remove serviceName field from PluginConfig - loadConfig() reads env vars only (OTEL_* / OPENCODE_* fallback) src/otel.ts: - buildResource(version) — remove serviceName parameter - setupOtel(...) — remove serviceName parameter - service.name resolved from process.env.OTEL_SERVICE_NAME directly src/index.ts: - Remove config.serviceName from setupOtel call scripts/install.sh: - Remove step 6 (write otel-plugin.json); env vars in shell profiles are enough (.zshenv is loaded by all zsh processes including opencode plugin worker) scripts/uninstall.sh: - Remove step 4 (delete otel-plugin.json)
scripts/pack.sh: - Packs src/, scripts/install.sh, scripts/uninstall.sh, package.json, README.md, LICENSE into dist/*.tar.gz - Handles macOS xattr via cp -rX + xattr -cr + COPYFILE_DISABLE=1 - Prints ossutil upload command and verification steps scripts/remote-install.sh: - Downloads tarball from configurable URL (DEFAULT_TARBALL_URL placeholder) - Extracts to ~/.cache/opentelemetry.instrumentation.opencode/package - Forwards --endpoint, --service-name, --headers, --debug, --lang to install.sh - Bilingual output (zh/en auto-detected), --lang override - Checks for node/npm/curl/tar dependencies
- Remove stepSpan (react.step) which only marked round number - Chat span and tool span are now direct children of invocation agentContext - Add gen_ai.loop.id and gen_ai.loop.iteration to chat and tool spans - Move gen_ai.react.round and gen_ai.react.finish_reason onto chat span - Remove stepSpan/stepContext from ActiveMessageSpan type Span hierarchy before: agentContext → stepSpan → span [chat] / toolSpan Span hierarchy after: agentContext → span [chat] (with loop attrs) agentContext → toolSpan (with loop attrs) Update message-traces.test.ts: expect 3 spans (ENTRY/AGENT/LLM) instead of 4, and check finish_reason on LLM span.
Previously install.sh only wrote env vars to shell profiles (bashrc/zshrc), which are not sourced in non-interactive shells or when opencode spawns plugin workers directly. Now the generated plugin .ts file includes process.env ||= defaults for each configured value (endpoint, service-name, headers, semconv dialect, debug flag, OTEL_LOGS_EXPORTER). These apply only if the var is not already set, so explicit shell env always takes priority.
Sunfire backend only accepts application/json (HTTP), not protobuf. All three exporters now use the http variant: - exporter-metrics-otlp-proto → exporter-metrics-otlp-http - exporter-trace-otlp-proto → exporter-trace-otlp-http - exporter-logs-otlp-http (unchanged)
…N format" This reverts commit 918ada7.
… global OTel Provider
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat(opencode): add OpenTelemetry instrumentation plugin for OpenCode
Summary
Add an OpenTelemetry observability plugin for OpenCode, enabling session-level Trace, Metrics, and Logs collection. Follows the same observability architecture as the existing Claude Code and OpenClaw plugins, but integrates as a native OpenCode Plugin (no hooks or shell aliases required).
What's included
~/.config/opencode/plugins/, zero-intrusion integration with no code changes requiredevent_tschema records (consumed by loongsuite-pilot), with daily rotationgen_ai.span_kind_name) when Sunfire endpoint detectedcurl -fsSL <url>/remote-install.sh | bashwith--endpointand--service-nameoptionsprocess.exit(), SIGTERM, SIGINT, andbeforeExitto guarantee final span flushKey differences from existing plugins
NODE_OPTIONSinjection~/.config/opencode/plugins/)settings.json)openclaw.plugin.json)intercept.jsviaNODE_OPTIONSNODE_OPTIONSinjectionevent_tschema)process.exitinterception + signal handlersEvent flow
Trace hierarchy
Test plan
Unit tests (bun test)
Run all tests via
bun testin the plugin directory, covering the following modules:End-to-end tests
bash scripts/install.sh --endpoint <url>correctly writes plugin entry + env config blockbash scripts/uninstall.shcleanly removes all artifactsllm.request/llm.response+tool.call/tool.resulteventsprocess.exitinterception ensures final span batch is flushedacs.arms.service.feature: "genai_app"Configuration
All configuration via environment variables (no config files required):
OTEL_EXPORTER_OTLP_ENDPOINThttp://localhost:4318OTEL_EXPORTER_OTLP_TRACES_ENDPOINTOTEL_EXPORTER_OTLP_METRICS_ENDPOINTOTEL_EXPORTER_OTLP_LOGS_ENDPOINTOTEL_EXPORTER_OTLP_HEADERSkey=valueOTEL_SERVICE_NAMEopencodeOTEL_RESOURCE_ATTRIBUTESOTEL_TRACES_EXPORTERnoneto disable tracesOTEL_LOGS_EXPORTERotlpto enable,noneto disableOTEL_METRIC_EXPORT_INTERVAL60000OTEL_METRIC_PREFIXopencode.OTEL_DISABLE_METRICSOTEL_TRACE_MAX_CONTENT_SIZE2048OTEL_BLRP_SCHEDULE_DELAY5000LOONGSUITE_SEMCONV_DIALECT_NAMEALIBABA_GROUPfor ARMS compatibilityChecklist