Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b2c8f8e
feat(metric): Add merge capabilities to expoBuckets and expoHistogram…
dashpole May 27, 2026
d73c4d5
feat(metric): Implement deltaExpoHistogram with double buffering
dashpole May 27, 2026
e8f2d68
feat(metric): Implement cumulativeExpoHistogram with double buffering
dashpole May 27, 2026
1ce34bd
test(metric): Add tests for exponential histogram underflow and NaN/I…
dashpole May 27, 2026
619f9b1
refactor(metric): Simplify newExpoHistogramDataPoint and restore comm…
dashpole May 27, 2026
2454e5b
fix(metric): resolve rebase conflicts and optimize buffer reuse for e…
dashpole Jun 11, 2026
1be1485
Potential fix for pull request finding
dashpole Jun 12, 2026
098dd1e
refactor(sdk/metric): reuse quiescent cold point and preserve cumulat…
dashpole Jul 9, 2026
c32bb2c
fix(sdk/metric): reuse existing capacity in expoBuckets.recordCount (…
dashpole Jul 10, 2026
e8e4cd9
fix(sdk/metric): track aggregation limit overflow separately from ove…
dashpole Jul 13, 2026
dc6ebed
fix(sdk/metric): slice hDPts to visited count in cumulative exp-histo…
dashpole Jul 31, 2026
2b92b51
refactor(sdk/metric): remove redundant int32 conversion for scale bou…
dashpole Jul 31, 2026
ed9df04
refactor(sdk/metric): remove unused fields from doubleBufferedCumulat…
dashpole Jul 31, 2026
0a1f9d3
refactor(sdk/metric): align exemplar reservoir checks across measure …
dashpole Jul 31, 2026
d63fa84
test(sdk/metric): restore TestExponentialHistogramMinMaxUnset for unr…
dashpole Jul 31, 2026
d33339c
docs(sdk/metric): add inline documentation for exponential histogram …
dashpole Jul 31, 2026
8057bcb
fix(sdk/metric): fix unit tests and linter for exponential histogram
dashpole Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Fixed

- Prevent underflowed measurements from updating sum, min, or max in exponential histograms in `go.opentelemetry.io/otel/sdk/metric`. (#8077)
- The simple span and log processors record `otel.sdk.processor.{span,log}.processed` when the record is submitted to the exporter instead of after the export completes, and no longer set `error.type` from the export outcome, in `go.opentelemetry.io/otel/sdk/trace` and `go.opentelemetry.io/otel/sdk/log`. (#8705)

<!-- Released section -->
Expand Down
14 changes: 11 additions & 3 deletions sdk/metric/internal/aggregate/aggregate.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,20 @@ func (b Builder[N]) ExponentialBucketHistogram(
maxSize, maxScale int32,
noMinMax, noSum bool,
) (Measure[N], ComputeAggregation) {
h := newExponentialHistogram[N](maxSize, maxScale, noMinMax, noSum, b.AggregationLimit, b.resFunc())
switch b.Temporality {
case metricdata.DeltaTemporality:
return b.filter(h.measure), h.delta
h := newDeltaExpoHistogram[N](maxSize, maxScale, noMinMax, noSum, b.AggregationLimit, b.resFunc())
return b.filter(h.measure), h.collect
default:
return b.filter(h.measure), h.cumulative
measure, collect := newCumulativeExpoHistogram[N](
maxSize,
maxScale,
noMinMax,
noSum,
b.AggregationLimit,
b.resFunc(),
)
return b.filter(measure), collect
}
}

Expand Down
20 changes: 15 additions & 5 deletions sdk/metric/internal/aggregate/atomic.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ func (s *atomicMinMax[N]) Update(val N) {
}
}

// reset resets the internal state, and is not safe to call concurrently.
func (s *atomicMinMax[N]) reset() {
s.set.Store(false)
}

// hotColdWaitGroup is a synchronization primitive which enables lockless
// writes for concurrent writers and enables a reader to acquire exclusive
// access to a snapshot of state including only completed operations.
Expand Down Expand Up @@ -226,6 +231,7 @@ type limitedSyncMap[V any] struct {
aggLimit int
len int
lenMux sync.Mutex
overflow atomic.Bool
}

// LoadOrStoreAttr performs lookup using lazy.Distinct() on the hot path without
Expand All @@ -238,11 +244,13 @@ func (m *limitedSyncMap[V]) LoadOrStoreAttr(lazy lazyFilteredAttributes, newValu
if loaded {
return actual.(V)
}
// If the overflow set exists, assume we have already overflowed and don't
// bother with the slow path below.
actual, loaded = m.Load(overflowSet.Equivalent())
if loaded {
return actual.(V)
// If aggregation overflow has already happened due to exceeding the limit,
// any new attribute set will be aggregated into the overflow set.
if m.aggLimit > 0 && m.overflow.Load() {
actual, loaded = m.Load(overflowSet.Equivalent())
if loaded {
return actual.(V)
}
}
// Slow path: add a new attribute set.
m.lenMux.Lock()
Expand All @@ -260,6 +268,7 @@ func (m *limitedSyncMap[V]) LoadOrStoreAttr(lazy lazyFilteredAttributes, newValu
if m.aggLimit > 0 && m.len >= m.aggLimit-1 {
fltrAttr = overflowSet
distinct = overflowSet.Equivalent()
m.overflow.Store(true)
} else {
fltrAttr = lazy.Set()
}
Expand All @@ -273,6 +282,7 @@ func (m *limitedSyncMap[V]) LoadOrStoreAttr(lazy lazyFilteredAttributes, newValu
func (m *limitedSyncMap[V]) Clear() {
m.lenMux.Lock()
defer m.lenMux.Unlock()
m.overflow.Store(false)
m.len = 0
m.Map.Clear()
}
Expand Down
36 changes: 36 additions & 0 deletions sdk/metric/internal/aggregate/atomic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,42 @@ func TestLimitedSyncMapLimit(t *testing.T) {
assert.Same(t, v7, v8, "Subsequent keys should return same overflow value")
}

func TestLimitedSyncMapOverflowAttributeBeforeLimit(t *testing.T) {
newValue := func(attribute.Set) any { return new(int) }

t.Run("NoLimit", func(t *testing.T) {
var m limitedSyncMap[any]
v1 := loadOrStore(&m, overflowSet, newValue)
assert.Equal(t, 1, m.Len())

attr := attribute.NewSet(attribute.String("key", "1"))
v2 := loadOrStore(&m, attr, newValue)
assert.Equal(t, 2, m.Len())
assert.NotSame(t, v1, v2)
})

t.Run("WithLimit", func(t *testing.T) {
m := limitedSyncMap[any]{aggLimit: 3}
v1 := loadOrStore(&m, overflowSet, newValue)
assert.Equal(t, 1, m.Len())

attr2 := attribute.NewSet(attribute.String("key", "2"))
v2 := loadOrStore(&m, attr2, newValue)
assert.Equal(t, 2, m.Len())
assert.NotSame(t, v1, v2)

attr3 := attribute.NewSet(attribute.String("key", "3"))
v3 := loadOrStore(&m, attr3, newValue)
assert.Equal(t, 2, m.Len())
assert.Same(t, v1, v3)

attr4 := attribute.NewSet(attribute.String("key", "4"))
v4 := loadOrStore(&m, attr4, newValue)
assert.Equal(t, 2, m.Len())
assert.Same(t, v3, v4)
})
}

func TestLimitedSyncMapConcurrentSafe(t *testing.T) {
m := limitedSyncMap[any]{aggLimit: 5}
newValue := func(attribute.Set) any { return 1 }
Expand Down
Loading
Loading