diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..c3c3206 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,41 @@ +name: Go +on: [push] +jobs: + build: + name: Build + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, ubuntu-latest] + goversion: ["1.24", "1.25"] + steps: + - name: Set up Go ${{matrix.goversion}} on ${{matrix.os}} + uses: actions/setup-go@v6 + with: + go-version: ${{matrix.goversion}} + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v5 + + - name: gofmt + run: | + [[ -z $(gofmt -l $(find . -name '*.go') ) ]] + + - name: Get dependencies + run: go mod download + + - name: Vet + env: + GO111MODULE: on + run: go vet -mod=readonly ./... + + - name: Test + env: + GO111MODULE: on + run: go test -mod=readonly -v -count 2 ./... + + - name: Race Test + env: + GO111MODULE: on + run: go test -race -mod=readonly -v -count 2 ./... diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8d0ab6a --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/vimeo/otelwrap + +go 1.24.0 + +require ( + go.opentelemetry.io/otel/metric v1.38.0 + go.opentelemetry.io/otel/sdk/metric v1.38.0 +) + +require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + golang.org/x/sys v0.35.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ae40c68 --- /dev/null +++ b/go.sum @@ -0,0 +1,31 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/otregistrar/registrar.go b/otregistrar/registrar.go new file mode 100644 index 0000000..79be39e --- /dev/null +++ b/otregistrar/registrar.go @@ -0,0 +1,355 @@ +// Package otregistrar provides functionality for batch-registering metrics/instruments with an Opentelemetry [metric.Meter]. +package otregistrar + +import ( + "fmt" + "maps" + "reflect" + "slices" + + "go.opentelemetry.io/otel/metric" +) + +const ( + metricNameTag = "otelname" + metricDescriptionTag = "oteldesc" + metricUnitTag = "otelunit" + histogramBoundRefTag = "otelhistbounds" + callbackRefTag = "otelcallback" +) + +// Options defines parameters for [RegisterInstruments], providing information that cannot be supplied directly by +// struct-tags. +type Options struct { + // registry of histogram bounds for both int and float histograms + // referenced by `otelhistbounds` tags on metric-fields. + // + // NOTE: [RegisterInstruments] will return an error if a histogram-typed field has a `otelhistbounds` tag and + // there is no matching entry in this map. + HistogramBounds map[string][]float64 + + // Callbacks for integer callbacks default lookup is by the field name, but that can be overridden with `otelcallback` + // When recursing into inner-struct fields, fields-names or `otelcallback` values must be + // prefixed with the dot-delimited path to that field (possibly + // substituted by any `otelcallback` values on the parent field) + // + // Field-names must be an exact string-match. + // + // One can also register callbacks later with [metric.Meter.RegisterCallback] on the meter. + IntCallbacks map[string][]metric.Int64Callback + + // Callbacks for float callbacks default lookup is by the field name, but that can be overridden with `otelcallback` + // When recursing into inner-struct fields, fields-names or `otelcallback` values must be + // prefixed with the dot-delimited path to that field (possibly + // substituted by any `otelcallback` values on the parent field) + // + // Field-names must be an exact string-match. + // + // One can also register callbacks later with [metric.Meter.RegisterCallback] on the meter. + FloatCallbacks map[string][]metric.Float64Callback +} + +// RegisterInstruments registers metrics based on the otelname, oteldesc, and +// otelunit tags on the fields of the passed struct. +func RegisterInstruments[T any](m metric.Meter, opts Options, s *T) error { + rv := reflect.ValueOf(s).Elem() + + return registerInstruments(m, rv, "", opts) +} + +func registerInstruments(m metric.Meter, rv reflect.Value, pathPrefix string, opts Options) error { + rt := rv.Type() + for i := range rv.Type().NumField() { + ft := rt.Field(i) + if !ft.IsExported() { + // we can't do anything with unexported fields + continue + } + f := rv.Field(i) + // skip anything that's not a pointer, struct or interface + // (instrument types are all interfaces as far as we're concerned) + switch f.Kind() { + case reflect.Struct, reflect.Pointer, reflect.Interface: + default: + continue + } + metricName, metricNameOK := ft.Tag.Lookup(metricNameTag) + genericOpts := make([]metric.InstrumentOption, 0, 2) + if desc, descOK := ft.Tag.Lookup(metricDescriptionTag); descOK { + genericOpts = append(genericOpts, metric.WithDescription(desc)) + } + if unit, unitOK := ft.Tag.Lookup(metricUnitTag); unitOK { + genericOpts = append(genericOpts, metric.WithUnit(unit)) + } + histBoundName, histBoundOK := ft.Tag.Lookup(histogramBoundRefTag) + + fPathComponent := func() string { + if cbRT, ok := ft.Tag.Lookup(callbackRefTag); ok { + return cbRT + } + return ft.Name + } + + fi := f.Addr().Interface() + switch t := fi.(type) { + case *metric.Int64Counter: + if !metricNameOK { + continue + } + mOpts := make([]metric.Int64CounterOption, len(genericOpts)) + for i, opt := range genericOpts { + mOpts[i] = opt + } + mt, mtErr := m.Int64Counter(metricName, mOpts...) + if mtErr != nil { + return fmt.Errorf("failed to register metric %q for field %q (idx %d): %w", metricName, ft.Name, i, mtErr) + } + *t = mt + case *metric.Int64UpDownCounter: + if !metricNameOK { + continue + } + mOpts := make([]metric.Int64UpDownCounterOption, len(genericOpts)) + for i, opt := range genericOpts { + mOpts[i] = opt + } + mt, mtErr := m.Int64UpDownCounter(metricName, mOpts...) + if mtErr != nil { + return fmt.Errorf("failed to register metric %q for field %q (idx %d): %w", metricName, ft.Name, i, mtErr) + } + *t = mt + case *metric.Int64Histogram: + if !metricNameOK { + continue + } + mOpts := make([]metric.Int64HistogramOption, len(genericOpts), len(genericOpts)+1) + for i, opt := range genericOpts { + mOpts[i] = opt + } + if histBoundOK && opts.HistogramBounds != nil { + // TODO: add a way to make this map lookup for histogram bounds besteffort + if bounds, boundsOK := opts.HistogramBounds[histBoundName]; boundsOK { + mOpts = append(mOpts, metric.WithExplicitBucketBoundaries(slices.Sorted(slices.Values(bounds))...)) + } else { + return fmt.Errorf("field %q has the %q tag set to %q, but no such entry exists in the bounds map; keys: %v", + ft.Name, histogramBoundRefTag, histBoundName, slices.Collect(maps.Keys(opts.HistogramBounds))) + } + // TODO: add fallbacks based on the unit and type + } + mt, mtErr := m.Int64Histogram(metricName, mOpts...) + if mtErr != nil { + return fmt.Errorf("failed to register metric %q for field %q (idx %d): %w", metricName, ft.Name, i, mtErr) + } + *t = mt + case *metric.Int64Gauge: + if !metricNameOK { + continue + } + mOpts := make([]metric.Int64GaugeOption, len(genericOpts)) + for i, opt := range genericOpts { + mOpts[i] = opt + } + mt, mtErr := m.Int64Gauge(metricName, mOpts...) + if mtErr != nil { + return fmt.Errorf("failed to register metric %q for field %q (idx %d): %w", metricName, ft.Name, i, mtErr) + } + *t = mt + case *metric.Int64ObservableCounter: + if !metricNameOK { + continue + } + mOpts := make([]metric.Int64ObservableCounterOption, len(genericOpts), len(genericOpts)+1) + for i, opt := range genericOpts { + mOpts[i] = opt + } + if cbs, cbOK := opts.IntCallbacks[pathPrefix+fPathComponent()]; cbOK { + for _, cb := range cbs { + mOpts = append(mOpts, metric.WithInt64Callback(cb)) + } + } + + mt, mtErr := m.Int64ObservableCounter(metricName, mOpts...) + if mtErr != nil { + return fmt.Errorf("failed to register metric %q for field %q (idx %d): %w", metricName, ft.Name, i, mtErr) + } + *t = mt + case *metric.Int64ObservableUpDownCounter: + if !metricNameOK { + continue + } + mOpts := make([]metric.Int64ObservableUpDownCounterOption, len(genericOpts), len(genericOpts)+1) + for i, opt := range genericOpts { + mOpts[i] = opt + } + if cbs, cbOK := opts.IntCallbacks[pathPrefix+fPathComponent()]; cbOK { + for _, cb := range cbs { + mOpts = append(mOpts, metric.WithInt64Callback(cb)) + } + } + mt, mtErr := m.Int64ObservableUpDownCounter(metricName, mOpts...) + if mtErr != nil { + return fmt.Errorf("failed to register metric %q for field %q (idx %d): %w", metricName, ft.Name, i, mtErr) + } + *t = mt + case *metric.Int64ObservableGauge: + if !metricNameOK { + continue + } + mOpts := make([]metric.Int64ObservableGaugeOption, len(genericOpts), len(genericOpts)+1) + for i, opt := range genericOpts { + mOpts[i] = opt + } + if cbs, cbOK := opts.IntCallbacks[pathPrefix+fPathComponent()]; cbOK { + for _, cb := range cbs { + mOpts = append(mOpts, metric.WithInt64Callback(cb)) + } + } + mt, mtErr := m.Int64ObservableGauge(metricName, mOpts...) + if mtErr != nil { + return fmt.Errorf("failed to register metric %q for field %q (idx %d): %w", metricName, ft.Name, i, mtErr) + } + *t = mt + case *metric.Float64Counter: + if !metricNameOK { + continue + } + mOpts := make([]metric.Float64CounterOption, len(genericOpts)) + for i, opt := range genericOpts { + mOpts[i] = opt + } + mt, mtErr := m.Float64Counter(metricName, mOpts...) + if mtErr != nil { + return fmt.Errorf("failed to register metric %q for field %q (idx %d): %w", metricName, ft.Name, i, mtErr) + } + *t = mt + case *metric.Float64UpDownCounter: + if !metricNameOK { + continue + } + mOpts := make([]metric.Float64UpDownCounterOption, len(genericOpts)) + for i, opt := range genericOpts { + mOpts[i] = opt + } + mt, mtErr := m.Float64UpDownCounter(metricName, mOpts...) + if mtErr != nil { + return fmt.Errorf("failed to register metric %q for field %q (idx %d): %w", metricName, ft.Name, i, mtErr) + } + *t = mt + case *metric.Float64Histogram: + if !metricNameOK { + continue + } + mOpts := make([]metric.Float64HistogramOption, len(genericOpts), len(genericOpts)+1) + for i, opt := range genericOpts { + mOpts[i] = opt + } + if histBoundOK && opts.HistogramBounds != nil { + // TODO: add a way to make this map lookup for histogram bounds besteffort + if bounds, boundsOK := opts.HistogramBounds[histBoundName]; boundsOK { + mOpts = append(mOpts, metric.WithExplicitBucketBoundaries(slices.Sorted(slices.Values(bounds))...)) + } else { + return fmt.Errorf("field %q has the %q tag set to %q, but no such entry exists in the bounds map; keys: %v", + ft.Name, histogramBoundRefTag, histBoundName, slices.Collect(maps.Keys(opts.HistogramBounds))) + } + } + // TODO: add fallbacks based on the unit and/or type + mt, mtErr := m.Float64Histogram(metricName, mOpts...) + if mtErr != nil { + return fmt.Errorf("failed to register metric %q for field %q (idx %d): %w", metricName, ft.Name, i, mtErr) + } + *t = mt + case *metric.Float64Gauge: + if !metricNameOK { + continue + } + mOpts := make([]metric.Float64GaugeOption, len(genericOpts)) + for i, opt := range genericOpts { + mOpts[i] = opt + } + mt, mtErr := m.Float64Gauge(metricName, mOpts...) + if mtErr != nil { + return fmt.Errorf("failed to register metric %q for field %q (idx %d): %w", metricName, ft.Name, i, mtErr) + } + *t = mt + case *metric.Float64ObservableCounter: + if !metricNameOK { + continue + } + mOpts := make([]metric.Float64ObservableCounterOption, len(genericOpts), len(genericOpts)+1) + for i, opt := range genericOpts { + mOpts[i] = opt + } + if cbs, cbOK := opts.FloatCallbacks[pathPrefix+fPathComponent()]; cbOK { + for _, cb := range cbs { + mOpts = append(mOpts, metric.WithFloat64Callback(cb)) + } + } + mt, mtErr := m.Float64ObservableCounter(metricName, mOpts...) + if mtErr != nil { + return fmt.Errorf("failed to register metric %q for field %q (idx %d): %w", metricName, ft.Name, i, mtErr) + } + *t = mt + case *metric.Float64ObservableUpDownCounter: + if !metricNameOK { + continue + } + mOpts := make([]metric.Float64ObservableUpDownCounterOption, len(genericOpts), len(genericOpts)+1) + for i, opt := range genericOpts { + mOpts[i] = opt + } + if cbs, cbOK := opts.FloatCallbacks[pathPrefix+fPathComponent()]; cbOK { + for _, cb := range cbs { + mOpts = append(mOpts, metric.WithFloat64Callback(cb)) + } + } + mt, mtErr := m.Float64ObservableUpDownCounter(metricName, mOpts...) + if mtErr != nil { + return fmt.Errorf("failed to register metric %q for field %q (idx %d): %w", metricName, ft.Name, i, mtErr) + } + *t = mt + case *metric.Float64ObservableGauge: + if !metricNameOK { + continue + } + mOpts := make([]metric.Float64ObservableGaugeOption, len(genericOpts), len(genericOpts)+1) + for i, opt := range genericOpts { + mOpts[i] = opt + } + if cbs, cbOK := opts.FloatCallbacks[pathPrefix+fPathComponent()]; cbOK { + for _, cb := range cbs { + mOpts = append(mOpts, metric.WithFloat64Callback(cb)) + } + } + mt, mtErr := m.Float64ObservableGauge(metricName, mOpts...) + if mtErr != nil { + return fmt.Errorf("failed to register metric %q for field %q (idx %d): %w", metricName, ft.Name, i, mtErr) + } + *t = mt + default: + // iteratively unwrap pointers/interfaces: + fe := f + switch ft.Type.Kind() { + case reflect.Pointer, reflect.Interface: + // don't even think about following nil pointers + PTRSTRIP: + for !fe.IsZero() { + switch fe.Kind() { + case reflect.Pointer, reflect.Interface: + fe = fe.Elem() + default: + break PTRSTRIP + } + } + + } + // If it's a struct, then it's not invalid :) + if fe.Kind() == reflect.Struct { + // TODO: handle reference-cycles + if recErr := registerInstruments(m, fe, pathPrefix+"."+fPathComponent()+".", opts); recErr != nil { + return fmt.Errorf("failed to recurse into %s (type %s): %w", ft.Name, ft.Type, recErr) + } + } + } + } + return nil +} diff --git a/otregistrar/registrar_test.go b/otregistrar/registrar_test.go new file mode 100644 index 0000000..283aefd --- /dev/null +++ b/otregistrar/registrar_test.go @@ -0,0 +1,387 @@ +package otregistrar + +import ( + "context" + "testing" + + "go.opentelemetry.io/otel/metric" + otsdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" +) + +func TestRegisterIntMetricsSuccess(t *testing.T) { + r := otsdkmetric.NewManualReader() + mp := otsdkmetric.NewMeterProvider(otsdkmetric.WithReader(r)) + + m := mp.Meter("fizzlebat") + + type deeperStruct struct { + IntUpDownCounterPathCB metric.Int64ObservableUpDownCounter `otelname:"ijklm.fizzlebat"` // no description or unit + } + + type anyStruct struct { + IntUpDownCounterPathCB metric.Int64ObservableUpDownCounter `otelname:"ijklm.fizzlebat.any"` // no description or unit + } + + ch := make(chan struct{}) + s := struct { + unexportedThing struct{} + unexportedInt int + ExportedButSkippedInt int + IntCounter metric.Int64Counter `otelname:"abcde.defg" oteldesc:"fizzlebat fizzle boo" otelunit:"s"` + IntUpDownCounter metric.Int64UpDownCounter `otelname:"abcde.defg.updown" oteldesc:"fizzlebat fizzle boo, but updown" otelunit:"s"` + IntGauge metric.Int64Gauge `otelname:"abcde.defg.gauge" oteldesc:"foobar gauge" otelunit:"By"` + IntHistogramNoBounds metric.Int64Histogram `otelname:"abcde.defg.histogram.nobounds" oteldesc:"foobar histogram" otelunit:"By/s"` + IntHistogramBounds metric.Int64Histogram `otelname:"abcde.defg.histogram.bounds" oteldesc:"foobar histogram" otelunit:"By/s" otelhistbounds:"intbounds"` + IntCounterCB metric.Int64ObservableCounter `otelname:"abcde.defg.callback" oteldesc:"foobar counter callback" otelunit:"s/s" otelcallback:"fooblebit"` + IntUpDownCounterCB metric.Int64ObservableUpDownCounter `otelname:"abcde.defg.updown.callback" oteldesc:"foobar updown counter callback" otelunit:"s/s" otelcallback:"seven"` + IntCounterCBNoCB metric.Int64ObservableCounter `otelname:"abcde.defg.callback.nocb" oteldesc:"foobar counter callback" otelunit:"s/s"` + IntUpDownCounterCBNoCB metric.Int64ObservableUpDownCounter `otelname:"abcde.defg.updown.callback.nocb" oteldesc:"foobar updown counter callback" otelunit:"s/s"` + IntGaugeCB metric.Int64ObservableGauge `otelname:"abcde.defg.gauge.cb" oteldesc:"foobar gauge callback"` // no unit + Deep *deeperStruct + DeepInterface any + ChanPtr *chan struct{} + }{Deep: &deeperStruct{}, DeepInterface: &anyStruct{}, ChanPtr: &ch} + + if regErr := RegisterInstruments(m, Options{ + HistogramBounds: map[string][]float64{ + "intbounds": {0, 1, 2, 3, 5, 4, 6}, + }, + IntCallbacks: map[string][]metric.Int64Callback{ + "fooblebit": {func(ctx context.Context, obs metric.Int64Observer) error { + obs.Observe(6) + return nil + }}, + "seven": {func(ctx context.Context, obs metric.Int64Observer) error { + obs.Observe(7) + return nil + }}, + "Deep.IntUpDownCounterPathCB": {func(ctx context.Context, obs metric.Int64Observer) error { + obs.Observe(8) + return nil + }}, + "IntGaugeCB": {func(ctx context.Context, obs metric.Int64Observer) error { + obs.Observe(9) + return nil + }}, + "DeepInterface.IntUpDownCounterPathCB": {func(ctx context.Context, obs metric.Int64Observer) error { + obs.Observe(10) + return nil + }}, + }, + }, &s); regErr != nil { + t.Errorf("failed to register instruments: %s", regErr) + return + } + t.Logf("got %+v", s) + ctx := t.Context() + // Record something so the metrics show up + s.IntCounter.Add(ctx, 1) + s.IntUpDownCounter.Add(ctx, 2) + s.IntGauge.Record(ctx, 3) + s.IntHistogramNoBounds.Record(ctx, 4) + s.IntHistogramBounds.Record(ctx, 5) + + rm := metricdata.ResourceMetrics{} + if collectErr := r.Collect(ctx, &rm); collectErr != nil { + t.Fatalf("failed to collect metrics: %s", collectErr) + } + expVals := map[string]int64{ + "abcde.defg": 1, + "abcde.defg.updown": 2, + "abcde.defg.gauge": 3, + "abcde.defg.histogram.nobounds": 4, + "abcde.defg.histogram.bounds": 5, + "abcde.defg.callback": 6, + "abcde.defg.updown.callback": 7, + // "abcde.defg.callback.nocb" : 0, // not registered + // "abcde.defg.updown.callback.nocb": 0, // not registered + "abcde.defg.gauge.cb": 9, + + // inner + "ijklm.fizzlebat": 8, + "ijklm.fizzlebat.any": 10, + } + for _, scope := range rm.ScopeMetrics { + for _, m := range scope.Metrics { + exp, expOK := expVals[m.Name] + if !expOK { + t.Errorf("unexpected metric/instrument %q", m.Name) + continue + } + switch d := m.Data.(type) { + case metricdata.Sum[int64]: + // we're only recording once + if len(d.DataPoints) != 1 { + t.Errorf("metric %q has %d datapoints; expected 1", m.Name, len(d.DataPoints)) + continue + } + for _, dp := range d.DataPoints { + if dp.Value != exp { + t.Errorf("unexpected value for metric %q: %d (expected %d)", m.Name, dp.Value, exp) + } + } + case metricdata.Gauge[int64]: + // we're only recording once + if len(d.DataPoints) != 1 { + t.Errorf("metric %q has %d datapoints; expected 1", m.Name, len(d.DataPoints)) + continue + } + for _, dp := range d.DataPoints { + if dp.Value != exp { + t.Errorf("unexpected value for metric %q: %d (expected %d)", m.Name, dp.Value, exp) + } + } + case metricdata.ExponentialHistogram[int64]: + // we're only recording once + if len(d.DataPoints) != 1 { + t.Errorf("metric %q has %d datapoints; expected 1", m.Name, len(d.DataPoints)) + continue + } + for _, dp := range d.DataPoints { + if dp.Sum != exp { + t.Errorf("unexpected sum for histogram metric %q: %d (expected %d)", m.Name, dp.Sum, exp) + } + if dp.Count != 1 { + t.Errorf("unexpected count for histogram metric %q: %d (expected %d)", m.Name, dp.Count, 1) + } + } + case metricdata.Histogram[int64]: + // we're only recording once + if len(d.DataPoints) != 1 { + t.Errorf("metric %q has %d datapoints; expected 1", m.Name, len(d.DataPoints)) + continue + } + for _, dp := range d.DataPoints { + if dp.Sum != exp { + t.Errorf("unexpected sum for histogram metric %q: %d (expected %d)", m.Name, dp.Sum, exp) + } + if dp.Count != 1 { + t.Errorf("unexpected count for histogram metric %q: %d (expected %d)", m.Name, dp.Count, 1) + } + } + default: + t.Errorf("unexpected data type for scalar values of type %T on metric %q: got data type %T", int64(0), m.Name, m.Data) + } + } + } +} + +func TestRegisterIntMetricsNoTags(t *testing.T) { + r := otsdkmetric.NewManualReader() + mp := otsdkmetric.NewMeterProvider(otsdkmetric.WithReader(r)) + + m := mp.Meter("fizzlebat") + + s := struct { + unexportedThing struct{} + unexportedInt int + ExportedButSkippedInt int + IntCounter metric.Int64Counter + IntUpDownCounter metric.Int64UpDownCounter + IntGauge metric.Int64Gauge + IntHistogramNoBounds metric.Int64Histogram + IntHistogramBounds metric.Int64Histogram + IntCounterCB metric.Int64ObservableCounter + IntUpDownCounterCB metric.Int64ObservableUpDownCounter + IntCounterCBNoCB metric.Int64ObservableCounter + IntUpDownCounterCBNoCB metric.Int64ObservableUpDownCounter + IntGaugeCB metric.Int64ObservableGauge + }{} + + if regErr := RegisterInstruments(m, Options{ + HistogramBounds: map[string][]float64{ + "intbounds": {0, 1, 2, 3, 5, 4, 6}, + }, + IntCallbacks: map[string][]metric.Int64Callback{ + "fooblebit": {func(ctx context.Context, obs metric.Int64Observer) error { + obs.Observe(6) + return nil + }}, + "seven": {func(ctx context.Context, obs metric.Int64Observer) error { + obs.Observe(7) + return nil + }}, + "Deep.IntUpDownCounterPathCB": {func(ctx context.Context, obs metric.Int64Observer) error { + obs.Observe(8) + return nil + }}, + }, + }, &s); regErr != nil { + t.Errorf("failed to register instruments: %s", regErr) + return + } +} +func TestRegisterFloatMetricsSuccess(t *testing.T) { + r := otsdkmetric.NewManualReader() + mp := otsdkmetric.NewMeterProvider(otsdkmetric.WithReader(r)) + + m := mp.Meter("fizzlebat") + + type deeperStruct struct { + FloatUpDownCounterPathCB metric.Float64ObservableUpDownCounter `otelname:"ijklm.fizzlebat"` // no description or unit + } + + s := struct { + unexportedThing struct{} + unexportedFloat float32 + ExportedButSkippedFloat float64 + FloatCounter metric.Float64Counter `otelname:"abcde.defg" oteldesc:"fizzlebat fizzle boo" otelunit:"s"` + FloatUpDownCounter metric.Float64UpDownCounter `otelname:"abcde.defg.updown" oteldesc:"fizzlebat fizzle boo, but updown" otelunit:"s"` + FloatGauge metric.Float64Gauge `otelname:"abcde.defg.gauge" oteldesc:"foobar gauge" otelunit:"By"` + FloatHistogramNoBounds metric.Float64Histogram `otelname:"abcde.defg.histogram.nobounds" oteldesc:"foobar histogram" otelunit:"By/s"` + FloatHistogramBounds metric.Float64Histogram `otelname:"abcde.defg.histogram.bounds" oteldesc:"foobar histogram" otelunit:"By/s" otelhistbounds:"floatbounds"` + FloatCounterCB metric.Float64ObservableCounter `otelname:"abcde.defg.callback" oteldesc:"foobar counter callback" otelunit:"s/s" otelcallback:"fooblebit"` + FloatUpDownCounterCB metric.Float64ObservableUpDownCounter `otelname:"abcde.defg.updown.callback" oteldesc:"foobar updown counter callback" otelunit:"s/s" otelcallback:"seven"` + FloatCounterCBNoCB metric.Float64ObservableCounter `otelname:"abcde.defg.callback.nocb" oteldesc:"foobar counter callback" otelunit:"s/s"` + FloatUpDownCounterCBNoCB metric.Float64ObservableUpDownCounter `otelname:"abcde.defg.updown.callback.nocb" oteldesc:"foobar updown counter callback" otelunit:"s/s"` + FloatGaugeCB metric.Float64ObservableGauge `otelname:"abcde.defg.gauge.cb" oteldesc:"foobar gauge callback"` // no unit + Deep deeperStruct + }{} + + if regErr := RegisterInstruments(m, Options{ + HistogramBounds: map[string][]float64{ + "floatbounds": {0, 1, 2, 3, 5, 4, 6}, + }, + FloatCallbacks: map[string][]metric.Float64Callback{ + "fooblebit": {func(ctx context.Context, obs metric.Float64Observer) error { + obs.Observe(6) + return nil + }}, + "seven": {func(ctx context.Context, obs metric.Float64Observer) error { + obs.Observe(7) + return nil + }}, + "Deep.FloatUpDownCounterPathCB": {func(ctx context.Context, obs metric.Float64Observer) error { + obs.Observe(8) + return nil + }}, + "FloatGaugeCB": {func(ctx context.Context, obs metric.Float64Observer) error { + obs.Observe(9) + return nil + }}, + }, + }, &s); regErr != nil { + t.Errorf("failed to register instruments: %s", regErr) + return + } + t.Logf("got %+v", s) + ctx := t.Context() + // Record something so the metrics show up + s.FloatCounter.Add(ctx, 1) + s.FloatUpDownCounter.Add(ctx, 2) + s.FloatGauge.Record(ctx, 3) + s.FloatHistogramNoBounds.Record(ctx, 4) + s.FloatHistogramBounds.Record(ctx, 5) + + rm := metricdata.ResourceMetrics{} + if collectErr := r.Collect(ctx, &rm); collectErr != nil { + t.Fatalf("failed to collect metrics: %s", collectErr) + } + expVals := map[string]float64{ + "abcde.defg": 1, + "abcde.defg.updown": 2, + "abcde.defg.gauge": 3, + "abcde.defg.histogram.nobounds": 4, + "abcde.defg.histogram.bounds": 5, + "abcde.defg.callback": 6, + "abcde.defg.updown.callback": 7, + // "abcde.defg.callback.nocb" : 0, // not registered + // "abcde.defg.updown.callback.nocb": 0, // not registered + "abcde.defg.gauge.cb": 9, + + // inner + "ijklm.fizzlebat": 8, + } + for _, scope := range rm.ScopeMetrics { + for _, m := range scope.Metrics { + exp, expOK := expVals[m.Name] + if !expOK { + t.Errorf("unexpected metric/instrument %q", m.Name) + continue + } + switch d := m.Data.(type) { + case metricdata.Sum[float64]: + // we're only recording once + if len(d.DataPoints) != 1 { + t.Errorf("metric %q has %d datapoints; expected 1", m.Name, len(d.DataPoints)) + continue + } + for _, dp := range d.DataPoints { + if dp.Value != exp { + t.Errorf("unexpected value for metric %q: %g (expected %g)", m.Name, dp.Value, exp) + } + } + case metricdata.Gauge[float64]: + // we're only recording once + if len(d.DataPoints) != 1 { + t.Errorf("metric %q has %d datapoints; expected 1", m.Name, len(d.DataPoints)) + continue + } + for _, dp := range d.DataPoints { + if dp.Value != exp { + t.Errorf("unexpected value for metric %q: %g (expected %g)", m.Name, dp.Value, exp) + } + } + case metricdata.ExponentialHistogram[float64]: + // we're only recording once + if len(d.DataPoints) != 1 { + t.Errorf("metric %q has %d datapoints; expected 1", m.Name, len(d.DataPoints)) + continue + } + for _, dp := range d.DataPoints { + if dp.Sum != exp { + t.Errorf("unexpected sum for histogram metric %q: %g (expected %g)", m.Name, dp.Sum, exp) + } + if dp.Count != 1 { + t.Errorf("unexpected count for histogram metric %q: %d (expected %d)", m.Name, dp.Count, 1) + } + } + case metricdata.Histogram[float64]: + // we're only recording once + if len(d.DataPoints) != 1 { + t.Errorf("metric %q has %d datapoints; expected 1", m.Name, len(d.DataPoints)) + continue + } + for _, dp := range d.DataPoints { + if dp.Sum != exp { + t.Errorf("unexpected sum for histogram metric %q: %g (expected %g)", m.Name, dp.Sum, exp) + } + if dp.Count != 1 { + t.Errorf("unexpected count for histogram metric %q: %d (expected %d)", m.Name, dp.Count, 1) + } + } + default: + t.Errorf("unexpected data type for scalar values of type %T on metric %q: got data type %T", float64(0), m.Name, m.Data) + } + } + } +} + +func TestRegisterFloatMetricsNoTags(t *testing.T) { + r := otsdkmetric.NewManualReader() + mp := otsdkmetric.NewMeterProvider(otsdkmetric.WithReader(r)) + + m := mp.Meter("fizzlebat") + + s := struct { + unexportedThing struct{} + unexportedFloat float32 + ExportedButSkippedFloat int + FloatCounter metric.Float64Counter + FloatUpDownCounter metric.Float64UpDownCounter + FloatGauge metric.Float64Gauge + FloatHistogramNoBounds metric.Float64Histogram + FloatHistogramBounds metric.Float64Histogram + FloatCounterCB metric.Float64ObservableCounter + FloatUpDownCounterCB metric.Float64ObservableUpDownCounter + FloatCounterCBNoCB metric.Float64ObservableCounter + FloatUpDownCounterCBNoCB metric.Float64ObservableUpDownCounter + FloatGaugeCB metric.Float64ObservableGauge + }{} + + if regErr := RegisterInstruments(m, Options{}, &s); regErr != nil { + t.Errorf("failed to register instruments: %s", regErr) + return + } +}