package telemetry import ( "context" "testing" "time" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" ) func TestProcessConfigValidate(t *testing.T) { t.Parallel() require.NoError(t, ProcessConfig{ TracesExporter: "none", MetricsExporter: "none", }.Validate()) require.NoError(t, ProcessConfig{ TracesExporter: "otlp", MetricsExporter: "otlp", TracesProtocol: "grpc", MetricsProtocol: "http/protobuf", }.Validate()) require.Error(t, ProcessConfig{ TracesExporter: "stdout", MetricsExporter: "none", }.Validate()) require.Error(t, ProcessConfig{ TracesExporter: "none", MetricsExporter: "kafka", }.Validate()) require.Error(t, ProcessConfig{ TracesExporter: "otlp", MetricsExporter: "none", TracesProtocol: "thrift", }.Validate()) } func TestNewWithProvidersBuildsRuntime(t *testing.T) { t.Parallel() reader := metric.NewManualReader() meterProvider := metric.NewMeterProvider(metric.WithReader(reader)) runtime, err := NewWithProviders(meterProvider, nil) require.NoError(t, err) require.NotNil(t, runtime) require.NotNil(t, runtime.MeterProvider()) require.NotNil(t, runtime.TracerProvider()) } func TestRecordHelpersEmitInstruments(t *testing.T) { t.Parallel() reader := metric.NewManualReader() meterProvider := metric.NewMeterProvider(metric.WithReader(reader)) runtime, err := NewWithProviders(meterProvider, nil) require.NoError(t, err) ctx := context.Background() runtime.RecordInternalHTTPRequest(ctx, []attribute.KeyValue{ attribute.String("route", "/healthz"), attribute.String("method", "GET"), attribute.String("status_code", "200"), }, 10*time.Millisecond) runtime.RecordRegisterRuntimeOutcome(ctx, "success", "") runtime.RecordTurnGenerationOutcome(ctx, "success", "", "scheduler") runtime.RecordCommandExecuteOutcome(ctx, "success", "") runtime.RecordOrderPutOutcome(ctx, "success", "") runtime.RecordReportGetOutcome(ctx, "success", "") runtime.RecordBanishOutcome(ctx, "success", "") runtime.RecordHealthEventConsumed(ctx) runtime.RecordLobbyEventPublished(ctx, "runtime_snapshot_update") runtime.RecordNotificationPublishAttempt(ctx, "game.turn.ready", "ok") runtime.RecordMembershipCacheResult(ctx, "hit") runtime.RecordEngineCall(ctx, "init", 25*time.Millisecond) var rm metricdata.ResourceMetrics require.NoError(t, reader.Collect(ctx, &rm)) names := collectInstrumentNames(rm) expected := []string{ "gamemaster.internal_http.requests", "gamemaster.internal_http.duration", "gamemaster.register_runtime.outcomes", "gamemaster.turn_generation.outcomes", "gamemaster.command_execute.outcomes", "gamemaster.order_put.outcomes", "gamemaster.report_get.outcomes", "gamemaster.banish.outcomes", "gamemaster.health_events.consumed", "gamemaster.lobby_events.published", "gamemaster.notification.publish_attempts", "gamemaster.membership_cache.hits", "gamemaster.engine_call.latency", } for _, name := range expected { require.Contains(t, names, name, "expected instrument %s to be recorded", name) } } func collectInstrumentNames(rm metricdata.ResourceMetrics) map[string]struct{} { names := make(map[string]struct{}) for _, sm := range rm.ScopeMetrics { for _, m := range sm.Metrics { names[m.Name] = struct{}{} } } return names } type stubRuntimeProbe struct { counts map[string]int err error } func (probe stubRuntimeProbe) CountByStatus(_ context.Context) (map[string]int, error) { return probe.counts, probe.err } type stubSchedulerProbe struct { due int err error } func (probe stubSchedulerProbe) CountDue(_ context.Context) (int, error) { return probe.due, probe.err } type stubVersionsProbe struct { count int err error } func (probe stubVersionsProbe) CountVersions(_ context.Context) (int, error) { return probe.count, probe.err } func TestRegisterGaugesEmitsObservations(t *testing.T) { t.Parallel() reader := metric.NewManualReader() meterProvider := metric.NewMeterProvider(metric.WithReader(reader)) runtime, err := NewWithProviders(meterProvider, nil) require.NoError(t, err) require.NoError(t, runtime.RegisterGauges(GaugeDependencies{ RuntimeRecordsByStatus: stubRuntimeProbe{counts: map[string]int{"running": 3}}, SchedulerDueGames: stubSchedulerProbe{due: 2}, EngineVersionsTotal: stubVersionsProbe{count: 5}, })) var rm metricdata.ResourceMetrics require.NoError(t, reader.Collect(context.Background(), &rm)) names := collectInstrumentNames(rm) require.Contains(t, names, "gamemaster.runtime_records_by_status") require.Contains(t, names, "gamemaster.scheduler.due_games") require.Contains(t, names, "gamemaster.engine_versions_total") } func TestRegisterGaugesRejectsNilDependencies(t *testing.T) { t.Parallel() reader := metric.NewManualReader() meterProvider := metric.NewMeterProvider(metric.WithReader(reader)) runtime, err := NewWithProviders(meterProvider, nil) require.NoError(t, err) require.Error(t, runtime.RegisterGauges(GaugeDependencies{ SchedulerDueGames: stubSchedulerProbe{}, EngineVersionsTotal: stubVersionsProbe{}, })) require.Error(t, runtime.RegisterGauges(GaugeDependencies{ RuntimeRecordsByStatus: stubRuntimeProbe{}, EngineVersionsTotal: stubVersionsProbe{}, })) require.Error(t, runtime.RegisterGauges(GaugeDependencies{ RuntimeRecordsByStatus: stubRuntimeProbe{}, SchedulerDueGames: stubSchedulerProbe{}, })) }