diff --git a/contrib/opentelemetry/handler.go b/contrib/opentelemetry/handler.go index 959ebfb11..09722a7df 100644 --- a/contrib/opentelemetry/handler.go +++ b/contrib/opentelemetry/handler.go @@ -16,9 +16,10 @@ var _ client.MetricsHandler = MetricsHandler{} // MetricsHandler is an implementation of client.MetricsHandler // for open telemetry. type MetricsHandler struct { - meter metric.Meter - attributes attribute.Set - onError func(error) + meter metric.Meter + attributes attribute.Set + onError func(error) + useMonotonicCounters bool } // MetricsHandlerOptions are options provided to NewMetricsHandler. @@ -34,6 +35,13 @@ type MetricsHandlerOptions struct { // // Optional: Defaults to panicking on any error. OnError func(error) + // UseMonotonicCounters controls whether counters are created as monotonic + // Int64Counter (true) or non-monotonic Int64UpDownCounter (false). + // When true, Prometheus will correctly report counters with the _total + // suffix instead of treating them as gauges. + // + // Optional: Defaults to false for backward compatibility. + UseMonotonicCounters bool } // NewMetricsHandler returns a client.MetricsHandler that is backed by the given Meter @@ -45,9 +53,10 @@ func NewMetricsHandler(options MetricsHandlerOptions) MetricsHandler { options.OnError = func(err error) { panic(err) } } return MetricsHandler{ - meter: options.Meter, - attributes: options.InitialAttributes, - onError: options.OnError, + meter: options.Meter, + attributes: options.InitialAttributes, + onError: options.OnError, + useMonotonicCounters: options.UseMonotonicCounters, } } @@ -89,13 +98,24 @@ func (m MetricsHandler) WithTags(tags map[string]string) client.MetricsHandler { attributes = append(attributes, attribute.String(k, v)) } return MetricsHandler{ - meter: m.meter, - attributes: attribute.NewSet(attributes...), - onError: m.onError, + meter: m.meter, + attributes: attribute.NewSet(attributes...), + onError: m.onError, + useMonotonicCounters: m.useMonotonicCounters, } } func (m MetricsHandler) Counter(name string) client.MetricsCounter { + if m.useMonotonicCounters { + c, err := m.meter.Int64Counter(name) + if err != nil { + m.onError(err) + return client.MetricsNopHandler.Counter(name) + } + return metrics.CounterFunc(func(d int64) { + c.Add(context.Background(), d, metric.WithAttributeSet(m.attributes)) + }) + } c, err := m.meter.Int64UpDownCounter(name) if err != nil { m.onError(err) diff --git a/contrib/opentelemetry/handler_test.go b/contrib/opentelemetry/handler_test.go index afe0c6229..f053ef612 100644 --- a/contrib/opentelemetry/handler_test.go +++ b/contrib/opentelemetry/handler_test.go @@ -139,6 +139,49 @@ func TestGaugeHandler(t *testing.T) { metricdatatest.AssertEqual(t, want, metrics[0], metricdatatest.IgnoreTimestamp()) } +func TestMonotonicCounterHandler(t *testing.T) { + ctx := context.Background() + metricReader := metric.NewManualReader() + meterProvider := metric.NewMeterProvider(metric.WithReader(metricReader)) + handler := opentelemetry.NewMetricsHandler( + opentelemetry.MetricsHandlerOptions{ + Meter: meterProvider.Meter("test"), + UseMonotonicCounters: true, + }, + ) + // Emit some values + testCounter := handler.WithTags(map[string]string{"tag1": "value1"}).Counter("testCounter") + testCounter.Inc(1) + testCounter.Inc(1) + // Emit some values with different tags + testCounter2 := handler.WithTags(map[string]string{"tag1": "value2"}).Counter("testCounter") + testCounter2.Inc(5) + // Assert result + var rm metricdata.ResourceMetrics + metricReader.Collect(ctx, &rm) + assert.Len(t, rm.ScopeMetrics, 1) + metrics := rm.ScopeMetrics[0].Metrics + assert.Len(t, metrics, 1) + want := metricdata.Metrics{ + Name: "testCounter", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Value: 2, + Attributes: attribute.NewSet(attribute.String("tag1", "value1")), + }, + { + Value: 5, + Attributes: attribute.NewSet(attribute.String("tag1", "value2")), + }, + }, + }, + } + metricdatatest.AssertEqual(t, want, metrics[0], metricdatatest.IgnoreTimestamp()) +} + func TestTimerHandler(t *testing.T) { ctx := context.Background() metricReader := metric.NewManualReader()