Back to Beads

OpenTelemetry Data Model

engdocs/design/otel/otel-data-model.md

1.2.120.7 KB
Original Source

OpenTelemetry Data Model

Last reviewed: 2026-08-07

Freshness source: internal/telemetry/, internal/storage/dolt/store.go, internal/compact/haiku.go, cmd/bd/find_duplicates.go, and hook execution code under internal/hooks/.

Complete schema of all telemetry events emitted by Beads. Each event consists of:

  1. Span (→ stdout/console only, when OTEL_TRACES_EXPORTER=console; the legacy BD_OTEL_STDOUT=true translates to that — no remote/OTLP trace backend is wired) with full structured attributes
  2. Metric counter/histogram (→ any OTLP v1.x+ backend, defaults to VictoriaMetrics) for aggregation

Telemetry is hard opt-in: nothing is emitted unless BD_OTEL_ENABLED=true is set (a legacy BD_OTEL_* variable also activates it); standard OTEL_* variables on their own do not.

All command spans automatically carry bd.command, bd.version, bd.args from startup context; bd.actor is added after actor resolution.


Metric Naming Convention

OTel SDK names use dot notation internally. Prometheus-compatible backends (VictoriaMetrics, Prometheus) export these as underscore-separated names, appending type suffixes:

Code name (SDK)Exported name (Prometheus/VM)
bd.storage.operationsbd_storage_operations_total
bd.storage.operation.durationbd_storage_operation_duration_ms
bd.storage.errorsbd_storage_errors_total
bd.issue.countbd_issue_count
bd.db.retry_countbd_db_retry_count_total
bd.db.lock_wait_msbd_db_lock_wait_ms
bd.db.circuit_tripsbd_db_circuit_trips_total
bd.db.circuit_rejectedbd_db_circuit_rejected_total
bd.ai.input_tokensbd_ai_input_tokens_total
bd.ai.output_tokensbd_ai_output_tokens_total
bd.ai.request.durationbd_ai_request_duration_ms

Event Index

EventCategoryStatus
bd.commandCLI✅ Implemented
storage.<Method>Storage (direct)✅ Implemented
storage.<Role>.<Method>Storage (issueops surface)✅ Implemented
dolt.*Dolt Backend✅ Implemented
doltserver.*Server Lifecycle🔲 Roadmap (Tier 1)
hook.execHooks✅ Implemented (span only)
anthropic.messages.newAI✅ Implemented

1. Identity Hierarchy

1.1 Instance

The outermost grouping. Derived at command startup time from the machine hostname and the working directory.

AttributeTypeDescription
hoststringSystem hostname
osstringSystem OS information

1.2 Command

Each bd command execution generates a span with full context.

AttributeTypeSource
bd.commandstringSubcommand name (create, list, show, etc.)
bd.versionstringCurrent bd version string
bd.argsstringFull argument list
bd.actorstringActor identity — set after actor resolution (may lag span start)

2. CLI Command Events

bd.command.<name>

Emitted once per bd subcommand execution. Anchors all subsequent events for that command. The span name is bd.command. + command name (e.g. bd.command.create).

AttributeTypeDescription
bd.commandstringSubcommand name
bd.versionstringbd version
bd.argsstringFull arguments passed to command, scrubbed via scrubArgsForTelemetry (secret-flag values and DSN userinfo redacted)
bd.actorstringActor identity (set after actor resolution)

3. Storage Events

Every storage.DoltStorage method AND every issueops role method is instrumented: the facade wave's role decorators in internal/telemetry/ emit storage.<Method> and storage.<Role>.<Method> spans (e.g. storage.IssueReader.Ready, storage.ReadyClaimer.ClaimNext, storage.Sweeper.Sweep), all feeding the same bd.storage.* metrics with db.operation as the label. The tables below are representative examples, not an exhaustive enumeration.

storage.CreateIssue

Emitted when an issue is created.

AttributeTypeDescription
db.operationstring"CreateIssue"
bd.issue.typestringIssue type (task, epic, merge-request, etc.)
bd.actorstringActor creating the issue

storage.UpdateIssue

Emitted when an issue is updated.

AttributeTypeDescription
db.operationstring"UpdateIssue"
bd.issue.idstringIssue ID being updated
bd.update.countintNumber of fields being updated
bd.actorstringActor updating the issue

storage.GetIssue

Emitted when an issue is retrieved.

AttributeTypeDescription
db.operationstring"GetIssue"
bd.issue.idstringIssue ID being retrieved

storage.SearchIssues

Emitted when searching for issues.

AttributeTypeDescription
db.operationstring"SearchIssues"
bd.querystringSearch query string
bd.result.countintNumber of results returned

storage.GetReadyWork

Emitted when querying for ready work.

AttributeTypeDescription
db.operationstring"GetReadyWork"
bd.result.countintNumber of ready issues returned

storage.GetBlockedIssues

Emitted when querying for blocked issues.

AttributeTypeDescription
db.operationstring"GetBlockedIssues"
bd.result.countintNumber of blocked issues returned

storage.RunInTransaction

Emitted when executing a transaction.

AttributeTypeDescription
db.operationstring"RunInTransaction"
db.commit_msgstringCommit message

Naming rule and complete inventory

Every storage span is named storage. + the operation name, and carries that same bare name in the db.operation attribute (internal/telemetry/storage.go, InstrumentedStorage.op). The sections above detail the highest-traffic spans; the rest follow the identical shape, so they are inventoried here rather than repeated.

There are two families.

Direct storage operationsstorage.<Method>, one per storage method (72 spans):

AddDependency AddIssueComment AddLabel CloseIssue CloseIssueChecked
CountDependencies CountDependents CountEvents CountIssueComments CountIssues
CountIssuesByGroup CreateIssue CreateIssues DeleteIssue GetAllConfig
GetAllEventsSince GetBlockedIssues GetConfig GetDependencies
GetDependenciesWithMetadata GetDependencyTree GetDependents
GetDependentsWithMetadata GetEpicsEligibleForClosure GetEvents GetIssue
GetIssueByExternalRef GetIssueComments GetIssueCommentsPage GetIssuesByIDs
GetIssuesByLabel GetLabels GetLocalMetadata GetProvenanceByRef
GetProvenanceEvents GetReadyWork GetReadyWorkWithCounts GetStatistics
IterAllEventsSince IterBlockedIssues IterDependenciesWithMetadata
IterDependentsWithMetadata IterEvents IterIssueComments IterIssues
IterReadyWork IterWisps ListWisps MergeMetadata MergeSlotAcquire
MergeSlotCheck MergeSlotCreate MergeSlotRelease RecordProvenanceEvent
RemoveDependency RemoveLabel ReopenIssue RunInIssueLifecycleTransaction
RunInTransaction SearchIssueIDs SearchIssues SearchIssuesWithCounts SetConfig
SetLocalMetadata SlotClear SlotGet SlotSet UnclaimIssue UnclaimIssueIfAssignee
UpdateIssue UpdateIssueChecked UpdateIssueType

Issueops role operationsstorage.<Role>.<Method>, emitted when a caller goes through the guarded issueops surface instead of calling storage directly (39 spans):

BatchCloser.CloseBatch BatchCreator.CreateBatch
BlockingAnnotator.AnnotateBlocking Bootstrapper.Bootstrap Commenter.AddComment
Counter.Count Counter.CountByGroup CycleDetector.DetectCycles Deleter.Delete
DependencyEditor.AddDependencies DependencyEditor.RemoveDependency
EdgeReader.ReadEdges InitVerifier.VerifyIdentity IssueClaimer.Claim
IssueOperations.Close IssueOperations.Create IssueOperations.Reopen
IssueOperations.Update IssueReader.Get IssueReader.List IssueReader.Ready
IssueRelations.Related Memories.Forget Memories.List Memories.Recall
Memories.Remember Querier.Query ReadyClaimer.ClaimNext ReadyCounter.CountReady
StatsReporter.AssigneeStats StatsReporter.Stats Sweeper.Sweep
TreeWalker.WalkTree VersionReconciler.ReconcileVersion
VersionReconciler.RecordedVersion WorkspaceConfig.GetSetting
WorkspaceConfig.ListSettings WorkspaceConfig.SetSetting
WorkspaceConfig.UnsetSetting

The two families do not double-count. The role decorators wrap the uninstrumented store (InstrumentedStorage.IssueLifecycle calls Unwrap() before wrapping), so one logical operation through the issueops front door emits its role span only — not a second storage.<Method> span beneath it. A create via issueops is one storage.IssueOperations.Create; a create via direct storage is one storage.CreateIssue. Counters and duration histograms are therefore comparable across both paths.


4. Dolt Backend Events

Shared attributes: every dolt.* SQL span carries the fixed attribute set from doltSpanAttrs()db.system (string, "dolt"), db.readonly (bool, whether the store is read-only), and db.server_mode (bool, currently always true). The tables below list only each span's additional attributes.

dolt.query

Emitted for each SQL read query via queryContext().

AttributeTypeDescription
db.operationstring"query"
db.statementstringSQL statement (truncated to 300 chars)

dolt.exec

Emitted for each SQL write statement via execContext().

AttributeTypeDescription
db.operationstring"exec"
db.statementstringSQL statement (truncated to 300 chars)

dolt.query_row

Emitted for single-row queries via queryRowContext().

AttributeTypeDescription
db.operationstring"query_row"
db.statementstringSQL statement (truncated to 300 chars)

dolt.commit

Emitted for DOLT_COMMIT operations. Carries only the shared doltSpanAttrs() (see the note at the top of this section) — the commit message no longer appears on this span. It survives only as db.commit_msg on the storage.RunInTransaction / storage.RunInIssueLifecycleTransaction spans (internal/telemetry/storage.go:556,564).

dolt.push

Emitted for DOLT_PUSH operations. The span name is dolt.push, or dolt.force_push for force pushes.

AttributeTypeDescription
dolt.remotestringRemote being pushed to
dolt.branchstringBranch being pushed

dolt.pull

Emitted for DOLT_PULL operations.

AttributeTypeDescription
dolt.remotestringRemote being pulled from
dolt.branchstringBranch being pulled

dolt.merge

Emitted for DOLT_MERGE operations.

AttributeTypeDescription
dolt.merge_branchstringBranch being merged
dolt.conflictsintConflict count (set when conflicts are detected)

dolt.merge_with_strategy

Emitted for bd vc merge --strategy merges (pinned-connection merge/resolve/commit sequence).

AttributeTypeDescription
dolt.merge_branchstringBranch being merged
dolt.merge_strategystringConflict-resolution strategy

dolt.branch

Emitted for DOLT_BRANCH operations.

AttributeTypeDescription
dolt.branchstringBranch name

dolt.checkout

Emitted for DOLT_CHECKOUT operations.

AttributeTypeDescription
dolt.branchstringBranch being checked out

5. Dolt Server Events (Roadmap — not yet implemented)

internal/doltserver/ has no OTel imports. The events below are planned for Tier 1.

doltserver.start (planned)

AttributeTypeDescription
portintPort server is listening on
data_dirstringPath to Dolt data directory
pidintProcess ID of server

doltserver.stop (planned)

AttributeTypeDescription
pidintProcess ID of stopped server
reasonstringStop reason (graceful, forced, idle_timeout, crash)

6. Hooks Events

hook.exec

Emitted for hook execution. Span only — no metric counters or histograms exist for hooks. Duration aggregation is a Tier 3 roadmap item.

AttributeTypeDescription
hook.eventstringEvent type (create, update, close, delete, etc.)
hook.pathstringAbsolute path to hook script
bd.issue_idstringID of triggering issue

Stdout/stderr are added as span events (not attributes):

  • hook.stdout event: output (string, truncated), bytes (int, original size)
  • hook.stderr event: output (string, truncated), bytes (int, original size)

7. AI Events

Emitted by the compaction engine (bd compact) via internal/compact/haiku.go, and by duplicate detection (bd find-duplicates --method ai) via cmd/bd/find_duplicates.go. Both use the Anthropic SDK with Anthropic-compatible credentials from ANTHROPIC_API_KEY, MINIMAX_API_KEY, or ai.api_key.

Note: Only compact/haiku.go records to the bd.ai.* OTel metric instruments. find_duplicates.go records token counts and duration as span attributes only.

anthropic.messages.new

One span per Anthropic API call. The bd.ai.operation attribute distinguishes the two callers.

AttributeTypeDescription
bd.ai.modelstringModel used (e.g. "claude-haiku-4-5")
bd.ai.operationstring"compact" or "find_duplicates"
bd.ai.input_tokensintInput tokens consumed
bd.ai.output_tokensintOutput tokens generated
bd.ai.attemptsintNumber of attempts, including retries (compact only)
bd.ai.batch_sizeintCandidate pairs evaluated (find_duplicates only)
bd.ai.duration_msfloatRequest duration in ms (find_duplicates only; compact records the bd.ai.request.duration metric instead)

Retry policy (compact only; find_duplicates makes a single unretried call): exponential backoff, up to 4 attempts (1 initial + up to 3 retries, maxRetries = 3), on HTTP 429, 5xx, and network timeout errors.


8. Metrics Reference

Metric (code name)TypeLabelsStatus
bd.storage.operationsCounterdb.operation✅ Implemented
bd.storage.operation.durationHistogram (ms)db.operation✅ Implemented
bd.storage.errorsCounterdb.operation✅ Implemented
bd.issue.countGaugestatus✅ Implemented
bd.db.retry_countCounter✅ Implemented
bd.db.lock_wait_msHistogram🔲 Registered, not recorded
bd.db.circuit_tripsCounter✅ Implemented
bd.db.circuit_rejectedCounter✅ Implemented
bd.db.serialization_errorsCounter✅ Implemented
bd.write_retries_totalCountertype (serialization | connection)✅ Implemented
bd.db.conn_acquire_msHistogram✅ Implemented
bd.db.pool_wait_countCounter✅ Implemented
bd.db.pool_wait_msHistogram✅ Implemented
bd.claim_verify_lost_totalCounterop (claim | unclaim | guarded-update | ready-claim)✅ Implemented
bd.claim_verify_recovered_totalCounterop, outcome🔲 Registered, not recorded
bd.ai.input_tokensCounterbd.ai.model✅ Implemented (compact only)
bd.ai.output_tokensCounterbd.ai.model✅ Implemented (compact only)
bd.ai.request.durationHistogram (ms)bd.ai.model✅ Implemented (compact only)

host, os, bd.command, bd.version, bd.actor, db.operation, db.statement,
bd.issue.id, bd.issue.type, hook.event, hook.path, bd.ai.model, bd.ai.operation

10. Configuration and Backend

Environment variables, backend compatibility, Dolt system tables, and roadmap are documented in otel-architecture.md to avoid duplication.

Key variables: BD_OTEL_ENABLED=true (master switch) plus the standard SDK vars — OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, OTEL_TRACES_EXPORTER/OTEL_METRICS_EXPORTER=console, OTEL_SDK_DISABLED. The legacy BD_OTEL_METRICS_URL/BD_OTEL_LOGS_URL/BD_OTEL_STDOUT trio is deprecated but still honored: each is translated to its OTEL_* equivalent with a stderr deprecation warning (commit 1cf7490c7, #3859).


Appendix: Source Reference Audit

Audited against main @ 5eb7c25dd. All line numbers below refer to that commit.

Every metric name, span name, and attribute listed in this document is backed by a specific source location. This table exists to prevent documentation drift and to make re-verification straightforward after code changes.

Metrics (internal/telemetry/storage.go, internal/storage/dolt/store.go, internal/compact/haiku.go)

Metric (SDK name)TypeSource
bd.storage.operationsCounterstorage.go:46m.Int64Counter("bd.storage.operations")
bd.storage.operation.durationHistogramstorage.go:49m.Float64Histogram("bd.storage.operation.duration")
bd.storage.errorsCounterstorage.go:53m.Int64Counter("bd.storage.errors")
bd.issue.countGaugestorage.go:56m.Int64Gauge("bd.issue.count")
bd.db.retry_countCounterstore.go:872m.Int64Counter("bd.db.retry_count")
bd.db.lock_wait_msHistogramstore.go:876 — registered; .Record() not called anywhere
bd.db.circuit_tripsCounterstore.go:880m.Int64Counter("bd.db.circuit_trips")
bd.db.circuit_rejectedCounterstore.go:884m.Int64Counter("bd.db.circuit_rejected")
bd.db.serialization_errorsCounterstore.go:888 — registered; recorded at store.go:1103,1116
bd.write_retries_totalCounterstore.go:892 — registered; recorded at store.go:1104,1117,1124 (type=serialization|connection)
bd.db.conn_acquire_msHistogramstore.go:896 — registered; recorded at transaction.go:167
bd.db.pool_wait_countCounterstore.go:900 — registered; recorded at transaction.go:174
bd.db.pool_wait_msHistogramstore.go:904 — registered; recorded at transaction.go:176
bd.claim_verify_lost_totalCounterstore.go:908 — registered; recorded at claim_verify.go:196, issues.go:439
bd.claim_verify_recovered_totalCounterstore.go:912 — registered; .Add() not called anywhere
bd.ai.input_tokensCounterhaiku.go:117m.Int64Counter("bd.ai.input_tokens")
bd.ai.output_tokensCounterhaiku.go:121m.Int64Counter("bd.ai.output_tokens")
bd.ai.request.durationHistogramhaiku.go:125m.Float64Histogram("bd.ai.request.duration")

Spans and attributes

Span nameAttributesSource
bd.command.<name>bd.command, bd.version, bd.args (scrubbed)built in cmd/bd/command_telemetry.go (startCommandSpan/commandSpanAttrs), called from cmd/bd/main.go:938
bd.command.<name>bd.actor (added later)cmd/bd/main.go:1367
storage.<op> / storage.<Role>.<Method> (all methods)db.operation + method-specific attrsinternal/telemetry/storage.go:75-76 (role decorators throughout internal/telemetry/)
dolt.querydb.operation="query", db.statement + doltSpanAttrs()store.go:1309
dolt.execdb.operation="exec", db.statement + doltSpanAttrs()store.go:1168
dolt.query_rowdb.operation="query_row", db.statement + doltSpanAttrs()store.go:1338
dolt.commitdoltSpanAttrs() onlystore.go:2901
dolt.push / dolt.force_pushdolt.remote, dolt.branchstore.go:3674-3682
dolt.pulldolt.remote, dolt.branchstore.go:3777-3781
dolt.mergedolt.merge_branch; dolt.conflictsstore.go:4348; conflicts at store.go:4370
dolt.merge_with_strategydolt.merge_branch, dolt.merge_strategystore.go:4395
dolt.branchdolt.branchstore.go:4309-4312
dolt.checkoutdolt.branchstore.go:4326-4329
hook.exechook.event, hook.path, bd.issue_idhooks_unix.go:31-36
hook.exec eventshook.stdout / hook.stderr with output, byteshooks_otel.go:14, 20
anthropic.messages.newbd.ai.model, bd.ai.operationhaiku.go:133-138
anthropic.messages.newbd.ai.input_tokens, bd.ai.output_tokens, bd.ai.attemptshaiku.go:171-175
anthropic.messages.newbd.ai.batch_size; bd.ai.duration_ms (find_duplicates only)find_duplicates.go:466-470; find_duplicates.go:491