1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2026-06-19 21:45:50 +02:00

Add WithUnsafeAttributes to support no-copy dynamic metric API calls (#8251)

Split from https://github.com/open-telemetry/opentelemetry-go/pull/8179

Part of https://github.com/open-telemetry/opentelemetry-go/issues/7743

This just defines the new WithUnsafeAttributes option, but converts it
to a set, and leaves the optimization of that path for a follow-up.

Benchmarks

```
goos: linux
goarch: amd64
pkg: go.opentelemetry.io/otel/sdk/metric
cpu: Intel(R) Xeon(R) CPU @ 2.20GHz
                                                                              │   out.txt    │
                                                                              │    sec/op    │
EndToEndCounterAdd/NoFilter/Attributes/1/Precomputed/WithAttributeSet-24        65.18n ± 25%
EndToEndCounterAdd/NoFilter/Attributes/1/Precomputed/WithAttributes-24          61.96n ± 19%
EndToEndCounterAdd/NoFilter/Attributes/1/Precomputed/WithUnsafeAttributes-24    199.9n ± 22%
EndToEndCounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributeSet-24            231.6n ±  9%
EndToEndCounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributes-24              288.0n ± 14%
EndToEndCounterAdd/NoFilter/Attributes/1/Dynamic/WithUnsafeAttributes-24        271.9n ±  6%
EndToEndCounterAdd/NoFilter/Attributes/1/Naive/WithAttributes-24                352.7n ± 14%
EndToEndCounterAdd/NoFilter/Attributes/5/Precomputed/WithAttributeSet-24        68.73n ± 22%
EndToEndCounterAdd/NoFilter/Attributes/5/Precomputed/WithAttributes-24          61.04n ± 22%
EndToEndCounterAdd/NoFilter/Attributes/5/Precomputed/WithUnsafeAttributes-24    754.0n ± 25%
EndToEndCounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributeSet-24            637.3n ± 21%
EndToEndCounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributes-24              908.2n ± 28%
EndToEndCounterAdd/NoFilter/Attributes/5/Dynamic/WithUnsafeAttributes-24        901.7n ± 14%
EndToEndCounterAdd/NoFilter/Attributes/5/Naive/WithAttributes-24                1.062µ ± 15%
EndToEndCounterAdd/NoFilter/Attributes/10/Precomputed/WithAttributeSet-24       53.82n ± 28%
EndToEndCounterAdd/NoFilter/Attributes/10/Precomputed/WithAttributes-24         65.82n ±  5%
EndToEndCounterAdd/NoFilter/Attributes/10/Precomputed/WithUnsafeAttributes-24   1.717µ ± 14%
EndToEndCounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributeSet-24           1.183µ ± 13%
EndToEndCounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributes-24             1.803µ ± 20%
EndToEndCounterAdd/NoFilter/Attributes/10/Dynamic/WithUnsafeAttributes-24       2.050µ ± 15%
EndToEndCounterAdd/NoFilter/Attributes/10/Naive/WithAttributes-24               2.082µ ± 33%
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithAttributeSet-24        204.1n ± 14%
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithAttributes-24          214.5n ± 28%
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithUnsafeAttributes-24    433.9n ± 18%
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributeSet-24            412.6n ± 10%
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributes-24              464.1n ± 18%
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithUnsafeAttributes-24        471.4n ± 13%
EndToEndCounterAdd/Filtered/Attributes/1/Naive/WithAttributes-24                558.5n ± 19%
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithAttributeSet-24        772.4n ± 14%
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithAttributes-24          748.7n ±  9%
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithUnsafeAttributes-24    1.453µ ± 17%
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributeSet-24            1.501µ ± 19%
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributes-24              1.489µ ±  9%
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithUnsafeAttributes-24        1.608µ ± 24%
EndToEndCounterAdd/Filtered/Attributes/5/Naive/WithAttributes-24                1.671µ ± 14%
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithAttributeSet-24       1.324µ ± 18%
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithAttributes-24         1.463µ ± 17%
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithUnsafeAttributes-24   2.833µ ± 33%
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributeSet-24           2.635µ ±  9%
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributes-24             3.125µ ± 14%
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithUnsafeAttributes-24       2.685µ ± 24%
EndToEndCounterAdd/Filtered/Attributes/10/Naive/WithAttributes-24               3.404µ ±  8%
geomean                                                                         610.5n

                                                                              │    out.txt     │
                                                                              │      B/op      │
EndToEndCounterAdd/NoFilter/Attributes/1/Precomputed/WithAttributeSet-24          0.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/1/Precomputed/WithAttributes-24            0.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/1/Precomputed/WithUnsafeAttributes-24      128.0 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributeSet-24              88.00 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributes-24                152.0 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/1/Dynamic/WithUnsafeAttributes-24          128.0 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/1/Naive/WithAttributes-24                  232.0 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/5/Precomputed/WithAttributeSet-24          0.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/5/Precomputed/WithAttributes-24            0.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/5/Precomputed/WithUnsafeAttributes-24      640.0 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributeSet-24              344.0 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributes-24                664.0 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/5/Dynamic/WithUnsafeAttributes-24          640.0 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/5/Naive/WithAttributes-24                 1000.0 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/10/Precomputed/WithAttributeSet-24         0.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/10/Precomputed/WithAttributes-24           0.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/10/Precomputed/WithUnsafeAttributes-24   1.375Ki ± 0%
EndToEndCounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributeSet-24             729.0 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributes-24             1.399Ki ± 0%
EndToEndCounterAdd/NoFilter/Attributes/10/Dynamic/WithUnsafeAttributes-24       1.376Ki ± 0%
EndToEndCounterAdd/NoFilter/Attributes/10/Naive/WithAttributes-24               2.102Ki ± 0%
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithAttributeSet-24          64.00 ± 0%
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithAttributes-24            64.00 ± 0%
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithUnsafeAttributes-24      192.0 ± 0%
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributeSet-24              152.0 ± 0%
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributes-24                216.0 ± 0%
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithUnsafeAttributes-24          192.0 ± 0%
EndToEndCounterAdd/Filtered/Attributes/1/Naive/WithAttributes-24                  296.0 ± 0%
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithAttributeSet-24          576.0 ± 0%
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithAttributes-24            576.0 ± 0%
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithUnsafeAttributes-24    1.188Ki ± 0%
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributeSet-24              921.0 ± 0%
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributes-24              1.212Ki ± 0%
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithUnsafeAttributes-24        1.188Ki ± 0%
EndToEndCounterAdd/Filtered/Attributes/5/Naive/WithAttributes-24                1.539Ki ± 0%
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithAttributeSet-24       1.312Ki ± 0%
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithAttributes-24         1.312Ki ± 0%
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithUnsafeAttributes-24   2.688Ki ± 0%
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributeSet-24           2.025Ki ± 0%
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributes-24             2.713Ki ± 0%
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithUnsafeAttributes-24       2.689Ki ± 0%
EndToEndCounterAdd/Filtered/Attributes/10/Naive/WithAttributes-24               3.414Ki ± 0%
geomean                                                                                      ¹
¹ summaries must be >0 to compute geomean

                                                                              │   out.txt    │
                                                                              │  allocs/op   │
EndToEndCounterAdd/NoFilter/Attributes/1/Precomputed/WithAttributeSet-24        0.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/1/Precomputed/WithAttributes-24          0.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/1/Precomputed/WithUnsafeAttributes-24    2.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributeSet-24            2.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributes-24              3.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/1/Dynamic/WithUnsafeAttributes-24        2.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/1/Naive/WithAttributes-24                5.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/5/Precomputed/WithAttributeSet-24        0.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/5/Precomputed/WithAttributes-24          0.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/5/Precomputed/WithUnsafeAttributes-24    2.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributeSet-24            2.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributes-24              3.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/5/Dynamic/WithUnsafeAttributes-24        2.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/5/Naive/WithAttributes-24                5.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/10/Precomputed/WithAttributeSet-24       0.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/10/Precomputed/WithAttributes-24         0.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/10/Precomputed/WithUnsafeAttributes-24   2.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributeSet-24           2.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributes-24             3.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/10/Dynamic/WithUnsafeAttributes-24       2.000 ± 0%
EndToEndCounterAdd/NoFilter/Attributes/10/Naive/WithAttributes-24               5.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithAttributeSet-24        1.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithAttributes-24          1.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithUnsafeAttributes-24    3.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributeSet-24            3.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributes-24              4.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithUnsafeAttributes-24        3.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/1/Naive/WithAttributes-24                6.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithAttributeSet-24        2.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithAttributes-24          2.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithUnsafeAttributes-24    4.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributeSet-24            4.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributes-24              5.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithUnsafeAttributes-24        4.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/5/Naive/WithAttributes-24                7.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithAttributeSet-24       2.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithAttributes-24         2.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithUnsafeAttributes-24   4.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributeSet-24           4.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributes-24             5.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithUnsafeAttributes-24       4.000 ± 0%
EndToEndCounterAdd/Filtered/Attributes/10/Naive/WithAttributes-24               7.000 ± 0%
geomean                                                                                    ¹
¹ summaries must be >0 to compute geomean
```
This commit is contained in:
David Ashpole
2026-06-11 14:33:54 -04:00
committed by GitHub
parent fbf2027dc8
commit 6e3d3448fc
7 changed files with 624 additions and 8 deletions
+4
View File
@@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased]
### Added
- Add `WithUnsafeAttributes` to `go.opentelemetry.io/otel/metric/x` as an experimental no-copy attribute option intended for future performance work. This is a work in progress. (#8251)
### Fixed
- Avoid preallocating scope attributes when they are disabled in `go.opentelemetry.io/otel/exporters/prometheus` . (#8404)
+33
View File
@@ -28,6 +28,39 @@ func WithDefaultAttributes(keys ...attribute.Key) metric.InstrumentOption {
return defaultAttributesOption{keys: keys}
}
type unsafeAttributesOption struct {
metric.MeasurementOption
kvs []attribute.KeyValue
}
// Experimental prevents the API from panicking when the option is used.
func (*unsafeAttributesOption) Experimental() {}
// RawAttributes returns the raw key-values associated with the option.
func (o *unsafeAttributesOption) RawAttributes() []attribute.KeyValue {
return o.kvs
}
// Settable implements the x.Settable interface.
// As with all Settable implementations, Set must not be called concurrently
// with the usage of the option in an instrument call (e.g., counter.Add)
// or by other goroutines.
func (o *unsafeAttributesOption) Set(kvs []attribute.KeyValue) {
o.kvs = kvs
}
// WithUnsafeAttributes returns a metric.MeasurementOption that stores the raw attributes
// and associates them with a measurement without making a copy.
// The caller must not modify the attributes slice after passing it to this function.
// This is a work-in-progress, and does not yet have better performance than metric.WithAttributeSet.
//
// Attributes passed via WithUnsafeAttributes are merged with attributes from standard options
// (e.g., metric.WithAttributes). If duplicate keys exist, attributes from WithUnsafeAttributes
// take precedence and will override standard attributes, regardless of the order they are passed.
func WithUnsafeAttributes(kvs ...attribute.KeyValue) metric.MeasurementOption {
return &unsafeAttributesOption{kvs: kvs}
}
// Settable is an optional interface that Options can implement
// to allow reuse without additional allocations.
//
+52
View File
@@ -0,0 +1,52 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package x
import (
"testing"
"go.opentelemetry.io/otel/attribute"
)
func TestWithUnsafeAttributes(t *testing.T) {
kvs := []attribute.KeyValue{attribute.String("A", "B")}
opt := WithUnsafeAttributes(kvs...)
unsafeOpt, ok := opt.(*unsafeAttributesOption)
if !ok {
t.Fatalf("expected *unsafeAttributesOption")
}
attrs := unsafeOpt.RawAttributes()
if len(attrs) != 1 {
t.Errorf("expected 1 attribute, got %d", len(attrs))
}
if attrs[0].Key != "A" || attrs[0].Value.AsString() != "B" {
t.Errorf("expected attribute A='B', got %v", attrs[0])
}
}
func TestUnsafeAttributesOption_Set(t *testing.T) {
opt := WithUnsafeAttributes()
kvs := []attribute.KeyValue{attribute.String("C", "D")}
settable, ok := opt.(Settable[[]attribute.KeyValue])
if !ok {
t.Fatalf("expected Settable[[]attribute.KeyValue]")
}
settable.Set(kvs)
unsafeOpt, ok := opt.(*unsafeAttributesOption)
if !ok {
t.Fatalf("expected *unsafeAttributesOption")
}
attrs := unsafeOpt.RawAttributes()
if len(attrs) != 1 {
t.Errorf("expected 1 attribute, got %d", len(attrs))
}
if attrs[0].Key != "C" || attrs[0].Value.AsString() != "D" {
t.Errorf("expected attribute C='D', got %v", attrs[0])
}
}
+39
View File
@@ -624,6 +624,16 @@ func BenchmarkEndToEndCounterAdd(b *testing.B) {
}
})
})
b.Run("Precomputed/WithUnsafeAttributes", func(b *testing.B) {
counter := testCounter(b, mp.provider())
precomputedOpts := []metric.AddOption{x.WithUnsafeAttributes(attributes(attrsLen)...)}
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
counter.Add(ctx, 1, precomputedOpts...)
}
})
})
// This case shows the performance of our API + SDK when
// following our contributor guidance for recording
// varying attributes by passing attribute.Set:
@@ -716,6 +726,35 @@ func BenchmarkEndToEndCounterAdd(b *testing.B) {
}
})
})
b.Run("Dynamic/WithUnsafeAttributes", func(b *testing.B) {
counter := testCounter(b, mp.provider())
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
opt := x.WithUnsafeAttributes()
for pb.Next() {
func() {
attrsSlice := attrPool.Get().(*[]attribute.KeyValue)
defer func() {
*attrsSlice = (*attrsSlice)[:0] // Reset.
attrPool.Put(attrsSlice)
}()
*attrsSlice = appendAttributes(*attrsSlice, attrsLen)
addOpt := addOptPool.Get().(*[]metric.AddOption)
defer func() {
*addOpt = (*addOpt)[:0]
addOptPool.Put(addOpt)
}()
if settable, ok := opt.(x.Settable[[]attribute.KeyValue]); ok {
settable.Set(*attrsSlice)
} else {
opt = x.WithUnsafeAttributes(*attrsSlice...)
}
*addOpt = append(*addOpt, opt)
counter.Add(ctx, 1, *addOpt...)
}()
}
})
})
// This case shows the performance of our API + SDK when
// users use it in the "obvious" way, without explicitly
// trying to optimize for performance.
+46 -4
View File
@@ -180,6 +180,44 @@ func (i instID) normalize() instID {
return i
}
type rawAttributesOption interface {
RawAttributes() []attribute.KeyValue
Experimental()
}
func extractRawKVs[T any](opts []T) []attribute.KeyValue {
var rawKVs []attribute.KeyValue
var count int
for _, opt := range opts {
if r, ok := any(opt).(rawAttributesOption); ok {
count++
if count == 1 {
rawKVs = r.RawAttributes()
} else {
if count == 2 {
// Create a new slice to avoid modifying the original slice from the first option.
rawKVs = append([]attribute.KeyValue(nil), rawKVs...)
}
rawKVs = append(rawKVs, r.RawAttributes()...)
}
}
}
return rawKVs
}
func resolveAttributes(configAttrs attribute.Set, rawKVs []attribute.KeyValue) attribute.Set {
if len(rawKVs) == 0 {
return configAttrs
}
merged := make([]attribute.KeyValue, 0, configAttrs.Len()+len(rawKVs))
merged = append(merged, configAttrs.ToSlice()...)
// rawKVs are appended after configAttrs, meaning they will override any duplicate keys in configAttrs.
// This behavior is documented in WithUnsafeAttributes.
merged = append(merged, rawKVs...)
// TODO(#7743): Defer computing the full attribute.NewSet.
return attribute.NewSet(merged...)
}
type int64Inst struct {
measures []aggregate.Measure[int64]
@@ -198,12 +236,14 @@ var (
func (i *int64Inst) Add(ctx context.Context, val int64, opts ...metric.AddOption) {
c := metric.NewAddConfig(opts)
i.aggregate(ctx, val, c.Attributes())
rawKVs := extractRawKVs(opts)
i.aggregate(ctx, val, resolveAttributes(c.Attributes(), rawKVs))
}
func (i *int64Inst) Record(ctx context.Context, val int64, opts ...metric.RecordOption) {
c := metric.NewRecordConfig(opts)
i.aggregate(ctx, val, c.Attributes())
rawKVs := extractRawKVs(opts)
i.aggregate(ctx, val, resolveAttributes(c.Attributes(), rawKVs))
}
func (i *int64Inst) Enabled(context.Context) bool {
@@ -238,12 +278,14 @@ var (
func (i *float64Inst) Add(ctx context.Context, val float64, opts ...metric.AddOption) {
c := metric.NewAddConfig(opts)
i.aggregate(ctx, val, c.Attributes())
rawKVs := extractRawKVs(opts)
i.aggregate(ctx, val, resolveAttributes(c.Attributes(), rawKVs))
}
func (i *float64Inst) Record(ctx context.Context, val float64, opts ...metric.RecordOption) {
c := metric.NewRecordConfig(opts)
i.aggregate(ctx, val, c.Attributes())
rawKVs := extractRawKVs(opts)
i.aggregate(ctx, val, resolveAttributes(c.Attributes(), rawKVs))
}
func (i *float64Inst) Enabled(context.Context) bool {
+440
View File
@@ -4,11 +4,17 @@
package metric
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/x"
"go.opentelemetry.io/otel/sdk/metric/internal/aggregate"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
"go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest"
)
func BenchmarkInstrument(b *testing.B) {
@@ -74,3 +80,437 @@ func BenchmarkInstrument(b *testing.B) {
}
})
}
func TestExtractRawKVs(t *testing.T) {
k1 := attribute.String("k1", "v1")
k2 := attribute.String("k2", "v2")
k3 := attribute.String("k3", "v3")
tests := []struct {
name string
opts []metric.AddOption
want []attribute.KeyValue
}{
{
name: "Empty",
opts: nil,
want: nil,
},
{
name: "NoRawAttributes",
opts: []metric.AddOption{metric.WithAttributes(k1)},
want: nil,
},
{
name: "OneRawAttributes",
opts: []metric.AddOption{x.WithUnsafeAttributes(k1, k2)},
want: []attribute.KeyValue{k1, k2},
},
{
name: "MultipleRawAttributes",
opts: []metric.AddOption{
x.WithUnsafeAttributes(k1),
x.WithUnsafeAttributes(k2, k3),
},
want: []attribute.KeyValue{k1, k2, k3},
},
{
name: "Mixed",
opts: []metric.AddOption{
x.WithUnsafeAttributes(k1),
metric.WithAttributes(k2),
x.WithUnsafeAttributes(k3),
},
want: []attribute.KeyValue{k1, k3},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractRawKVs(tt.opts)
require.Equal(t, tt.want, got)
})
}
}
func TestExtractRawKVs_NoModifyOriginalSlice(t *testing.T) {
k1 := attribute.String("k1", "v1")
k2 := attribute.String("k2", "v2")
s1 := []attribute.KeyValue{k1}
// Give it some extra capacity to ensure append would overwrite if it reused the backing array.
s1 = append(s1, attribute.KeyValue{})[:1]
opt1 := x.WithUnsafeAttributes(s1...)
opt2 := x.WithUnsafeAttributes(k2)
opts := []metric.AddOption{opt1, opt2}
got := extractRawKVs(opts)
require.Equal(t, []attribute.KeyValue{k1, k2}, got)
// Check that s1 was not modified.
require.Equal(t, []attribute.KeyValue{k1}, s1[:1])
fullS1 := s1[:cap(s1)]
if len(fullS1) > 1 {
require.NotEqual(t, k2, fullS1[1], "Original slice was modified by extractRawKVs")
}
}
func TestResolveAttributes(t *testing.T) {
k1 := attribute.String("k1", "v1")
k2 := attribute.String("k2", "v2")
k3 := attribute.String("k3", "v3")
k1Alt := attribute.String("k1", "v1_alt")
tests := []struct {
name string
configAttrs attribute.Set
rawKVs []attribute.KeyValue
want attribute.Set
}{
{
name: "Empty",
configAttrs: *attribute.EmptySet(),
rawKVs: nil,
want: *attribute.EmptySet(),
},
{
name: "OnlyConfig",
configAttrs: attribute.NewSet(k1, k2),
rawKVs: nil,
want: attribute.NewSet(k1, k2),
},
{
name: "OnlyRaw",
configAttrs: *attribute.EmptySet(),
rawKVs: []attribute.KeyValue{k1, k2},
want: attribute.NewSet(k1, k2),
},
{
name: "MergeNoOverlap",
configAttrs: attribute.NewSet(k1),
rawKVs: []attribute.KeyValue{k2, k3},
want: attribute.NewSet(k1, k2, k3),
},
{
name: "MergeWithOverlap_RawOverrides",
configAttrs: attribute.NewSet(k1, k2),
rawKVs: []attribute.KeyValue{k1Alt, k3},
want: attribute.NewSet(k1Alt, k2, k3),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := resolveAttributes(tt.configAttrs, tt.rawKVs)
require.Equal(t, tt.want, got)
})
}
}
func TestMeterWithUnsafeAttributes(t *testing.T) {
k1 := attribute.Key("k1")
k2 := attribute.Key("k2")
combined := attribute.NewSet(k1.String("alice"), k2.String("bob"))
testCases := []struct {
name string
instName string
record func(t *testing.T, m metric.Meter)
wantData func(attrs attribute.Set) metricdata.Aggregation
}{
{
name: "Int64Counter",
instName: "sint",
record: func(t *testing.T, m metric.Meter) {
ctr, err := m.Int64Counter("sint")
require.NoError(t, err)
ctr.Add(t.Context(), 3, x.WithUnsafeAttributes(k1.String("alice"), k2.String("bob")))
},
wantData: func(attrs attribute.Set) metricdata.Aggregation {
return metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[int64]{{Attributes: attrs, Value: 3}},
}
},
},
{
name: "Float64Counter",
instName: "sfloat",
record: func(t *testing.T, m metric.Meter) {
ctr, err := m.Float64Counter("sfloat")
require.NoError(t, err)
ctr.Add(t.Context(), 3.5, x.WithUnsafeAttributes(k1.String("alice"), k2.String("bob")))
},
wantData: func(attrs attribute.Set) metricdata.Aggregation {
return metricdata.Sum[float64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[float64]{{Attributes: attrs, Value: 3.5}},
}
},
},
{
name: "Int64ObservableCounter",
instName: "aint",
record: func(t *testing.T, m metric.Meter) {
_, err := m.Int64ObservableCounter(
"aint",
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
o.Observe(4, x.WithUnsafeAttributes(k1.String("alice"), k2.String("bob")))
return nil
}),
)
require.NoError(t, err)
},
wantData: func(attrs attribute.Set) metricdata.Aggregation {
return metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[int64]{{Attributes: attrs, Value: 4}},
}
},
},
{
name: "Float64ObservableCounter",
instName: "afloat",
record: func(t *testing.T, m metric.Meter) {
_, err := m.Float64ObservableCounter(
"afloat",
metric.WithFloat64Callback(func(_ context.Context, o metric.Float64Observer) error {
o.Observe(4.5, x.WithUnsafeAttributes(k1.String("alice"), k2.String("bob")))
return nil
}),
)
require.NoError(t, err)
},
wantData: func(attrs attribute.Set) metricdata.Aggregation {
return metricdata.Sum[float64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: true,
DataPoints: []metricdata.DataPoint[float64]{{Attributes: attrs, Value: 4.5}},
}
},
},
{
name: "Int64UpDownCounter",
instName: "sudint",
record: func(t *testing.T, m metric.Meter) {
ctr, err := m.Int64UpDownCounter("sudint")
require.NoError(t, err)
ctr.Add(t.Context(), -3, x.WithUnsafeAttributes(k1.String("alice"), k2.String("bob")))
},
wantData: func(attrs attribute.Set) metricdata.Aggregation {
return metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: false,
DataPoints: []metricdata.DataPoint[int64]{{Attributes: attrs, Value: -3}},
}
},
},
{
name: "Float64UpDownCounter",
instName: "sudfloat",
record: func(t *testing.T, m metric.Meter) {
ctr, err := m.Float64UpDownCounter("sudfloat")
require.NoError(t, err)
ctr.Add(t.Context(), -3.5, x.WithUnsafeAttributes(k1.String("alice"), k2.String("bob")))
},
wantData: func(attrs attribute.Set) metricdata.Aggregation {
return metricdata.Sum[float64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: false,
DataPoints: []metricdata.DataPoint[float64]{{Attributes: attrs, Value: -3.5}},
}
},
},
{
name: "Int64Histogram",
instName: "shist",
record: func(t *testing.T, m metric.Meter) {
hist, err := m.Int64Histogram("shist")
require.NoError(t, err)
hist.Record(t.Context(), 5, x.WithUnsafeAttributes(k1.String("alice"), k2.String("bob")))
},
wantData: func(attrs attribute.Set) metricdata.Aggregation {
return metricdata.Histogram[int64]{
Temporality: metricdata.CumulativeTemporality,
DataPoints: []metricdata.HistogramDataPoint[int64]{
{
Attributes: attrs,
Count: 1,
Sum: 5,
Bounds: []float64{
0,
5,
10,
25,
50,
75,
100,
250,
500,
750,
1000,
2500,
5000,
7500,
10000,
},
BucketCounts: []uint64{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
Min: metricdata.NewExtrema[int64](5),
Max: metricdata.NewExtrema[int64](5),
},
},
}
},
},
{
name: "Float64Histogram",
instName: "sfhist",
record: func(t *testing.T, m metric.Meter) {
hist, err := m.Float64Histogram("sfhist")
require.NoError(t, err)
hist.Record(t.Context(), 5.5, x.WithUnsafeAttributes(k1.String("alice"), k2.String("bob")))
},
wantData: func(attrs attribute.Set) metricdata.Aggregation {
return metricdata.Histogram[float64]{
Temporality: metricdata.CumulativeTemporality,
DataPoints: []metricdata.HistogramDataPoint[float64]{
{
Attributes: attrs,
Count: 1,
Sum: 5.5,
Bounds: []float64{
0,
5,
10,
25,
50,
75,
100,
250,
500,
750,
1000,
2500,
5000,
7500,
10000,
},
BucketCounts: []uint64{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
Min: metricdata.NewExtrema[float64](5.5),
Max: metricdata.NewExtrema[float64](5.5),
},
},
}
},
},
{
name: "Int64ObservableUpDownCounter",
instName: "audint",
record: func(t *testing.T, m metric.Meter) {
_, err := m.Int64ObservableUpDownCounter(
"audint",
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
o.Observe(-4, x.WithUnsafeAttributes(k1.String("alice"), k2.String("bob")))
return nil
}),
)
require.NoError(t, err)
},
wantData: func(attrs attribute.Set) metricdata.Aggregation {
return metricdata.Sum[int64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: false,
DataPoints: []metricdata.DataPoint[int64]{{Attributes: attrs, Value: -4}},
}
},
},
{
name: "Float64ObservableUpDownCounter",
instName: "audfloat",
record: func(t *testing.T, m metric.Meter) {
_, err := m.Float64ObservableUpDownCounter(
"audfloat",
metric.WithFloat64Callback(func(_ context.Context, o metric.Float64Observer) error {
o.Observe(-4.5, x.WithUnsafeAttributes(k1.String("alice"), k2.String("bob")))
return nil
}),
)
require.NoError(t, err)
},
wantData: func(attrs attribute.Set) metricdata.Aggregation {
return metricdata.Sum[float64]{
Temporality: metricdata.CumulativeTemporality,
IsMonotonic: false,
DataPoints: []metricdata.DataPoint[float64]{{Attributes: attrs, Value: -4.5}},
}
},
},
{
name: "Int64ObservableGauge",
instName: "agint",
record: func(t *testing.T, m metric.Meter) {
_, err := m.Int64ObservableGauge(
"agint",
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
o.Observe(10, x.WithUnsafeAttributes(k1.String("alice"), k2.String("bob")))
return nil
}),
)
require.NoError(t, err)
},
wantData: func(attrs attribute.Set) metricdata.Aggregation {
return metricdata.Gauge[int64]{
DataPoints: []metricdata.DataPoint[int64]{{Attributes: attrs, Value: 10}},
}
},
},
{
name: "Float64ObservableGauge",
instName: "agfloat",
record: func(t *testing.T, m metric.Meter) {
_, err := m.Float64ObservableGauge(
"agfloat",
metric.WithFloat64Callback(func(_ context.Context, o metric.Float64Observer) error {
o.Observe(10.5, x.WithUnsafeAttributes(k1.String("alice"), k2.String("bob")))
return nil
}),
)
require.NoError(t, err)
},
wantData: func(attrs attribute.Set) metricdata.Aggregation {
return metricdata.Gauge[float64]{
DataPoints: []metricdata.DataPoint[float64]{{Attributes: attrs, Value: 10.5}},
}
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
rdr := NewManualReader()
m := NewMeterProvider(WithReader(rdr)).Meter("test")
tt.record(t, m)
rm := metricdata.ResourceMetrics{}
err := rdr.Collect(t.Context(), &rm)
require.NoError(t, err)
require.Len(t, rm.ScopeMetrics, 1)
sm := rm.ScopeMetrics[0]
require.Len(t, sm.Metrics, 1)
got := sm.Metrics[0]
want := metricdata.Metrics{
Name: tt.instName,
Data: tt.wantData(combined),
}
metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreExemplars())
})
}
}
+10 -4
View File
@@ -593,11 +593,13 @@ func (r observer) ObserveFloat64(o metric.Float64Observable, v float64, opts ...
return
}
c := metric.NewObserveConfig(opts)
rawKVs := extractRawKVs(opts)
set := resolveAttributes(c.Attributes(), rawKVs)
// Access to r.pipe.float64Measure is already guarded by a lock in pipeline.produce.
// TODO (#5946): Refactor pipeline and observable measures.
measures := r.pipe.float64Measures[oImpl.observableID]
for _, m := range measures {
m(context.Background(), v, c.Attributes())
m(context.Background(), v, set)
}
}
@@ -624,11 +626,13 @@ func (r observer) ObserveInt64(o metric.Int64Observable, v int64, opts ...metric
return
}
c := metric.NewObserveConfig(opts)
rawKVs := extractRawKVs(opts)
set := resolveAttributes(c.Attributes(), rawKVs)
// Access to r.pipe.int64Measures is already guarded b a lock in pipeline.produce.
// TODO (#5946): Refactor pipeline and observable measures.
measures := r.pipe.int64Measures[oImpl.observableID]
for _, m := range measures {
m(context.Background(), v, c.Attributes())
m(context.Background(), v, set)
}
}
@@ -793,7 +797,8 @@ type int64Observer struct {
func (o int64Observer) Observe(val int64, opts ...metric.ObserveOption) {
c := metric.NewObserveConfig(opts)
o.observe(val, c.Attributes())
rawKVs := extractRawKVs(opts)
o.observe(val, resolveAttributes(c.Attributes(), rawKVs))
}
type float64Observer struct {
@@ -803,7 +808,8 @@ type float64Observer struct {
func (o float64Observer) Observe(val float64, opts ...metric.ObserveOption) {
c := metric.NewObserveConfig(opts)
o.observe(val, c.Attributes())
rawKVs := extractRawKVs(opts)
o.observe(val, resolveAttributes(c.Attributes(), rawKVs))
}
func defaultAttributes[T any](opts []T) []attribute.Key {