opentelemetry-collector/service/telemetry/metrics_test.go

302 lines
9.7 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package telemetry
import (
"context"
"fmt"
"net/http"
"testing"
"time"
io_prometheus_client "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
config "go.opentelemetry.io/contrib/otelconf/v0.3.0"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
"go.opentelemetry.io/collector/config/configtelemetry"
"go.opentelemetry.io/collector/service/internal/promtest"
)
const (
metricPrefix = "otelcol_"
otelPrefix = "otel_sdk_"
grpcPrefix = "grpc_"
httpPrefix = "http_"
counterName = "test_counter"
)
var testInstanceID = "test_instance_id"
func TestTelemetryInit(t *testing.T) {
type metricValue struct {
value float64
labels map[string]string
}
for _, tt := range []struct {
name string
expectedMetrics map[string]metricValue
}{
{
name: "UseOpenTelemetryForInternalMetrics",
expectedMetrics: map[string]metricValue{
metricPrefix + otelPrefix + counterName: {
value: 13,
labels: map[string]string{
"service_name": "otelcol",
"service_version": "latest",
"service_instance_id": testInstanceID,
},
},
metricPrefix + grpcPrefix + counterName: {
value: 11,
labels: map[string]string{
"net_sock_peer_addr": "",
"net_sock_peer_name": "",
"net_sock_peer_port": "",
"service_name": "otelcol",
"service_version": "latest",
"service_instance_id": testInstanceID,
},
},
metricPrefix + httpPrefix + counterName: {
value: 10,
labels: map[string]string{
"net_host_name": "",
"net_host_port": "",
"service_name": "otelcol",
"service_version": "latest",
"service_instance_id": testInstanceID,
},
},
"target_info": {
value: 0,
labels: map[string]string{
"service_name": "otelcol",
"service_version": "latest",
"service_instance_id": testInstanceID,
},
},
"promhttp_metric_handler_errors_total": {
value: 0,
labels: map[string]string{
"cause": "encoding",
},
},
},
},
} {
prom := promtest.GetAvailableLocalAddressPrometheus(t)
endpoint := fmt.Sprintf("http://%s:%d/metrics", *prom.Host, *prom.Port)
cfg := Config{
Metrics: MetricsConfig{
Level: configtelemetry.LevelDetailed,
MeterProvider: config.MeterProvider{
Readers: []config.MetricReader{{
Pull: &config.PullMetricReader{
Exporter: config.PullMetricExporter{Prometheus: prom},
},
}},
},
},
}
t.Run(tt.name, func(t *testing.T) {
sdk, err := config.NewSDK(
config.WithContext(context.Background()),
config.WithOpenTelemetryConfiguration(config.OpenTelemetryConfiguration{
MeterProvider: &config.MeterProvider{
Readers: cfg.Metrics.Readers,
},
Resource: &config.Resource{
SchemaUrl: ptr(""),
Attributes: []config.AttributeNameValue{
{Name: string(semconv.ServiceInstanceIDKey), Value: testInstanceID},
{Name: string(semconv.ServiceNameKey), Value: "otelcol"},
{Name: string(semconv.ServiceVersionKey), Value: "latest"},
},
},
}),
)
require.NoError(t, err)
mp, err := newMeterProvider(Settings{SDK: &sdk}, cfg)
require.NoError(t, err)
defer func() {
if prov, ok := mp.(interface{ Shutdown(context.Context) error }); ok {
require.NoError(t, prov.Shutdown(context.Background()))
}
}()
createTestMetrics(t, mp)
metrics := getMetricsFromPrometheus(t, endpoint)
require.Len(t, metrics, len(tt.expectedMetrics))
for metricName, metricValue := range tt.expectedMetrics {
mf, present := metrics[metricName]
require.True(t, present, "expected metric %q was not present", metricName)
if metricName == "promhttp_metric_handler_errors_total" {
continue
}
require.Len(t, mf.Metric, 1, "only one measure should exist for metric %q", metricName)
labels := make(map[string]string)
for _, pair := range mf.Metric[0].Label {
labels[pair.GetName()] = pair.GetValue()
}
require.Equal(t, metricValue.labels, labels, "labels for metric %q was different than expected", metricName)
require.InDelta(t, metricValue.value, mf.Metric[0].Counter.GetValue(), 0.01, "value for metric %q was different than expected", metricName)
}
})
}
}
func createTestMetrics(t *testing.T, mp metric.MeterProvider) {
// Creates a OTel Go counter
counter, err := mp.Meter("collector_test").Int64Counter(metricPrefix+otelPrefix+counterName, metric.WithUnit("ms"))
require.NoError(t, err)
counter.Add(context.Background(), 13)
grpcExampleCounter, err := mp.Meter("go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc").Int64Counter(metricPrefix + grpcPrefix + counterName)
require.NoError(t, err)
grpcExampleCounter.Add(context.Background(), 11, metric.WithAttributeSet(attribute.NewSet(
attribute.String(string(semconv.NetSockPeerAddrKey), ""),
attribute.String(string(semconv.NetSockPeerPortKey), ""),
attribute.String(string(semconv.NetSockPeerNameKey), ""),
)))
httpExampleCounter, err := mp.Meter("go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp").Int64Counter(metricPrefix + httpPrefix + counterName)
require.NoError(t, err)
httpExampleCounter.Add(context.Background(), 10, metric.WithAttributeSet(attribute.NewSet(
attribute.String(string(semconv.NetHostNameKey), ""),
attribute.String(string(semconv.NetHostPortKey), ""),
)))
}
func getMetricsFromPrometheus(t *testing.T, endpoint string) map[string]*io_prometheus_client.MetricFamily {
client := &http.Client{
Timeout: 10 * time.Second,
}
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
require.NoError(t, err)
var rr *http.Response
maxRetries := 5
for i := 0; i < maxRetries; i++ {
rr, err = client.Do(req)
if err == nil && rr.StatusCode == http.StatusOK {
break
}
// skip sleep on last retry
if i < maxRetries-1 {
time.Sleep(2 * time.Second) // Wait before retrying
}
}
require.NoError(t, err, "failed to get metrics from Prometheus after %d attempts", maxRetries)
require.Equal(t, http.StatusOK, rr.StatusCode, "unexpected status code after %d attempts", maxRetries)
defer rr.Body.Close()
var parser expfmt.TextParser
parsed, err := parser.TextToMetricFamilies(rr.Body)
require.NoError(t, err)
return parsed
}
// Test that the MeterProvider implements the 'Enabled' functionality.
// See https://pkg.go.dev/go.opentelemetry.io/otel/sdk/metric/internal/x#readme-instrument-enabled.
func TestInstrumentEnabled(t *testing.T) {
prom := promtest.GetAvailableLocalAddressPrometheus(t)
cfg := Config{
Metrics: MetricsConfig{
Level: configtelemetry.LevelDetailed,
MeterProvider: config.MeterProvider{
Readers: []config.MetricReader{{
Pull: &config.PullMetricReader{Exporter: config.PullMetricExporter{Prometheus: prom}},
}},
},
},
}
sdk, err := config.NewSDK(
config.WithContext(context.Background()),
config.WithOpenTelemetryConfiguration(config.OpenTelemetryConfiguration{
MeterProvider: &config.MeterProvider{
Readers: cfg.Metrics.Readers,
},
Resource: &config.Resource{
SchemaUrl: ptr(""),
Attributes: []config.AttributeNameValue{
{Name: string(semconv.ServiceInstanceIDKey), Value: testInstanceID},
{Name: string(semconv.ServiceNameKey), Value: "otelcol"},
{Name: string(semconv.ServiceVersionKey), Value: "latest"},
},
},
}),
)
require.NoError(t, err)
meterProvider, err := newMeterProvider(Settings{SDK: &sdk}, cfg)
defer func() {
if prov, ok := meterProvider.(interface{ Shutdown(context.Context) error }); ok {
require.NoError(t, prov.Shutdown(context.Background()))
}
}()
require.NoError(t, err)
meter := meterProvider.Meter("go.opentelemetry.io/collector/service/telemetry")
type enabledInstrument interface{ Enabled(context.Context) bool }
intCnt, err := meter.Int64Counter("int64.counter")
require.NoError(t, err)
_, ok := intCnt.(enabledInstrument)
assert.True(t, ok, "Int64Counter does not implement the experimental 'Enabled' method")
intUpDownCnt, err := meter.Int64UpDownCounter("int64.updowncounter")
require.NoError(t, err)
_, ok = intUpDownCnt.(enabledInstrument)
assert.True(t, ok, "Int64UpDownCounter does not implement the experimental 'Enabled' method")
intHist, err := meter.Int64Histogram("int64.updowncounter")
require.NoError(t, err)
_, ok = intHist.(enabledInstrument)
assert.True(t, ok, "Int64Histogram does not implement the experimental 'Enabled' method")
intGauge, err := meter.Int64Gauge("int64.updowncounter")
require.NoError(t, err)
_, ok = intGauge.(enabledInstrument)
assert.True(t, ok, "Int64Gauge does not implement the experimental 'Enabled' method")
floatCnt, err := meter.Float64Counter("int64.updowncounter")
require.NoError(t, err)
_, ok = floatCnt.(enabledInstrument)
assert.True(t, ok, "Float64Counter does not implement the experimental 'Enabled' method")
floatUpDownCnt, err := meter.Float64UpDownCounter("int64.updowncounter")
require.NoError(t, err)
_, ok = floatUpDownCnt.(enabledInstrument)
assert.True(t, ok, "Float64UpDownCounter does not implement the experimental 'Enabled' method")
floatHist, err := meter.Float64Histogram("int64.updowncounter")
require.NoError(t, err)
_, ok = floatHist.(enabledInstrument)
assert.True(t, ok, "Float64Histogram does not implement the experimental 'Enabled' method")
floatGauge, err := meter.Float64Gauge("int64.updowncounter")
require.NoError(t, err)
_, ok = floatGauge.(enabledInstrument)
assert.True(t, ok, "Float64Gauge does not implement the experimental 'Enabled' method")
}
func ptr[T any](v T) *T {
return &v
}