package otelsetup import ( "context" "testing" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric/noop" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" ) // observerShape mirrors gitea.unbound.se/shiny/subscriptions.Observer. This // compile-time assertion guards that SubscriptionMetrics still satisfies that // interface structurally, without otelsetup importing the library. // // KEEP IN SYNC with subscriptions.Observer: this only proves SubscriptionMetrics // matches this local copy. The authoritative check that the local copy still // matches the real interface is the `subscriptions.WithObserver(...)` call site // in each consuming service — keep a `var _ subscriptions.Observer` guard there. type observerShape interface { Pushed(string) PushSkipped(string) Dropped(string) ChannelFull(string) } var _ observerShape = (*SubscriptionMetrics)(nil) // TestSubscriptionMetrics_DisabledProviderIsSafe proves the "recording is free // when metrics are disabled" claim: with the default global no-op provider, the // methods neither panic nor emit instruments. func TestSubscriptionMetrics_DisabledProviderIsSafe(t *testing.T) { otel.SetMeterProvider(noop.NewMeterProvider()) m, err := NewSubscriptionMetrics("entryBasesChanged") if err != nil { t.Fatalf("NewSubscriptionMetrics returned error: %v", err) } // Must not panic on the no-op provider. m.Pushed("c1") m.PushSkipped("c1") m.Dropped("c1") m.ChannelFull("c1") } func TestNewSubscriptionMetrics_RecordsOutcomes(t *testing.T) { reader := sdkmetric.NewManualReader() otel.SetMeterProvider(sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader))) m, err := NewSubscriptionMetrics("availableCompanies") if err != nil { t.Fatalf("NewSubscriptionMetrics returned error: %v", err) } // The subscriber key is ignored for metrics; different keys must not create // new series (cardinality guard is implicit — we only label by outcome). m.Pushed("c1") m.Pushed("c2") m.PushSkipped("c1") m.Dropped("c1") m.ChannelFull("c1") var rm metricdata.ResourceMetrics if err := reader.Collect(context.Background(), &rm); err != nil { t.Fatalf("collect: %v", err) } counts := map[string]int64{} dataPoints := 0 found := false for _, sm := range rm.ScopeMetrics { for _, md := range sm.Metrics { if md.Name != "subscription.notifications" { continue } found = true sum, ok := md.Data.(metricdata.Sum[int64]) if !ok { t.Fatalf("expected Sum[int64], got %T", md.Data) } for _, dp := range sum.DataPoints { dataPoints++ sub, _ := dp.Attributes.Value(attribute.Key("subscription")) if sub.AsString() != "availableCompanies" { t.Errorf("unexpected subscription attribute: %q", sub.AsString()) } outcome, _ := dp.Attributes.Value(attribute.Key("outcome")) counts[outcome.AsString()] += dp.Value } } } if !found { t.Fatal("subscription.notifications counter not emitted") } // One series per outcome, keyed by outcome only (not by subscriber key). if dataPoints != 4 { t.Errorf("expected 4 data points (one per outcome), got %d", dataPoints) } want := map[string]int64{"pushed": 2, "skipped": 1, "dropped": 1, "channel_full": 1} for k, v := range want { if counts[k] != v { t.Errorf("outcome %q: got %d, want %d", k, counts[k], v) } } }