-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Copy implementation from oteldb by tdakkota
- Loading branch information
Showing
5 changed files
with
447 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
// Package autometric contains a simple reflect-based OpenTelemetry metric initializer. | ||
package autometric | ||
|
||
import ( | ||
"reflect" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/go-faster/errors" | ||
"go.opentelemetry.io/otel/metric" | ||
) | ||
|
||
var ( | ||
int64CounterType = reflect.TypeOf(new(metric.Int64Counter)).Elem() | ||
int64UpDownCounterType = reflect.TypeOf(new(metric.Int64UpDownCounter)).Elem() | ||
int64HistogramType = reflect.TypeOf(new(metric.Int64Histogram)).Elem() | ||
int64GaugeType = reflect.TypeOf(new(metric.Int64Gauge)).Elem() | ||
int64ObservableCounterType = reflect.TypeOf(new(metric.Int64ObservableCounter)).Elem() | ||
int64ObservableUpDownCounterType = reflect.TypeOf(new(metric.Int64ObservableUpDownCounter)).Elem() | ||
int64ObservableGaugeType = reflect.TypeOf(new(metric.Int64ObservableGauge)).Elem() | ||
) | ||
|
||
var ( | ||
float64CounterType = reflect.TypeOf(new(metric.Float64Counter)).Elem() | ||
float64UpDownCounterType = reflect.TypeOf(new(metric.Float64UpDownCounter)).Elem() | ||
float64HistogramType = reflect.TypeOf(new(metric.Float64Histogram)).Elem() | ||
float64GaugeType = reflect.TypeOf(new(metric.Float64Gauge)).Elem() | ||
float64ObservableCounterType = reflect.TypeOf(new(metric.Float64ObservableCounter)).Elem() | ||
float64ObservableUpDownCounterType = reflect.TypeOf(new(metric.Float64ObservableUpDownCounter)).Elem() | ||
float64ObservableGaugeType = reflect.TypeOf(new(metric.Float64ObservableGauge)).Elem() | ||
) | ||
|
||
// InitOptions defines options for [Init]. | ||
type InitOptions struct { | ||
// Prefix defines common prefix for all metrics. | ||
Prefix string | ||
// FieldName returns name for given field. | ||
FieldName func(prefix string, sf reflect.StructField) string | ||
} | ||
|
||
func (opts *InitOptions) setDefaults() { | ||
if opts.FieldName == nil { | ||
opts.FieldName = fieldName | ||
} | ||
} | ||
|
||
func fieldName(prefix string, sf reflect.StructField) string { | ||
name := snakeCase(sf.Name) | ||
if tag, ok := sf.Tag.Lookup("name"); ok { | ||
name = tag | ||
} | ||
return prefix + name | ||
} | ||
|
||
// Init initialize metrics in given struct s using given meter. | ||
func Init(m metric.Meter, s any, opts InitOptions) error { | ||
opts.setDefaults() | ||
|
||
ptr := reflect.ValueOf(s) | ||
if !isValidPtrStruct(ptr) { | ||
return errors.Errorf("a pointer-to-struct expected, got %T", s) | ||
} | ||
|
||
var ( | ||
struct_ = ptr.Elem() | ||
structType = struct_.Type() | ||
) | ||
for i := 0; i < struct_.NumField(); i++ { | ||
fieldType := structType.Field(i) | ||
if fieldType.Anonymous || !fieldType.IsExported() { | ||
continue | ||
} | ||
if n, ok := fieldType.Tag.Lookup("autometric"); ok && n == "-" { | ||
continue | ||
} | ||
|
||
field := struct_.Field(i) | ||
if !field.CanSet() { | ||
continue | ||
} | ||
|
||
mt, err := makeField(m, fieldType, opts) | ||
if err != nil { | ||
return errors.Wrapf(err, "field (%s).%s", structType, fieldType.Name) | ||
} | ||
field.Set(reflect.ValueOf(mt)) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func makeField(m metric.Meter, sf reflect.StructField, opts InitOptions) (any, error) { | ||
var ( | ||
name = opts.FieldName(opts.Prefix, sf) | ||
unit = sf.Tag.Get("unit") | ||
desc = sf.Tag.Get("description") | ||
boundaries []float64 | ||
) | ||
if b, ok := sf.Tag.Lookup("boundaries"); ok { | ||
switch ftyp := sf.Type; ftyp { | ||
case int64HistogramType, float64HistogramType: | ||
default: | ||
return nil, errors.Errorf("boundaries tag should be used only on histogram metrics: got %v", ftyp) | ||
} | ||
for _, val := range strings.Split(b, ",") { | ||
f, err := strconv.ParseFloat(val, 64) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "parse boundaries") | ||
} | ||
boundaries = append(boundaries, f) | ||
} | ||
} | ||
|
||
switch ftyp := sf.Type; ftyp { | ||
case int64CounterType: | ||
return m.Int64Counter(name, | ||
metric.WithUnit(unit), | ||
metric.WithDescription(desc), | ||
) | ||
case int64UpDownCounterType: | ||
return m.Int64UpDownCounter(name, | ||
metric.WithUnit(unit), | ||
metric.WithDescription(desc), | ||
) | ||
case int64HistogramType: | ||
return m.Int64Histogram(name, | ||
metric.WithUnit(unit), | ||
metric.WithDescription(desc), | ||
metric.WithExplicitBucketBoundaries(boundaries...), | ||
) | ||
case int64GaugeType: | ||
return m.Int64Gauge(name, | ||
metric.WithUnit(unit), | ||
metric.WithDescription(desc), | ||
) | ||
case int64ObservableCounterType, | ||
int64ObservableUpDownCounterType, | ||
int64ObservableGaugeType: | ||
return nil, errors.New("observables are not supported") | ||
|
||
case float64CounterType: | ||
return m.Float64Counter(name, | ||
metric.WithUnit(unit), | ||
metric.WithDescription(desc), | ||
) | ||
case float64UpDownCounterType: | ||
return m.Float64UpDownCounter(name, | ||
metric.WithUnit(unit), | ||
metric.WithDescription(desc), | ||
) | ||
case float64HistogramType: | ||
return m.Float64Histogram(name, | ||
metric.WithUnit(unit), | ||
metric.WithDescription(desc), | ||
metric.WithExplicitBucketBoundaries(boundaries...), | ||
) | ||
case float64GaugeType: | ||
return m.Float64Gauge(name, | ||
metric.WithUnit(unit), | ||
metric.WithDescription(desc), | ||
) | ||
case float64ObservableCounterType, | ||
float64ObservableUpDownCounterType, | ||
float64ObservableGaugeType: | ||
return nil, errors.New("observables are not supported") | ||
default: | ||
return nil, errors.Errorf("unexpected type %v", ftyp) | ||
} | ||
} | ||
|
||
func isValidPtrStruct(ptr reflect.Value) bool { | ||
return ptr.Kind() == reflect.Pointer && | ||
ptr.Elem().Kind() == reflect.Struct | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
// Package autometric contains a simple reflect-based OpenTelemetry metric initializer. | ||
package autometric | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
"go.opentelemetry.io/otel/metric" | ||
sdkmetric "go.opentelemetry.io/otel/sdk/metric" | ||
"go.opentelemetry.io/otel/sdk/metric/metricdata" | ||
) | ||
|
||
func TestInit(t *testing.T) { | ||
ctx := context.Background() | ||
|
||
reader := sdkmetric.NewManualReader() | ||
mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) | ||
meter := mp.Meter("test-meter") | ||
|
||
var test struct { | ||
// Ignored fields. | ||
_ int | ||
_ metric.Int64Counter | ||
// Embedded fields. | ||
fmt.Stringer | ||
// Private fields. | ||
private int | ||
privateCounter metric.Int64Counter | ||
// Skip. | ||
SkipMe metric.Int64Counter `autometric:"-"` | ||
SkipMe2 metric.Int64ObservableCounter `autometric:"-"` | ||
|
||
Int64Counter metric.Int64Counter | ||
Int64UpDownCounter metric.Int64UpDownCounter | ||
Int64Histogram metric.Int64Histogram | ||
Int64Gauge metric.Int64Gauge | ||
Float64Counter metric.Float64Counter | ||
Float64UpDownCounter metric.Float64UpDownCounter | ||
Float64Histogram metric.Float64Histogram | ||
Float64Gauge metric.Float64Gauge | ||
|
||
Renamed metric.Int64Counter `name:"mega_counter"` | ||
WithDesc metric.Int64Counter `name:"with_desc" description:"foo"` | ||
WithUnit metric.Int64Counter `name:"with_unit" unit:"By"` | ||
WithBounds metric.Float64Histogram `name:"with_bounds" boundaries:"1,2,5"` | ||
} | ||
const prefix = "testmetrics.points." | ||
require.NoError(t, Init(meter, &test, InitOptions{ | ||
Prefix: prefix, | ||
})) | ||
|
||
require.Nil(t, test.privateCounter) | ||
|
||
require.NotNil(t, test.Int64Counter) | ||
test.Int64Counter.Add(ctx, 1) | ||
require.NotNil(t, test.Int64UpDownCounter) | ||
test.Int64UpDownCounter.Add(ctx, 1) | ||
require.NotNil(t, test.Int64Histogram) | ||
test.Int64Histogram.Record(ctx, 1) | ||
require.NotNil(t, test.Int64Gauge) | ||
test.Int64Gauge.Record(ctx, 1) | ||
require.NotNil(t, test.Float64Counter) | ||
test.Float64Counter.Add(ctx, 1) | ||
require.NotNil(t, test.Float64UpDownCounter) | ||
test.Float64UpDownCounter.Add(ctx, 1) | ||
require.NotNil(t, test.Float64Histogram) | ||
test.Float64Histogram.Record(ctx, 1) | ||
require.NotNil(t, test.Float64Gauge) | ||
test.Float64Gauge.Record(ctx, 1) | ||
|
||
require.NotNil(t, test.Renamed) | ||
test.Renamed.Add(ctx, 1) | ||
require.NotNil(t, test.WithDesc) | ||
test.WithDesc.Add(ctx, 1) | ||
require.NotNil(t, test.WithUnit) | ||
test.WithUnit.Add(ctx, 1) | ||
require.NotNil(t, test.WithBounds) | ||
test.WithBounds.Record(ctx, 1) | ||
|
||
require.NoError(t, mp.ForceFlush(ctx)) | ||
var data metricdata.ResourceMetrics | ||
require.NoError(t, reader.Collect(ctx, &data)) | ||
|
||
type MetricInfo struct { | ||
Name string | ||
Description string | ||
Unit string | ||
} | ||
var infos []MetricInfo | ||
for _, scope := range data.ScopeMetrics { | ||
for _, metric := range scope.Metrics { | ||
infos = append(infos, MetricInfo{ | ||
Name: metric.Name, | ||
Description: metric.Description, | ||
Unit: metric.Unit, | ||
}) | ||
} | ||
} | ||
require.Equal(t, | ||
[]MetricInfo{ | ||
{Name: prefix + "int64_counter"}, | ||
{Name: prefix + "int64_up_down_counter"}, | ||
{Name: prefix + "int64_histogram"}, | ||
{Name: prefix + "int64_gauge"}, | ||
{Name: prefix + "float64_counter"}, | ||
{Name: prefix + "float64_up_down_counter"}, | ||
{Name: prefix + "float64_histogram"}, | ||
{Name: prefix + "float64_gauge"}, | ||
|
||
{Name: prefix + "mega_counter"}, | ||
{Name: prefix + "with_desc", Description: "foo"}, | ||
{Name: prefix + "with_unit", Unit: "By"}, | ||
{Name: prefix + "with_bounds"}, | ||
}, | ||
infos, | ||
) | ||
} | ||
|
||
func TestInitErrors(t *testing.T) { | ||
type ( | ||
JustStruct struct{} | ||
|
||
UnexpectedType struct { | ||
Foo metric.Observable | ||
} | ||
UnsupportedInt64Observable struct { | ||
Observable metric.Int64ObservableCounter | ||
} | ||
UnsupportedFloat64Observable struct { | ||
Observable metric.Float64ObservableCounter | ||
} | ||
|
||
BoundariesOnNonHistogram struct { | ||
C metric.Int64Counter `boundaries:"foo"` | ||
} | ||
|
||
BadBoundaries struct { | ||
H metric.Float64Histogram `boundaries:"foo"` | ||
} | ||
BadBoundaries2 struct { | ||
H metric.Float64Histogram `boundaries:"foo,"` | ||
} | ||
) | ||
|
||
for i, tt := range []struct { | ||
s any | ||
err string | ||
}{ | ||
{0, "a pointer-to-struct expected, got int"}, | ||
{JustStruct{}, "a pointer-to-struct expected, got autometric.JustStruct"}, | ||
|
||
{&UnexpectedType{}, "field (autometric.UnexpectedType).Foo: unexpected type metric.Observable"}, | ||
{&UnsupportedInt64Observable{}, "field (autometric.UnsupportedInt64Observable).Observable: observables are not supported"}, | ||
{&UnsupportedFloat64Observable{}, "field (autometric.UnsupportedFloat64Observable).Observable: observables are not supported"}, | ||
|
||
{&BoundariesOnNonHistogram{}, `field (autometric.BoundariesOnNonHistogram).C: boundaries tag should be used only on histogram metrics: got metric.Int64Counter`}, | ||
{&BadBoundaries{}, `field (autometric.BadBoundaries).H: parse boundaries: strconv.ParseFloat: parsing "foo": invalid syntax`}, | ||
{&BadBoundaries2{}, `field (autometric.BadBoundaries2).H: parse boundaries: strconv.ParseFloat: parsing "foo": invalid syntax`}, | ||
} { | ||
t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { | ||
mp := sdkmetric.NewMeterProvider() | ||
meter := mp.Meter("test-meter") | ||
require.EqualError(t, Init(meter, tt.s, InitOptions{}), tt.err) | ||
}) | ||
} | ||
} |
Oops, something went wrong.