Skip to content

Commit 53b2edd

Browse files
authored
Merge 5fc09ea into 3b55ac7
2 parents 3b55ac7 + 5fc09ea commit 53b2edd

40 files changed

Lines changed: 1434 additions & 167 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
88

99
## Unreleased
1010

11+
- Add Exemplar support in the Lightstep Metrics SDK. [#576](https://github.com/lightstep/otel-launcher-go/pull/576)
12+
1113
## [1.23.0](https://github.com/lightstep/otel-launcher-go/releases/tag/v1.23.0) - 2023-12-20)
1214

1315
- Update to OTel-Arrow v0.13.0 dependencies. [#591](https://github.com/lightstep/otel-launcher-go/pull/591)

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ lint: $(TOOLS_DIR)/golangci-lint $(TOOLS_DIR)/misspell
136136
set -e; for dir in $(ALL_GO_MOD_DIRS) $(TOOLS_MOD_DIR); do \
137137
echo "go mod tidy in $${dir}"; \
138138
(cd "$${dir}" && \
139-
go mod tidy); \
139+
GOWORK=off go mod tidy); \
140140
done
141141
set -e; for dir in $(ALL_GO_MOD_DIRS); do \
142142
echo "golangci-lint in $${dir}"; \

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ require (
4646
github.com/lightstep/otel-launcher-go/lightstep/instrumentation v1.23.0 // indirect
4747
github.com/lightstep/otel-launcher-go/lightstep/sdk/internal v1.23.0 // indirect
4848
github.com/lightstep/otel-launcher-go/lightstep/sdk/trace v1.23.0 // indirect
49+
github.com/lightstep/varopt v1.4.0 // indirect
4950
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
5051
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect
5152
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
782782
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
783783
github.com/lightstep/go-expohisto v1.0.0 h1:UPtTS1rGdtehbbAF7o/dhkWLTDI73UifG8LbfQI7cA4=
784784
github.com/lightstep/go-expohisto v1.0.0/go.mod h1:xDXD0++Mu2FOaItXtdDfksfgxfV0z1TMPa+e/EUd0cs=
785+
github.com/lightstep/varopt v1.4.0 h1:MCpQouffyrj0xGe7pQRP0urgMHG04gHjE9v5FhpODzo=
786+
github.com/lightstep/varopt v1.4.0/go.mod h1:8XCrfUxO78WYWeFHSFD1j1ePNhRsGXd44YTfn+l3kjs=
785787
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
786788
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
787789
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=

lightstep/sdk/internal/common.go

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package internal
1616

1717
import (
18+
"fmt"
1819
"sync"
1920

2021
"go.opentelemetry.io/collector/pdata/pcommon"
@@ -61,43 +62,46 @@ func (rm *ResourceMap) Get(in *resource.Resource) pcommon.Resource {
6162

6263
func CopyAttributes(dest pcommon.Map, src attribute.Set) {
6364
for iter := src.Iter(); iter.Next(); {
64-
inA := iter.Attribute()
65-
key := string(inA.Key)
66-
switch inA.Value.Type() {
67-
case attribute.BOOL:
68-
dest.PutBool(key, inA.Value.AsBool())
69-
case attribute.INT64:
70-
dest.PutInt(key, inA.Value.AsInt64())
71-
case attribute.FLOAT64:
72-
dest.PutDouble(key, inA.Value.AsFloat64())
73-
case attribute.STRING:
74-
dest.PutStr(key, inA.Value.AsString())
75-
case attribute.BOOLSLICE:
76-
sl := dest.PutEmptySlice(key)
77-
sl.EnsureCapacity(len(inA.Value.AsBoolSlice()))
78-
for _, v := range inA.Value.AsBoolSlice() {
79-
sl.AppendEmpty().SetBool(v)
80-
}
81-
case attribute.INT64SLICE:
82-
sl := dest.PutEmptySlice(key)
83-
sl.EnsureCapacity(len(inA.Value.AsInt64Slice()))
84-
for _, v := range inA.Value.AsInt64Slice() {
85-
sl.AppendEmpty().SetInt(v)
86-
}
87-
case attribute.FLOAT64SLICE:
88-
sl := dest.PutEmptySlice(key)
89-
sl.EnsureCapacity(len(inA.Value.AsFloat64Slice()))
90-
for _, v := range inA.Value.AsFloat64Slice() {
91-
sl.AppendEmpty().SetDouble(v)
92-
}
93-
case attribute.STRINGSLICE:
94-
sl := dest.PutEmptySlice(key)
95-
sl.EnsureCapacity(len(inA.Value.AsStringSlice()))
96-
for _, v := range inA.Value.AsStringSlice() {
97-
sl.AppendEmpty().SetStr(v)
98-
}
99-
default:
100-
panic("unhandled case")
65+
CopyAttribute(dest, iter.Attribute())
66+
}
67+
}
68+
69+
func CopyAttribute(dest pcommon.Map, inA attribute.KeyValue) {
70+
key := string(inA.Key)
71+
switch inA.Value.Type() {
72+
case attribute.BOOL:
73+
dest.PutBool(key, inA.Value.AsBool())
74+
case attribute.INT64:
75+
dest.PutInt(key, inA.Value.AsInt64())
76+
case attribute.FLOAT64:
77+
dest.PutDouble(key, inA.Value.AsFloat64())
78+
case attribute.STRING:
79+
dest.PutStr(key, inA.Value.AsString())
80+
case attribute.BOOLSLICE:
81+
sl := dest.PutEmptySlice(key)
82+
sl.EnsureCapacity(len(inA.Value.AsBoolSlice()))
83+
for _, v := range inA.Value.AsBoolSlice() {
84+
sl.AppendEmpty().SetBool(v)
85+
}
86+
case attribute.INT64SLICE:
87+
sl := dest.PutEmptySlice(key)
88+
sl.EnsureCapacity(len(inA.Value.AsInt64Slice()))
89+
for _, v := range inA.Value.AsInt64Slice() {
90+
sl.AppendEmpty().SetInt(v)
91+
}
92+
case attribute.FLOAT64SLICE:
93+
sl := dest.PutEmptySlice(key)
94+
sl.EnsureCapacity(len(inA.Value.AsFloat64Slice()))
95+
for _, v := range inA.Value.AsFloat64Slice() {
96+
sl.AppendEmpty().SetDouble(v)
97+
}
98+
case attribute.STRINGSLICE:
99+
sl := dest.PutEmptySlice(key)
100+
sl.EnsureCapacity(len(inA.Value.AsStringSlice()))
101+
for _, v := range inA.Value.AsStringSlice() {
102+
sl.AppendEmpty().SetStr(v)
101103
}
104+
default:
105+
panic(fmt.Errorf("unhandled case: %v", inA.Value.Type()))
102106
}
103107
}

lightstep/sdk/metric/README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,90 @@ dynamically configure allowed cardinality values.
199199
This limit is used to truncate attribute key and string values to a
200200
reasonable size. The default limit is 8kB. Zero is not a valid
201201
limit.
202+
203+
#### Exemplars
204+
205+
**Status**: Experimental
206+
207+
Exemplars are sample measurements associated with synchronous metric
208+
instruments. When OpenTelemetry tracing is used in conjunction with
209+
this Metrics SDK, exemplars will be annotated with the TraceID and
210+
SpanID of the traced context.
211+
212+
Collection of metric exemplars are off by default. The
213+
`sdkinstrument.Performance.ExemplarsEnabled` field can be used to
214+
enable exemplars by default. This field may be set to a number of
215+
exemplars to collect by default for all Counter and Histogram
216+
instruments.
217+
218+
Exemplars can also be configured using the `aggregator.Config.Exemplar`
219+
structure, or with a hint like:
220+
221+
```
222+
{
223+
"description": "measurement of ...",
224+
"config": {
225+
"exemplar": {
226+
"size": 10,
227+
"filter": "trace_based",
228+
}
229+
}
230+
}
231+
```
232+
233+
Like the OpenTelemetry specification, the supported filters are
234+
"always_off", "always_on", and "trace_based". Unlike the
235+
OpenTelemetry specification, this SDK has two reservoir
236+
implementations:
237+
238+
- "Last": always chooses the last metric event as the exemplar. This
239+
method is automatically selected when the size is 1.
240+
- "Weighted": uses a weighted sampling technique.
241+
242+
With weighted sampling, an unbiased sampler is used such that the
243+
distribution of values can be estimated from the exemplars. Each
244+
exemplar includes a `sample.weight` attribute indicating its
245+
contribution to the aggregate value.
246+
247+
Note that this weighted sampling property does not apply to
248+
UpDownCounter instruments, because they allow negative measurements.
249+
however these instruments can still generate exemplars.
250+
251+
As a simple example, consider a counter instrument with two input
252+
attribute values counting blue and yellow items, i.e.,
253+
254+
```
255+
counter := meter.Int64Counter("...")
256+
257+
for _ := range BLUE {
258+
ctx, span := tracer.Start(...)
259+
counter.Add(ctx, 1, attribute.String("color", "yellow")
260+
span.End()
261+
}
262+
263+
for _ := range YELLOW {
264+
ctx, span := tracer.Start(...)
265+
counter.Add(ctx, 1, attribute.String("color", "blue")
266+
span.End()
267+
}
268+
```
269+
270+
Suppose the attribute is filtered, so that a single timeseries is
271+
generated. The aggregate sum equals `len(YELLOW) + len(BLUE)`.
272+
273+
Each exemplar will have one of the filtered attributes, `color=yellow`
274+
or `color=blue`, Each exemplar will have a value of 1, in this case
275+
(i.e., the original measurement). The ratio of exemplars with
276+
`color=yellow` or `color=blue` will match the ratio of counts
277+
associated with each.
278+
279+
If the number of BLUE items is 3000, and the number of YELLOW items is
280+
7000, and the number of exemplars is 1000, then:
281+
282+
- We expect 300 BLUE examplars
283+
- We expect 700 YELLOW examplars
284+
- Each exemplar has a `sample.weight` of 10, the ratio of total count to exemplar count.
285+
286+
Note that we expect the sum of `sample.weight` for the exemplars to
287+
equal the total number of input events (i.e., 3000 BLUE, 7000 YELLOW).
288+

lightstep/sdk/metric/aggregator/aggregator.go

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
"github.com/lightstep/otel-launcher-go/lightstep/sdk/metric/number"
2525
"github.com/lightstep/otel-launcher-go/lightstep/sdk/metric/sdkinstrument"
2626
"go.opentelemetry.io/otel"
27+
"go.opentelemetry.io/otel/attribute"
28+
"go.opentelemetry.io/otel/trace"
2729
)
2830

2931
// Sentinel errors for Aggregator interface.
@@ -33,6 +35,28 @@ var (
3335
ErrInfInput = fmt.Errorf("±Inf value is an invalid input")
3436
)
3537

38+
// ExemplarFilterKind determines which events are eligible for
39+
// becoming exemplars.
40+
type ExemplarFilterKind int
41+
42+
const (
43+
// AlwaysOffKind is the default when aggregator.Config{} is
44+
// used with a zero value. This is a good default because
45+
// exemplars require additional synchronization.
46+
AlwaysOffKind ExemplarFilterKind = iota
47+
48+
// AlwaysOnKind considers all events for exemplar sampling.
49+
AlwaysOnKind
50+
51+
// WhenTracedKind considers events only in sampled trace
52+
// contexts for exemplar sampling.
53+
WhenTracedKind
54+
)
55+
56+
// DefaultExemplarReservoirSize determines how many exemplars will be
57+
// selected per instrument.
58+
const DefaultExemplarReservoirSize = 10
59+
3660
// RangeTest is a common routine for testing for valid input values.
3761
// This rejects NaN and Inf values. This rejects negative values when the
3862
// aggregation does not support negative values, including
@@ -68,6 +92,20 @@ func RangeTest[N number.Any, Traits number.Traits[N]](num N, desc sdkinstrument.
6892
return true
6993
}
7094

95+
// ExemplarConfig configures exemplar selection.
96+
type ExemplarConfig struct {
97+
// Filter determines which contexts are selected.
98+
Filter ExemplarFilterKind
99+
// Size determines limits how many exemplars per timeseries.
100+
Size uint32
101+
}
102+
103+
// JSONExemplarConfig configures exemplar selection.
104+
type JSONExemplarConfig struct {
105+
Filter string `json:"filter"`
106+
Size uint32 `json:"size"`
107+
}
108+
71109
// JSONHistogramConfig configures the exponential histogram.
72110
type JSONHistogramConfig struct {
73111
MaxSize int32 `json:"max_size"`
@@ -77,6 +115,7 @@ type JSONHistogramConfig struct {
77115
type JSONConfig struct {
78116
Histogram JSONHistogramConfig `json:"histogram"`
79117
CardinalityLimit uint32 `json:"cardinality_limit"`
118+
Exemplar JSONExemplarConfig `json:"exemplar"`
80119
}
81120

82121
// Config supports the configuration for all aggregators in a single struct.
@@ -87,6 +126,9 @@ type Config struct {
87126
// CardinalityLimit limits the number of instances of this
88127
// aggregator in a given view.
89128
CardinalityLimit uint32
129+
130+
// ExemplarFilter enables or disables exemplars
131+
Exemplar ExemplarConfig
90132
}
91133

92134
// Valid returns true for valid configurations.
@@ -140,7 +182,7 @@ type Methods[N number.Any, Storage any] interface {
140182

141183
// Update modifies Storage concurrently with respect to
142184
// concurrent Move(), Copy(), and Update() operations.
143-
Update(ptr *Storage, number N)
185+
Update(ptr *Storage, number N, ex ExemplarBits)
144186

145187
// Move atomically copies `input` to `output` and resets
146188
// `input` to the zero state. The change to `input` is
@@ -181,7 +223,44 @@ type Methods[N number.Any, Storage any] interface {
181223
// aggregation. If the instrument is asynchronous, this will
182224
// be called after subtraction. Not synchronized.
183225
HasChange(ptr *Storage) bool
226+
227+
// Exemplars returns sample points included in this aggregation.
228+
Exemplars(ptr *Storage, in []WeightedExemplarBits) []WeightedExemplarBits
229+
230+
// Weight is the sample weight. It is 1 for histogram and
231+
// gauge aggregations, and it is the value for sum data
232+
// aggregations.
233+
Weight(number N) float64
184234
}
185235

186236
// ConfigSelector is a per-instrument-kind, per-number-kind Config choice.
187237
type ConfigSelector func(sdkinstrument.Kind) (int64Config, float64Config Config)
238+
239+
// ExemplarBits conducts extra information into the aggregation pipeline.
240+
//
241+
// Note: we could opt for an allocation instead of copying this struct
242+
// by value through the pipeline.
243+
type ExemplarBits struct {
244+
// Time of the event.
245+
Time time.Time
246+
247+
// Attributes are the complete original set of attributes.
248+
Attributes []attribute.KeyValue
249+
250+
// Span has a reference to the span context, which has the 24
251+
// bytes of ID. We keep a span reference here because it is
252+
// slightly smaller.
253+
Span trace.Span
254+
255+
// Number is the input value.
256+
Number number.Number
257+
}
258+
259+
// WeightedExemplarBits are the exemplar and its calculated sample weight.
260+
type WeightedExemplarBits struct {
261+
// ExemplarBits calculated at the event.
262+
ExemplarBits
263+
264+
// Weight calculated by aggregation pipeline.
265+
Weight float64
266+
}

lightstep/sdk/metric/aggregator/gauge/gauge.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ func (Methods[N, Traits]) Copy(from, to *State[N, Traits]) {
137137
to.seq = from.seq
138138
}
139139

140-
func (Methods[N, Traits]) Update(state *State[N, Traits], number N) {
140+
func (Methods[N, Traits]) Update(state *State[N, Traits], number N, _ aggregator.ExemplarBits) {
141141
newSeq := atomic.AddUint64(&sequenceVar, 1)
142142

143143
state.lock.Lock()
@@ -169,3 +169,11 @@ func (Methods[N, Traits]) ToStorage(aggr aggregation.Aggregation) (*State[N, Tra
169169
func (Methods[N, Traits]) SubtractSwap(operand, argument *State[N, Traits]) {
170170
panic("not used for non-temporal metrics")
171171
}
172+
173+
func (Methods[N, Traits]) Exemplars(ptr *State[N, Traits], in []aggregator.WeightedExemplarBits) []aggregator.WeightedExemplarBits {
174+
return in
175+
}
176+
177+
func (Methods[N, Traits]) Weight(_ N) float64 {
178+
return 1
179+
}

0 commit comments

Comments
 (0)