blob: 4e61d2c77de6d28b0311c83706163f9be4da7560 [file]
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package otel
import (
"context"
"testing"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/core"
"golang.org/x/tools/internal/event/export/metric"
"golang.org/x/tools/internal/event/keys"
"golang.org/x/tools/internal/event/label"
)
// captureMetricData creates a metric using the production event system and returns the resulting data.
func captureMetricData(name string, setup func(*metric.Config), record func(context.Context)) metric.Data {
var captured metric.Data
output := func(ctx context.Context, ev core.Event, lm label.Map) context.Context {
if event.IsMetric(ev) {
if entries := metric.Entries.Get(lm); entries != nil {
for _, data := range entries.([]metric.Data) {
if data.Handle() == name {
captured = data
}
}
}
}
return ctx
}
cfg := metric.Config{}
setup(&cfg)
event.SetExporter(cfg.Exporter(output))
defer event.SetExporter(nil)
record(context.Background())
return captured
}
func TestConvertInt64Data_Counter(t *testing.T) {
key := keys.NewInt("value", "")
data := captureMetricData("request_count", func(cfg *metric.Config) {
metric.Scalar{
Name: "request_count",
Description: "Number of requests",
}.SumInt64(cfg, key)
}, func(ctx context.Context) {
event.Metric(ctx, key.Of(42))
}).(*metric.Int64Data)
m := convertInt64Data(data)
if m.Name != "request_count" {
t.Errorf("expected name 'request_count', got %q", m.Name)
}
if m.Description != "Number of requests" {
t.Errorf("expected description 'Number of requests', got %q", m.Description)
}
if m.Gauge != nil {
t.Error("expected Gauge to be nil for counter")
}
if m.Sum == nil {
t.Fatal("expected Sum to be non-nil for counter")
}
if len(m.Sum.DataPoints) != 1 {
t.Fatalf("expected 1 data point, got %d", len(m.Sum.DataPoints))
}
if m.Sum.DataPoints[0].AsInt == nil || *m.Sum.DataPoints[0].AsInt != 42 {
t.Errorf("expected value 42, got %v", m.Sum.DataPoints[0].AsInt)
}
if !m.Sum.IsMonotonic {
t.Error("expected IsMonotonic to be true for counter")
}
}
func TestConvertInt64Data_Gauge(t *testing.T) {
key := keys.NewInt("value", "")
data := captureMetricData("current_connections", func(cfg *metric.Config) {
metric.Scalar{Name: "current_connections"}.LatestInt64(cfg, key)
}, func(ctx context.Context) {
event.Metric(ctx, key.Of(100))
}).(*metric.Int64Data)
m := convertInt64Data(data)
if m.Sum != nil {
t.Error("expected Sum to be nil for gauge")
}
if m.Gauge == nil {
t.Fatal("expected Gauge to be non-nil")
}
if len(m.Gauge.DataPoints) != 1 {
t.Fatalf("expected 1 data point, got %d", len(m.Gauge.DataPoints))
}
if m.Gauge.DataPoints[0].AsInt == nil || *m.Gauge.DataPoints[0].AsInt != 100 {
t.Errorf("expected value 100, got %v", m.Gauge.DataPoints[0].AsInt)
}
}
func TestConvertFloat64Data(t *testing.T) {
key := keys.NewFloat("value", "")
data := captureMetricData("cpu_usage", func(cfg *metric.Config) {
metric.Scalar{Name: "cpu_usage"}.LatestFloat64(cfg, key)
}, func(ctx context.Context) {
event.Metric(ctx, key.Of(0.75))
}).(*metric.Float64Data)
m := convertFloat64Data(data)
if m.Gauge == nil {
t.Fatal("expected Gauge to be non-nil")
}
if len(m.Gauge.DataPoints) != 1 {
t.Fatalf("expected 1 data point, got %d", len(m.Gauge.DataPoints))
}
if m.Gauge.DataPoints[0].AsDouble == nil || *m.Gauge.DataPoints[0].AsDouble != 0.75 {
t.Errorf("expected value 0.75, got %v", m.Gauge.DataPoints[0].AsDouble)
}
}
func TestConvertHistogramInt64Data(t *testing.T) {
key := keys.NewInt("value", "")
buckets := []int64{10, 50, 100, 500}
// Record multiple values across different buckets:
// Bucket boundaries: <=10, <=50, <=100, <=500, >500
// Values: 5, 8 (<=10), 20, 30, 40 (<=50), 75 (<=100), 200, 300 (<=500), 1000 (>500)
values := []int{5, 8, 20, 30, 40, 75, 200, 300, 1000}
data := captureMetricData("response_time", func(cfg *metric.Config) {
metric.HistogramInt64{
Name: "response_time",
Description: "Response time in ms",
Buckets: buckets,
}.Record(cfg, key)
}, func(ctx context.Context) {
for _, v := range values {
event.Metric(ctx, key.Of(v))
}
}).(*metric.HistogramInt64Data)
m := convertHistogramInt64Data(data)
if m.Histogram == nil {
t.Fatal("expected Histogram to be non-nil")
}
dp := m.Histogram.DataPoints[0]
// Verify count
if dp.Count != 9 {
t.Errorf("expected count 9, got %d", dp.Count)
}
// Verify sum: 5+8+20+30+40+75+200+300+1000 = 1678
if dp.Sum == nil || *dp.Sum != 1678 {
t.Errorf("expected sum 1678, got %v", dp.Sum)
}
// Verify min/max
if dp.Min == nil || *dp.Min != 5 {
t.Errorf("expected min 5, got %v", dp.Min)
}
if dp.Max == nil || *dp.Max != 1000 {
t.Errorf("expected max 1000, got %v", dp.Max)
}
// Verify bucket counts (non-cumulative):
// <=10: 2 (5, 8)
// <=50: 3 (20, 30, 40)
// <=100: 1 (75)
// <=500: 2 (200, 300)
// >500: 1 (1000)
expectedBuckets := []intAsString{2, 3, 1, 2, 1}
if len(dp.BucketCounts) != len(expectedBuckets) {
t.Fatalf("expected %d buckets, got %d", len(expectedBuckets), len(dp.BucketCounts))
}
for i, expected := range expectedBuckets {
if dp.BucketCounts[i] != expected {
t.Errorf("bucket[%d]: expected %d, got %d", i, expected, dp.BucketCounts[i])
}
}
// Verify explicit bounds match configured buckets
expectedBounds := []float64{10, 50, 100, 500}
if len(dp.ExplicitBounds) != len(expectedBounds) {
t.Fatalf("expected %d bounds, got %d", len(expectedBounds), len(dp.ExplicitBounds))
}
for i, expected := range expectedBounds {
if dp.ExplicitBounds[i] != expected {
t.Errorf("bound[%d]: expected %v, got %v", i, expected, dp.ExplicitBounds[i])
}
}
}
func TestConvertInt64Data_WithAttributes(t *testing.T) {
valueKey := keys.NewInt("value", "")
methodKey := keys.NewString("method", "")
data := captureMetricData("requests_by_method", func(cfg *metric.Config) {
metric.Scalar{
Name: "requests_by_method",
Keys: []label.Key{methodKey}, // Group by method
}.SumInt64(cfg, valueKey)
}, func(ctx context.Context) {
event.Metric(ctx, valueKey.Of(10), methodKey.Of("GET"))
event.Metric(ctx, valueKey.Of(5), methodKey.Of("POST"))
}).(*metric.Int64Data)
m := convertInt64Data(data)
// Should have 2 data points (one per method)
if len(m.Sum.DataPoints) != 2 {
t.Fatalf("expected 2 data points, got %d", len(m.Sum.DataPoints))
}
// Verify attributes contain the method labels
for _, dp := range m.Sum.DataPoints {
if len(dp.Attributes) != 1 {
t.Errorf("expected 1 attribute, got %d", len(dp.Attributes))
}
if dp.Attributes[0].Key != "method" {
t.Errorf("expected attribute key 'method', got %q", dp.Attributes[0].Key)
}
// Value should be "GET" or "POST"
sv := dp.Attributes[0].Value.StringValue
if sv == nil || (*sv != "GET" && *sv != "POST") {
t.Errorf("unexpected method value: %v", sv)
}
}
}