265 lines
9.0 KiB
Go
265 lines
9.0 KiB
Go
package telemetry
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
|
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
|
)
|
|
|
|
func TestProcessConfigValidate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
cfg ProcessConfig
|
|
wantErr string
|
|
}{
|
|
{name: "defaults", cfg: ProcessConfig{TracesExporter: "none", MetricsExporter: "none"}},
|
|
{name: "otlp", cfg: ProcessConfig{TracesExporter: "otlp", MetricsExporter: "otlp", TracesProtocol: "grpc", MetricsProtocol: "http/protobuf"}},
|
|
{name: "bad traces exporter", cfg: ProcessConfig{TracesExporter: "weird", MetricsExporter: "none"}, wantErr: "unsupported traces exporter"},
|
|
{name: "bad metrics exporter", cfg: ProcessConfig{TracesExporter: "none", MetricsExporter: "weird"}, wantErr: "unsupported metrics exporter"},
|
|
{name: "bad traces protocol", cfg: ProcessConfig{TracesExporter: "none", MetricsExporter: "none", TracesProtocol: "xyz"}, wantErr: "OTLP traces protocol"},
|
|
{name: "bad metrics protocol", cfg: ProcessConfig{TracesExporter: "none", MetricsExporter: "none", MetricsProtocol: "xyz"}, wantErr: "OTLP metrics protocol"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := tt.cfg.Validate()
|
|
if tt.wantErr == "" {
|
|
require.NoError(t, err)
|
|
return
|
|
}
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.wantErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewProcessNoExporters(t *testing.T) {
|
|
runtime, err := NewProcess(context.Background(), ProcessConfig{
|
|
ServiceName: "galaxy-lobby-test",
|
|
TracesExporter: "none",
|
|
MetricsExporter: "none",
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, runtime)
|
|
|
|
assert.NotNil(t, runtime.TracerProvider())
|
|
assert.NotNil(t, runtime.MeterProvider())
|
|
|
|
require.NoError(t, runtime.Shutdown(context.Background()))
|
|
require.NoError(t, runtime.Shutdown(context.Background())) // idempotent
|
|
}
|
|
|
|
func TestNewProcessNilContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := NewProcess(nil, ProcessConfig{TracesExporter: "none", MetricsExporter: "none"}, nil) //nolint:staticcheck // test exercises the nil-context guard.
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "nil context")
|
|
}
|
|
|
|
func TestNewProcessInvalidConfig(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := NewProcess(context.Background(), ProcessConfig{TracesExporter: "weird", MetricsExporter: "none"}, nil)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "unsupported traces exporter")
|
|
}
|
|
|
|
func TestRuntimeNilSafeRecorders(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var runtime *Runtime
|
|
runtime.RecordPublicHTTPRequest(context.Background(), nil, 0)
|
|
runtime.RecordInternalHTTPRequest(context.Background(), nil, 0)
|
|
runtime.RecordGameTransition(context.Background(), "draft", "enrollment_open", "command")
|
|
runtime.RecordApplicationOutcome(context.Background(), "submitted")
|
|
runtime.RecordInviteOutcome(context.Background(), "created")
|
|
runtime.RecordMembershipChange(context.Background(), "activated")
|
|
runtime.RecordStartFlowOutcome(context.Background(), "running")
|
|
runtime.RecordNotificationPublish(context.Background(), "lobby.application.submitted", "ok")
|
|
runtime.RecordEnrollmentAutomationCheck(context.Background(), "no_op")
|
|
runtime.RecordRaceNameOutcome(context.Background(), "reserved")
|
|
runtime.RecordPendingRegistrationExpiration(context.Background(), "tick")
|
|
runtime.RecordUserLifecycleCascadeRelease(context.Background(), "permanent_blocked")
|
|
runtime.RecordCapabilityEvaluation(context.Background(), "capable")
|
|
require.Error(t, runtime.RegisterGauges(GaugeDependencies{}))
|
|
require.NoError(t, runtime.Shutdown(context.Background()))
|
|
}
|
|
|
|
func TestRuntimeLobbyCountersRegistered(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
reader := sdkmetric.NewManualReader()
|
|
provider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader))
|
|
t.Cleanup(func() { _ = provider.Shutdown(context.Background()) })
|
|
|
|
runtime, err := NewWithProviders(provider, nil)
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
runtime.RecordGameTransition(ctx, "draft", "enrollment_open", "command")
|
|
runtime.RecordApplicationOutcome(ctx, "submitted")
|
|
runtime.RecordInviteOutcome(ctx, "created")
|
|
runtime.RecordMembershipChange(ctx, "activated")
|
|
runtime.RecordStartFlowOutcome(ctx, "running")
|
|
runtime.RecordNotificationPublish(ctx, "lobby.application.submitted", "ok")
|
|
runtime.RecordEnrollmentAutomationCheck(ctx, "no_op")
|
|
runtime.RecordRaceNameOutcome(ctx, "reserved")
|
|
runtime.RecordPendingRegistrationExpiration(ctx, "tick")
|
|
runtime.RecordUserLifecycleCascadeRelease(ctx, "permanent_blocked")
|
|
runtime.RecordCapabilityEvaluation(ctx, "capable")
|
|
|
|
var rm metricdata.ResourceMetrics
|
|
require.NoError(t, reader.Collect(ctx, &rm))
|
|
|
|
got := collectedMetricNames(rm)
|
|
want := []string{
|
|
"lobby.application.outcomes",
|
|
"lobby.capability_evaluations",
|
|
"lobby.enrollment_automation.checks",
|
|
"lobby.game.transitions",
|
|
"lobby.invite.outcomes",
|
|
"lobby.membership.changes",
|
|
"lobby.notification.publish_attempts",
|
|
"lobby.pending_registration.expirations",
|
|
"lobby.race_name.outcomes",
|
|
"lobby.start_flow.outcomes",
|
|
"lobby.user_lifecycle.cascade_releases",
|
|
}
|
|
for _, name := range want {
|
|
assert.Contains(t, got, name)
|
|
}
|
|
}
|
|
|
|
type stubActiveGames map[string]int
|
|
|
|
func (s stubActiveGames) CountByStatus(_ context.Context) (map[string]int, error) {
|
|
return s, nil
|
|
}
|
|
|
|
type stubLag struct {
|
|
age time.Duration
|
|
ok bool
|
|
err error
|
|
}
|
|
|
|
func (s stubLag) OldestUnprocessedAge(_ context.Context, _ string, _ string) (time.Duration, bool, error) {
|
|
return s.age, s.ok, s.err
|
|
}
|
|
|
|
type stubOffsets struct{}
|
|
|
|
func (stubOffsets) Load(_ context.Context, _ string) (string, bool, error) {
|
|
return "0-0", true, nil
|
|
}
|
|
|
|
func TestRuntimeGaugesObserveCounts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
reader := sdkmetric.NewManualReader()
|
|
provider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader))
|
|
t.Cleanup(func() { _ = provider.Shutdown(context.Background()) })
|
|
|
|
runtime, err := NewWithProviders(provider, nil)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, runtime.RegisterGauges(GaugeDependencies{
|
|
ActiveGames: stubActiveGames{"running": 3, "paused": 1},
|
|
StreamLag: stubLag{age: 750 * time.Millisecond, ok: true},
|
|
Offsets: stubOffsets{},
|
|
GMEvents: StreamGaugeBinding{OffsetLabel: "gm_lobby_events", StreamName: "gm:lobby_events"},
|
|
RuntimeResults: StreamGaugeBinding{OffsetLabel: "runtime_results", StreamName: "runtime:job_results"},
|
|
UserLifecycle: StreamGaugeBinding{OffsetLabel: "user_lifecycle", StreamName: "user:lifecycle_events"},
|
|
}))
|
|
|
|
ctx := context.Background()
|
|
|
|
var rm metricdata.ResourceMetrics
|
|
require.NoError(t, reader.Collect(ctx, &rm))
|
|
|
|
gauges := collectedGauges(rm)
|
|
require.Contains(t, gauges, "lobby.active_games")
|
|
require.Contains(t, gauges, "lobby.gm_events.oldest_unprocessed_age_ms")
|
|
require.Contains(t, gauges, "lobby.runtime_results.oldest_unprocessed_age_ms")
|
|
require.Contains(t, gauges, "lobby.user_lifecycle.oldest_unprocessed_age_ms")
|
|
}
|
|
|
|
func TestRuntimeRegisterGaugesValidates(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime, err := NewWithProviders(sdkmetric.NewMeterProvider(), nil)
|
|
require.NoError(t, err)
|
|
|
|
cases := []struct {
|
|
name string
|
|
deps GaugeDependencies
|
|
}{
|
|
{"missing active games", GaugeDependencies{}},
|
|
{"missing stream lag", GaugeDependencies{ActiveGames: stubActiveGames{}}},
|
|
{"missing offsets", GaugeDependencies{ActiveGames: stubActiveGames{}, StreamLag: stubLag{}}},
|
|
{"missing gm events stream", GaugeDependencies{ActiveGames: stubActiveGames{}, StreamLag: stubLag{}, Offsets: stubOffsets{}}},
|
|
}
|
|
for _, tc := range cases {
|
|
err := runtime.RegisterGauges(tc.deps)
|
|
require.Error(t, err, tc.name)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeGaugeProbeErrorsAreLogged(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
reader := sdkmetric.NewManualReader()
|
|
provider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader))
|
|
t.Cleanup(func() { _ = provider.Shutdown(context.Background()) })
|
|
|
|
runtime, err := NewWithProviders(provider, nil)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, runtime.RegisterGauges(GaugeDependencies{
|
|
ActiveGames: stubActiveGames{"running": 0},
|
|
StreamLag: stubLag{err: errors.New("boom")},
|
|
Offsets: stubOffsets{},
|
|
GMEvents: StreamGaugeBinding{OffsetLabel: "gm_lobby_events", StreamName: "gm:lobby_events"},
|
|
RuntimeResults: StreamGaugeBinding{OffsetLabel: "runtime_results", StreamName: "runtime:job_results"},
|
|
UserLifecycle: StreamGaugeBinding{OffsetLabel: "user_lifecycle", StreamName: "user:lifecycle_events"},
|
|
}))
|
|
|
|
var rm metricdata.ResourceMetrics
|
|
require.NoError(t, reader.Collect(context.Background(), &rm))
|
|
}
|
|
|
|
func collectedMetricNames(rm metricdata.ResourceMetrics) []string {
|
|
var names []string
|
|
for _, scope := range rm.ScopeMetrics {
|
|
for _, m := range scope.Metrics {
|
|
names = append(names, m.Name)
|
|
}
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
func collectedGauges(rm metricdata.ResourceMetrics) map[string]struct{} {
|
|
gauges := map[string]struct{}{}
|
|
for _, scope := range rm.ScopeMetrics {
|
|
for _, m := range scope.Metrics {
|
|
if _, ok := m.Data.(metricdata.Gauge[int64]); ok {
|
|
gauges[m.Name] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
return gauges
|
|
}
|