Skip to content

Commit 1492efa

Browse files
pree-dewpellared
andauthored
Fix incorrect metrics getting generated from multiple readers (#5900)
Fixes #5866 --------- Co-authored-by: Robert Pająk <[email protected]>
1 parent d2b0663 commit 1492efa

File tree

4 files changed

+171
-31
lines changed

4 files changed

+171
-31
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
3535
- Fix `WithEndpointURL` to always use a secure connection when an https URL is passed in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#5944)
3636
- Fix `WithEndpointURL` to always use a secure connection when an https URL is passed in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. (#5944)
3737
- Fix `WithEndpointURL` to always use a secure connection when an https URL is passed in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#5944)
38+
- Fix incorrect metrics generated from callbacks when multiple readers are used in `go.opentelemetry.io/otel/sdk/metric`. (#5900)
3839

3940
### Changed
4041

sdk/metric/meter.go

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@ func (m *meter) int64ObservableInstrument(id Instrument, callbacks []metric.Int6
150150
continue
151151
}
152152
inst.appendMeasures(in)
153+
154+
// Add the measures to the pipeline. It is required to maintain
155+
// measures per pipeline to avoid calling the measure that
156+
// is not part of the pipeline.
157+
insert.pipeline.addInt64Measure(inst.observableID, in)
153158
for _, cback := range callbacks {
154159
inst := int64Observer{measures: in}
155160
fn := cback
@@ -309,6 +314,11 @@ func (m *meter) float64ObservableInstrument(id Instrument, callbacks []metric.Fl
309314
continue
310315
}
311316
inst.appendMeasures(in)
317+
318+
// Add the measures to the pipeline. It is required to maintain
319+
// measures per pipeline to avoid calling the measure that
320+
// is not part of the pipeline.
321+
insert.pipeline.addFloat64Measure(inst.observableID, in)
312322
for _, cback := range callbacks {
313323
inst := float64Observer{measures: in}
314324
fn := cback
@@ -441,8 +451,8 @@ func (m *meter) RegisterCallback(f metric.Callback, insts ...metric.Observable)
441451
return noopRegister{}, nil
442452
}
443453

444-
reg := newObserver()
445454
var err error
455+
validInstruments := make([]metric.Observable, 0, len(insts))
446456
for _, inst := range insts {
447457
switch o := inst.(type) {
448458
case int64Observable:
@@ -452,49 +462,64 @@ func (m *meter) RegisterCallback(f metric.Callback, insts ...metric.Observable)
452462
}
453463
continue
454464
}
455-
reg.registerInt64(o.observableID)
465+
466+
validInstruments = append(validInstruments, inst)
456467
case float64Observable:
457468
if e := o.registerable(m); e != nil {
458469
if !errors.Is(e, errEmptyAgg) {
459470
err = errors.Join(err, e)
460471
}
461472
continue
462473
}
463-
reg.registerFloat64(o.observableID)
474+
475+
validInstruments = append(validInstruments, inst)
464476
default:
465477
// Instrument external to the SDK.
466478
return nil, fmt.Errorf("invalid observable: from different implementation")
467479
}
468480
}
469481

470-
if reg.len() == 0 {
482+
if len(validInstruments) == 0 {
471483
// All insts use drop aggregation or are invalid.
472484
return noopRegister{}, err
473485
}
474486

475-
// Some or all instruments were valid.
476-
cback := func(ctx context.Context) error { return f(ctx, reg) }
477-
return m.pipes.registerMultiCallback(cback), err
487+
unregs := make([]func(), len(m.pipes))
488+
for ix, pipe := range m.pipes {
489+
reg := newObserver(pipe)
490+
for _, inst := range validInstruments {
491+
switch o := inst.(type) {
492+
case int64Observable:
493+
reg.registerInt64(o.observableID)
494+
case float64Observable:
495+
reg.registerFloat64(o.observableID)
496+
}
497+
}
498+
499+
// Some or all instruments were valid.
500+
cBack := func(ctx context.Context) error { return f(ctx, reg) }
501+
unregs[ix] = pipe.addMultiCallback(cBack)
502+
}
503+
504+
return unregisterFuncs{f: unregs}, err
478505
}
479506

480507
type observer struct {
481508
embedded.Observer
482509

510+
pipe *pipeline
483511
float64 map[observableID[float64]]struct{}
484512
int64 map[observableID[int64]]struct{}
485513
}
486514

487-
func newObserver() observer {
515+
func newObserver(p *pipeline) observer {
488516
return observer{
517+
pipe: p,
489518
float64: make(map[observableID[float64]]struct{}),
490519
int64: make(map[observableID[int64]]struct{}),
491520
}
492521
}
493522

494-
func (r observer) len() int {
495-
return len(r.float64) + len(r.int64)
496-
}
497-
498523
func (r observer) registerFloat64(id observableID[float64]) {
499524
r.float64[id] = struct{}{}
500525
}
@@ -530,7 +555,12 @@ func (r observer) ObserveFloat64(o metric.Float64Observable, v float64, opts ...
530555
return
531556
}
532557
c := metric.NewObserveConfig(opts)
533-
oImpl.observe(v, c.Attributes())
558+
// Access to r.pipe.float64Measure is already guarded by a lock in pipeline.produce.
559+
// TODO (#5946): Refactor pipeline and observable measures.
560+
measures := r.pipe.float64Measures[oImpl.observableID]
561+
for _, m := range measures {
562+
m(context.Background(), v, c.Attributes())
563+
}
534564
}
535565

536566
func (r observer) ObserveInt64(o metric.Int64Observable, v int64, opts ...metric.ObserveOption) {
@@ -555,7 +585,12 @@ func (r observer) ObserveInt64(o metric.Int64Observable, v int64, opts ...metric
555585
return
556586
}
557587
c := metric.NewObserveConfig(opts)
558-
oImpl.observe(v, c.Attributes())
588+
// Access to r.pipe.int64Measures is already guarded b a lock in pipeline.produce.
589+
// TODO (#5946): Refactor pipeline and observable measures.
590+
measures := r.pipe.int64Measures[oImpl.observableID]
591+
for _, m := range measures {
592+
m(context.Background(), v, c.Attributes())
593+
}
559594
}
560595

561596
type noopRegister struct{ embedded.Registration }

sdk/metric/pipeline.go

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"sync/atomic"
1313

1414
"go.opentelemetry.io/otel/internal/global"
15-
"go.opentelemetry.io/otel/metric"
1615
"go.opentelemetry.io/otel/metric/embedded"
1716
"go.opentelemetry.io/otel/sdk/instrumentation"
1817
"go.opentelemetry.io/otel/sdk/metric/exemplar"
@@ -43,10 +42,12 @@ func newPipeline(res *resource.Resource, reader Reader, views []View, exemplarFi
4342
res = resource.Empty()
4443
}
4544
return &pipeline{
46-
resource: res,
47-
reader: reader,
48-
views: views,
49-
exemplarFilter: exemplarFilter,
45+
resource: res,
46+
reader: reader,
47+
views: views,
48+
int64Measures: map[observableID[int64]][]aggregate.Measure[int64]{},
49+
float64Measures: map[observableID[float64]][]aggregate.Measure[float64]{},
50+
exemplarFilter: exemplarFilter,
5051
// aggregations is lazy allocated when needed.
5152
}
5253
}
@@ -64,10 +65,26 @@ type pipeline struct {
6465
views []View
6566

6667
sync.Mutex
67-
aggregations map[instrumentation.Scope][]instrumentSync
68-
callbacks []func(context.Context) error
69-
multiCallbacks list.List
70-
exemplarFilter exemplar.Filter
68+
int64Measures map[observableID[int64]][]aggregate.Measure[int64]
69+
float64Measures map[observableID[float64]][]aggregate.Measure[float64]
70+
aggregations map[instrumentation.Scope][]instrumentSync
71+
callbacks []func(context.Context) error
72+
multiCallbacks list.List
73+
exemplarFilter exemplar.Filter
74+
}
75+
76+
// addInt64Measure adds a new int64 measure to the pipeline for each observer.
77+
func (p *pipeline) addInt64Measure(id observableID[int64], m []aggregate.Measure[int64]) {
78+
p.Lock()
79+
defer p.Unlock()
80+
p.int64Measures[id] = m
81+
}
82+
83+
// addFloat64Measure adds a new float64 measure to the pipeline for each observer.
84+
func (p *pipeline) addFloat64Measure(id observableID[float64], m []aggregate.Measure[float64]) {
85+
p.Lock()
86+
defer p.Unlock()
87+
p.float64Measures[id] = m
7188
}
7289

7390
// addSync adds the instrumentSync to pipeline p with scope. This method is not
@@ -574,14 +591,6 @@ func newPipelines(res *resource.Resource, readers []Reader, views []View, exempl
574591
return pipes
575592
}
576593

577-
func (p pipelines) registerMultiCallback(c multiCallback) metric.Registration {
578-
unregs := make([]func(), len(p))
579-
for i, pipe := range p {
580-
unregs[i] = pipe.addMultiCallback(c)
581-
}
582-
return unregisterFuncs{f: unregs}
583-
}
584-
585594
type unregisterFuncs struct {
586595
embedded.Registration
587596
f []func()

sdk/metric/pipeline_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"runtime"
1212
"strings"
1313
"sync"
14+
"sync/atomic"
1415
"testing"
1516

1617
"github.com/go-logr/logr"
@@ -24,6 +25,7 @@ import (
2425
"go.opentelemetry.io/otel/metric"
2526
"go.opentelemetry.io/otel/sdk/instrumentation"
2627
"go.opentelemetry.io/otel/sdk/metric/exemplar"
28+
"go.opentelemetry.io/otel/sdk/metric/internal/aggregate"
2729
"go.opentelemetry.io/otel/sdk/metric/metricdata"
2830
"go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest"
2931
"go.opentelemetry.io/otel/sdk/resource"
@@ -101,6 +103,21 @@ func TestPipelineConcurrentSafe(t *testing.T) {
101103
defer wg.Done()
102104
pipe.addMultiCallback(func(context.Context) error { return nil })
103105
}()
106+
107+
wg.Add(1)
108+
go func() {
109+
defer wg.Done()
110+
b := aggregate.Builder[int64]{
111+
Temporality: metricdata.CumulativeTemporality,
112+
ReservoirFunc: nil,
113+
AggregationLimit: 0,
114+
}
115+
var oID observableID[int64]
116+
m, _ := b.PrecomputedSum(false)
117+
measures := []aggregate.Measure[int64]{}
118+
measures = append(measures, m)
119+
pipe.addInt64Measure(oID, measures)
120+
}()
104121
}
105122
wg.Wait()
106123
}
@@ -518,3 +535,81 @@ func TestExemplars(t *testing.T) {
518535
check(t, r, 2, 2, 2)
519536
})
520537
}
538+
539+
func TestAddingAndObservingMeasureConcurrentSafe(t *testing.T) {
540+
r1 := NewManualReader()
541+
r2 := NewManualReader()
542+
543+
mp := NewMeterProvider(WithReader(r1), WithReader(r2))
544+
m := mp.Meter("test")
545+
546+
oc1, err := m.Int64ObservableCounter("int64-observable-counter")
547+
require.NoError(t, err)
548+
549+
wg := sync.WaitGroup{}
550+
wg.Add(1)
551+
go func() {
552+
defer wg.Done()
553+
_, err := m.Int64ObservableCounter("int64-observable-counter-2")
554+
require.NoError(t, err)
555+
}()
556+
557+
wg.Add(1)
558+
go func() {
559+
defer wg.Done()
560+
_, err := m.RegisterCallback(
561+
func(_ context.Context, o metric.Observer) error {
562+
o.ObserveInt64(oc1, 2)
563+
return nil
564+
}, oc1)
565+
require.NoError(t, err)
566+
}()
567+
568+
wg.Add(1)
569+
go func() {
570+
defer wg.Done()
571+
_ = mp.pipes[0].produce(context.Background(), &metricdata.ResourceMetrics{})
572+
}()
573+
574+
wg.Add(1)
575+
go func() {
576+
defer wg.Done()
577+
_ = mp.pipes[1].produce(context.Background(), &metricdata.ResourceMetrics{})
578+
}()
579+
580+
wg.Wait()
581+
}
582+
583+
func TestPipelineWithMultipleReaders(t *testing.T) {
584+
r1 := NewManualReader()
585+
r2 := NewManualReader()
586+
mp := NewMeterProvider(WithReader(r1), WithReader(r2))
587+
m := mp.Meter("test")
588+
var val atomic.Int64
589+
oc, err := m.Int64ObservableCounter("int64-observable-counter")
590+
require.NoError(t, err)
591+
reg, err := m.RegisterCallback(
592+
// SDK calls this function when collecting data.
593+
func(_ context.Context, o metric.Observer) error {
594+
o.ObserveInt64(oc, val.Load())
595+
return nil
596+
}, oc)
597+
require.NoError(t, err)
598+
t.Cleanup(func() { assert.NoError(t, reg.Unregister()) })
599+
ctx := context.Background()
600+
rm := new(metricdata.ResourceMetrics)
601+
val.Add(1)
602+
err = r1.Collect(ctx, rm)
603+
require.NoError(t, err)
604+
if assert.Len(t, rm.ScopeMetrics, 1) &&
605+
assert.Len(t, rm.ScopeMetrics[0].Metrics, 1) {
606+
assert.Equal(t, int64(1), rm.ScopeMetrics[0].Metrics[0].Data.(metricdata.Sum[int64]).DataPoints[0].Value)
607+
}
608+
val.Add(1)
609+
err = r2.Collect(ctx, rm)
610+
require.NoError(t, err)
611+
if assert.Len(t, rm.ScopeMetrics, 1) &&
612+
assert.Len(t, rm.ScopeMetrics[0].Metrics, 1) {
613+
assert.Equal(t, int64(2), rm.ScopeMetrics[0].Metrics[0].Data.(metricdata.Sum[int64]).DataPoints[0].Value)
614+
}
615+
}

0 commit comments

Comments
 (0)