Repository navigation
[Bug]: opencode.session.duration histogram saturates at 10s — 93% of recorded values collapse into the top default bucket #138
Description
Package version: 2.0.1 (also affects 1.x line per code shape)
Summary
The opencode.session.duration histogram metric is effectively unusable for sessions longer than 10 seconds: values above the OTel SDK's default top bucket boundary (10,000 ms) all collapse into a single bucket, and the per-bucket representative value written to the backend is the bucket boundary/midpoint, not the observed value. In our ~2.5-day dataset, 885 of 953 duration datapoints (92.9%) report exactly 10000 ms, while ground-truth span data shows those same sessions running anywhere from 10s to 7.1 hours.
Root cause (two compounding problems)
1. No explicit bucket advice on the instrument. session.duration is created without advice.explicitBucketBoundaries (src sessionDurationHistogram: meter.createHistogram(...)), so the SDK default boundaries apply:
[0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000]
Anything >10s lands in the open final bucket and is indistinguishable. Notably, session.cost.total does define custom boundaries via advice ([0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 25]), so the pattern already exists in the codebase — session.duration just doesn't use it.
2. startMs fallback can reset the clock. In ensureSession():
const totals = {
startMs: identity?.startMs ?? at, // ← falls back to CURRENT event time
...
}When sessionIdentity is missing (session created before plugin init, or the identity map entry was evicted by setBoundedMap bounding), the session is re-timed from the current event, silently producing tiny durations for long-lived sessions.
Evidence (Elastic EDOT → Elasticsearch OTel mapping mode)
Metric datapoints (one per idle event; distinct values[] observed: {1750, 3750, 6250, 8750, 10000} — exactly the midpoints of the default buckets, plus the top boundary):
- 953
opencode.session.durationdatapoints over ~2.5 days, single OpenCode host - 885 (92.9%) carry
values: [10000] - Ground truth from the plugin's own
opencode.sessionspans (same sessions,durationin ns, n=889):- 0–10s: 121 sessions (13.6%)
- 10s–1min: 220
- 1–10min: 302
-
10min: 249 sessions (28%) — longest 7.1h — all reported as exactly 10000 ms
So not a single session >10s is distinguishable from a 9.9s session in the metric stream. (n=889 spans vs 953 datapoints — idle events can fire more than once per session.)
Suggested fix
- Add explicit bucket advice sized for real sessions, e.g.:
sessionDurationHistogram: meter.createHistogram(`${prefix}session.duration`, {
unit: "ms",
description: "Duration of a session from created to idle in milliseconds",
advice: {
explicitBucketBoundaries: [
1_000, 5_000, 10_000, 30_000, 60_000,
120_000, 300_000, 600_000, 1_800_000, 3_600_000, 10_800_000,
],
},
}),(the session.cost.total instrument already follows this pattern)
- Fix the
startMsfallback inensureSession()— fall back to the session's actual creation time (e.g. keep asessionCreatedmap fromsession.createdevents, or query the host API), notat(current event time).
Workaround for consumers
Until fixed: derive session duration from the opencode.session span (attributes.session.id + duration in ns) instead of the metric — that's what we've switched our dashboard to.
Reported by a user of the plugin running OpenCode 2.x + Elastic EDOT collector with mapping: otel.
Activity