package otel import ( "context" "net/http" "net/http/httptest" "testing" "github.com/openstatushq/openstatus/apps/checker/checker" "github.com/openstatushq/openstatus/apps/checker/request" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" sdkMetrics "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" ) func newTestMeter(t *testing.T) (metric.Meter, *sdkMetrics.ManualReader) { t.Helper() reader := sdkMetrics.NewManualReader() provider := sdkMetrics.NewMeterProvider(sdkMetrics.WithReader(reader)) t.Cleanup(func() { require.NoError(t, provider.Shutdown(context.Background())) }) return provider.Meter("test"), reader } func collectMetrics(t *testing.T, reader *sdkMetrics.ManualReader) metricdata.ResourceMetrics { t.Helper() var rm metricdata.ResourceMetrics require.NoError(t, reader.Collect(context.Background(), &rm)) return rm } func newOTLPTestServer(t *testing.T) *httptest.Server { t.Helper() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) t.Cleanup(server.Close) return server } // --- recordGauge tests --- func TestRecordGauge(t *testing.T) { meter, reader := newTestMeter(t) ctx := context.Background() att := metric.WithAttributes(attribute.String("test.key", "test-value")) err := recordGauge(ctx, meter, "test.gauge", "A test gauge", 42.5, att) require.NoError(t, err) rm := collectMetrics(t, reader) require.Len(t, rm.ScopeMetrics, 1) sm := rm.ScopeMetrics[0] require.Len(t, sm.Metrics, 1) m := sm.Metrics[0] assert.Equal(t, "test.gauge", m.Name) assert.Equal(t, "A test gauge", m.Description) assert.Equal(t, "ms", m.Unit) gauge, ok := m.Data.(metricdata.Gauge[float64]) require.True(t, ok, "expected Gauge[float64] data type") require.Len(t, gauge.DataPoints, 1) assert.Equal(t, 42.5, gauge.DataPoints[0].Value) attrs := gauge.DataPoints[0].Attributes val, found := attrs.Value(attribute.Key("test.key")) assert.True(t, found) assert.Equal(t, "test-value", val.AsString()) } func TestRecordGauge_MultipleMetrics(t *testing.T) { meter, reader := newTestMeter(t) ctx := context.Background() att := metric.WithAttributes(attribute.String("region", "us-east-1")) gauges := []struct { name string desc string value float64 }{ {"http.dns.duration", "DNS duration", 10.0}, {"http.tls.duration", "TLS duration", 25.0}, {"http.ttfb.duration", "TTFB duration", 50.0}, } for _, g := range gauges { require.NoError(t, recordGauge(ctx, meter, g.name, g.desc, g.value, att)) } rm := collectMetrics(t, reader) require.Len(t, rm.ScopeMetrics, 1) assert.Len(t, rm.ScopeMetrics[0].Metrics, 3) } // --- recordErrorCounter tests --- func TestRecordErrorCounter(t *testing.T) { meter, reader := newTestMeter(t) ctx := context.Background() att := metric.WithAttributes(attribute.String("region", "us-east-1")) recordErrorCounter(ctx, meter, att) rm := collectMetrics(t, reader) require.Len(t, rm.ScopeMetrics, 1) require.Len(t, rm.ScopeMetrics[0].Metrics, 1) m := rm.ScopeMetrics[0].Metrics[0] assert.Equal(t, "openstatus.error", m.Name) sum, ok := m.Data.(metricdata.Sum[int64]) require.True(t, ok, "expected Sum[int64] data type") require.Len(t, sum.DataPoints, 1) assert.Equal(t, int64(1), sum.DataPoints[0].Value) } // --- recordStatusCounter tests --- func TestRecordStatusCounter(t *testing.T) { meter, reader := newTestMeter(t) ctx := context.Background() att := metric.WithAttributes(attribute.String("region", "us-east-1")) recordStatusCounter(ctx, meter, att) rm := collectMetrics(t, reader) require.Len(t, rm.ScopeMetrics, 1) require.Len(t, rm.ScopeMetrics[0].Metrics, 1) m := rm.ScopeMetrics[0].Metrics[0] assert.Equal(t, "openstatus.status", m.Name) sum, ok := m.Data.(metricdata.Sum[int64]) require.True(t, ok, "expected Sum[int64] data type") require.Len(t, sum.DataPoints, 1) assert.Equal(t, int64(1), sum.DataPoints[0].Value) } // --- setupOTelSDK tests --- func TestSetupOTelSDK(t *testing.T) { server := newOTLPTestServer(t) ctx := context.Background() shutdown, err := setupOTelSDK(ctx, server.URL, nil) require.NoError(t, err) require.NotNil(t, shutdown) assert.NoError(t, shutdown(ctx)) } func TestSetupOTelSDK_InvalidURL(t *testing.T) { ctx := context.Background() shutdown, err := setupOTelSDK(ctx, "://invalid", nil) if err != nil { assert.Nil(t, shutdown, "shutdown should be nil when setup fails") } else { require.NotNil(t, shutdown) _ = shutdown(ctx) } } // --- withMeter tests --- func TestWithMeter(t *testing.T) { server := newOTLPTestServer(t) called := false withMeter(context.Background(), server.URL, nil, func(meter metric.Meter) { called = true assert.NotNil(t, meter) }) assert.True(t, called, "callback should be called") } func TestWithMeter_InvalidEndpoint(t *testing.T) { called := false // Must not panic. The OTLP exporter no longer fails at creation for // invalid URLs (it defers the error to export time), so the callback // will still be invoked. withMeter(context.Background(), "://invalid", nil, func(meter metric.Meter) { called = true }) assert.True(t, called, "callback should be called since exporter defers URL validation") } // --- RecordHTTPMetrics tests --- func TestRecordHTTPMetrics_Success(t *testing.T) { server := newOTLPTestServer(t) req := request.HttpCheckerRequest{ URL: "https://example.com", MonitorID: "mon-1", } req.OtelConfig.Endpoint = server.URL result := checker.Response{ Status: 200, Latency: 150, Timing: checker.Timing{ DnsStart: 0, DnsDone: 10, ConnectStart: 10, ConnectDone: 30, TlsHandshakeStart: 30, TlsHandshakeDone: 55, FirstByteStart: 55, FirstByteDone: 100, TransferStart: 100, TransferDone: 150, }, } // Should not panic. RecordHTTPMetrics(context.Background(), req, result, "us-east-1") } func TestRecordHTTPMetrics_Error(t *testing.T) { server := newOTLPTestServer(t) req := request.HttpCheckerRequest{ URL: "https://example.com", MonitorID: "mon-1", } req.OtelConfig.Endpoint = server.URL result := checker.Response{ Status: 500, Error: "connection refused", } // Should record error counter and not panic. RecordHTTPMetrics(context.Background(), req, result, "us-east-1") } func TestRecordHTTPMetrics_SetupFailure(t *testing.T) { req := request.HttpCheckerRequest{ URL: "https://example.com", MonitorID: "mon-1", } req.OtelConfig.Endpoint = "://invalid" result := checker.Response{ Status: 200, Latency: 100, } // Must not panic — this was the original nil pointer bug. RecordHTTPMetrics(context.Background(), req, result, "us-east-1") } // --- RecordTCPMetrics tests --- func TestRecordTCPMetrics_Success(t *testing.T) { server := newOTLPTestServer(t) req := request.TCPCheckerRequest{ URI: "example.com:443", MonitorID: "mon-2", } req.OtelConfig.Endpoint = server.URL result := checker.TCPResponse{ Latency: 45, Timing: checker.TCPResponseTiming{ TCPStart: 0, TCPDone: 45, }, } // Should not panic. RecordTCPMetrics(context.Background(), req, result, "us-east-1") } func TestRecordTCPMetrics_Error(t *testing.T) { server := newOTLPTestServer(t) req := request.TCPCheckerRequest{ URI: "example.com:443", MonitorID: "mon-2", } req.OtelConfig.Endpoint = server.URL result := checker.TCPResponse{ Error: 1, } // Should record error counter and not panic. RecordTCPMetrics(context.Background(), req, result, "us-east-1") } func TestRecordTCPMetrics_SetupFailure(t *testing.T) { req := request.TCPCheckerRequest{ URI: "example.com:443", MonitorID: "mon-2", } req.OtelConfig.Endpoint = "://invalid" result := checker.TCPResponse{ Latency: 45, } // Must not panic — same nil pointer guard as HTTP. RecordTCPMetrics(context.Background(), req, result, "us-east-1") } // --- RecordDNSMetrics tests --- func TestRecordDNSMetrics_Success(t *testing.T) { server := newOTLPTestServer(t) req := request.DNSCheckerRequest{ URI: "example.com", MonitorID: "mon-3", } req.OtelConfig.Endpoint = server.URL // Should not panic. RecordDNSMetrics(context.Background(), req, 30, false, "us-east-1") } func TestRecordDNSMetrics_Error(t *testing.T) { server := newOTLPTestServer(t) req := request.DNSCheckerRequest{ URI: "example.com", MonitorID: "mon-3", } req.OtelConfig.Endpoint = server.URL // Should record error counter and not panic. RecordDNSMetrics(context.Background(), req, 0, true, "us-east-1") } func TestRecordDNSMetrics_SetupFailure(t *testing.T) { req := request.DNSCheckerRequest{ URI: "example.com", MonitorID: "mon-3", } req.OtelConfig.Endpoint = "://invalid" // Must not panic — same nil pointer guard as HTTP. RecordDNSMetrics(context.Background(), req, 30, false, "us-east-1") }