package telemetry import ( "bytes" "context" "io" "log/slog" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" sdktrace "go.opentelemetry.io/otel/sdk/trace" ) func TestNewProcessBuildsWithoutExporters(t *testing.T) { t.Parallel() runtime, err := newProcess(context.Background(), ProcessConfig{ ServiceName: "galaxy-user-test", TracesExporter: processExporterNone, MetricsExporter: processExporterNone, }, slog.New(slog.NewTextHandler(io.Discard, nil)), io.Discard, io.Discard) require.NoError(t, err) assert.NotNil(t, runtime.TracerProvider()) assert.NotNil(t, runtime.MeterProvider()) assert.NotNil(t, runtime.Handler()) require.NoError(t, runtime.Shutdown(context.Background())) require.NoError(t, runtime.Shutdown(context.Background())) } func TestNewProcessBuildsWithStdoutExporters(t *testing.T) { t.Parallel() traceBuffer := &bytes.Buffer{} metricBuffer := &bytes.Buffer{} runtime, err := newProcess(context.Background(), ProcessConfig{ ServiceName: "galaxy-user-test", TracesExporter: processExporterNone, MetricsExporter: processExporterNone, StdoutTracesEnabled: true, StdoutMetricsEnabled: true, }, slog.New(slog.NewTextHandler(io.Discard, nil)), traceBuffer, metricBuffer) require.NoError(t, err) ctx, span := runtime.TracerProvider().Tracer("test").Start(context.Background(), "internal-request") runtime.RecordUserCreationOutcome(ctx, "created") span.End() require.NoError(t, runtime.Shutdown(context.Background())) assert.NotEmpty(t, traceBuffer.String()) assert.NotEmpty(t, metricBuffer.String()) } func TestNewPreservesBusinessMetrics(t *testing.T) { t.Parallel() reader := sdkmetric.NewManualReader() meterProvider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) tracerProvider := sdktrace.NewTracerProvider() runtime, err := NewWithProviders(meterProvider, tracerProvider) require.NoError(t, err) runtime.RecordInternalHTTPRequest(context.Background(), []attribute.KeyValue{ attribute.String("route", "/api/v1/internal/users/:user_id/exists"), attribute.String("method", "GET"), attribute.String("edge_outcome", "success"), }, 125*time.Millisecond) runtime.RecordAuthResolutionOutcome(context.Background(), "resolve_by_email", "existing") runtime.RecordUserCreationOutcome(context.Background(), "created") runtime.RecordRaceNameReservationConflict(context.Background(), "update_my_profile") runtime.RecordEntitlementMutation(context.Background(), "grant", "success") runtime.RecordSanctionMutation(context.Background(), "apply", "conflict") runtime.RecordLimitMutation(context.Background(), "remove", "subject_not_found") runtime.RecordEventPublicationFailure(context.Background(), "user.profile.changed") assertMetricCount(t, reader, "user.internal_http.requests", map[string]string{ "route": "/api/v1/internal/users/:user_id/exists", "method": "GET", "edge_outcome": "success", }, 1) assertHistogramCount(t, reader, "user.internal_http.duration", map[string]string{ "route": "/api/v1/internal/users/:user_id/exists", "method": "GET", "edge_outcome": "success", }, 1) assertMetricCount(t, reader, "user.auth_resolution.outcomes", map[string]string{ "operation": "resolve_by_email", "outcome": "existing", }, 1) assertMetricCount(t, reader, "user.user_creation.outcomes", map[string]string{ "outcome": "created", }, 1) assertMetricCount(t, reader, "user.race_name.reservation_conflicts", map[string]string{ "operation": "update_my_profile", }, 1) assertMetricCount(t, reader, "user.entitlement.mutations", map[string]string{ "command": "grant", "outcome": "success", }, 1) assertMetricCount(t, reader, "user.sanction.mutations", map[string]string{ "command": "apply", "outcome": "conflict", }, 1) assertMetricCount(t, reader, "user.limit.mutations", map[string]string{ "command": "remove", "outcome": "subject_not_found", }, 1) assertMetricCount(t, reader, "user.event_publication_failures", map[string]string{ "event_type": "user.profile.changed", }, 1) } func assertMetricCount(t *testing.T, reader *sdkmetric.ManualReader, metricName string, wantAttrs map[string]string, wantValue int64) { t.Helper() var resourceMetrics metricdata.ResourceMetrics require.NoError(t, reader.Collect(context.Background(), &resourceMetrics)) for _, scopeMetrics := range resourceMetrics.ScopeMetrics { for _, metric := range scopeMetrics.Metrics { if metric.Name != metricName { continue } sum, ok := metric.Data.(metricdata.Sum[int64]) require.True(t, ok) for _, point := range sum.DataPoints { if hasMetricAttributes(point.Attributes.ToSlice(), wantAttrs) { assert.Equal(t, wantValue, point.Value) return } } } } require.Failf(t, "test failed", "metric %q with attrs %v not found", metricName, wantAttrs) } func assertHistogramCount(t *testing.T, reader *sdkmetric.ManualReader, metricName string, wantAttrs map[string]string, wantCount uint64) { t.Helper() var resourceMetrics metricdata.ResourceMetrics require.NoError(t, reader.Collect(context.Background(), &resourceMetrics)) for _, scopeMetrics := range resourceMetrics.ScopeMetrics { for _, metric := range scopeMetrics.Metrics { if metric.Name != metricName { continue } histogram, ok := metric.Data.(metricdata.Histogram[float64]) require.True(t, ok) for _, point := range histogram.DataPoints { if hasMetricAttributes(point.Attributes.ToSlice(), wantAttrs) { assert.Equal(t, wantCount, point.Count) return } } } } require.Failf(t, "test failed", "histogram %q with attrs %v not found", metricName, wantAttrs) } func hasMetricAttributes(values []attribute.KeyValue, want map[string]string) bool { if len(values) != len(want) { return false } for _, value := range values { if want[string(value.Key)] != value.Value.AsString() { return false } } return true }