fix(highlight-go): allow histogram bucket boundaries, and stop racing on the instrument cache - #740
Open
Vadman97 wants to merge 1 commit into
Open
fix(highlight-go): allow histogram bucket boundaries, and stop racing on the instrument cache#740Vadman97 wants to merge 1 commit into
Vadman97 wants to merge 1 commit into
Conversation
… on the instrument cache Investigating a backend histogram that was unusable in ClickHouse traced back to two problems in this SDK. Instruments could not be configured. RecordHistogram called Float64Histogram(name) with no options, so every histogram inherited the OTEL default boundaries — which end at 10000 and are shaped for millisecond latencies. A metric on any other scale collapses entirely into the +Inf overflow bucket, leaving no computable quantile; the case that prompted this had 94% of observations there and 13 of its 16 buckets empty by construction. Unit and description were unreachable for the same reason, so metrics arrived with an empty unit regardless of what they measured. Each Record* function now has a WithOptions variant taking the corresponding OTEL instrument options, wired through hmetric as well since that is what callers actually use. Options are read when the instrument is first built for a name; the doc comments say so. The instrument caches were also read without holding a lock. Every Record* function took the mutex to build the instrument, released it, then indexed the map again to record — an unsynchronized read against a concurrent writer, which Go can escalate from a data race to a fatal "concurrent map read and map write". The three copy-pasted blocks are now one generic cache that returns the instrument it looked up, with proper double-checked locking. Verified by restoring the old access pattern under the new concurrency test, which reports data races and fails. Also replaces the fmt.Printf calls with the package logger, which they should have been using — a library should not write to stdout — and fixes the counter path reporting its failures as "float64 histogram".
Vadman97
marked this pull request as ready for review
August 19, 2026 23:57
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.
Summary
Follow-up to launchdarkly/observability#1780. Investigating why
otel.ingest.timestamp_skew_secwas unusable in ClickHouse bottomed out in two defects here.1. Instruments could not be configured.
RecordHistogramcalleddefaultMeter.Float64Histogram(name)with no options, so every histogram inherited OTEL's default boundaries —[0, 5, …, 10000], shaped for millisecond latencies. A metric on any other scale collapses entirely into the+Infoverflow bucket and no quantile is computable. The metric that prompted this only ever emits values ≥7200 (it measures a ±2h clamp), so 94% of its observations sat in the overflow bucket and 13 of its 16 buckets were empty by construction. Unit and description were unreachable for the same reason, so it exported with an empty unit despite a_secname.Each
Record*function now has aWithOptionsvariant taking the matching OTEL instrument options, wired throughhmetrictoo since that's the wrapper callers actually use:Options are read when the instrument is first built for a name and ignored afterwards — that's inherent to memoizing instruments, and the doc comments say so.
2. The instrument caches were read without a lock. Every
Record*function took the mutex to build the instrument, released it, then indexed the map again to record:That's an unsynchronized map read against a concurrent writer. Go can escalate it from a data race to a fatal
concurrent map read and map write, which takes down the process — in a telemetry SDK, on a path any instrumented service hits under load.The three copy-pasted blocks are now one generic
instrumentCachethat returns the instrument it looked up, with proper double-checked locking. Instruments stay lazily built:defaultMeteris a noop untilStartOTLPinstalls the real provider, so building eagerly would permanently cache an instrument that discards every measurement.Also: replaced the
fmt.Printfcalls with the package logger — a library shouldn't write to stdout — and fixed the counter path reporting its failures as"float64 histogram".Compatibility
Additive.
RecordMetric,RecordHistogram,RecordCountand the threehmetricwrappers keep their signatures and now delegate to theWithOptionsvariants.How did you test this change?
go test ./... -race -count=1insdk/highlight-go. Newotel_metrics_test.godrives the realsdkmetricpipeline through aManualReaderand asserts on what would actually be exported:+Infbucket (documents the problem)I verified that last test is meaningful by restoring the old unsynchronized access pattern and re-running it:
WARNING: DATA RACE×6 and a failure. With the fix, clean.TestParseConsoleMessagesinsdk/highlight-go/logfails, but it fails identically on unmodifiedorigin/main— a JSON type mismatch in the console-message parser, unrelated to this change.Follow-up
Once released and the submodule is bumped, the observability backend can declare boundaries for
otel.ingest.timestamp_skew_secand a unit/description forotel.ingest.timestamp_clamped.🤖 Generated with Claude Code
Note
Overview
Adds
*WithOptionshelpers inhighlight-goandhmetricso histograms, counters, and gauges can be created with OTEL options (explicit bucket boundaries, unit, description). ExistingRecord*/Histogram/Incr/GaugeAPIs are unchanged and delegate to those variants.Replaces the three per-instrument maps and hand-rolled locking with a generic
instrumentCachethat uses double-checked locking and returns the built instrument fromget, fixing unsynchronized map reads after unlock that could race under concurrent first use. Instrument creation errors now go through the package logger instead offmt.Printf.New
otel_metrics_test.goexercises default vs explicit histogram bounds, metadata on export, first-call-wins configuration, and concurrent first-use safety via aManualReader.Reviewed by Cursor Bugbot for commit 394106c. Bugbot is set up for automated code reviews on this repo. Configure here.