feat: game lobby service

This commit is contained in:
Ilia Denisov
2026-04-25 23:20:55 +02:00
committed by GitHub
parent 32dc29359a
commit 48b0056b49
336 changed files with 57074 additions and 1418 deletions
@@ -0,0 +1,44 @@
// Package metricsintentpub wraps a ports.IntentPublisher with the
// `lobby.notification.publish_attempts` counter from
// `lobby/README.md` §Observability.
package metricsintentpub
import (
"context"
"galaxy/lobby/internal/ports"
"galaxy/lobby/internal/telemetry"
"galaxy/notificationintent"
)
// Publisher decorates an inner ports.IntentPublisher and increments
// `lobby.notification.publish_attempts` after each call.
type Publisher struct {
inner ports.IntentPublisher
telemetry *telemetry.Runtime
}
// New constructs one Publisher around inner. When telemetryRuntime is nil,
// the wrapper still delegates Publish but does not record metrics.
func New(inner ports.IntentPublisher, telemetryRuntime *telemetry.Runtime) *Publisher {
return &Publisher{inner: inner, telemetry: telemetryRuntime}
}
// Publish forwards intent to the inner publisher and records the attempt
// outcome under the frozen `result` attribute (`ok`/`error`).
func (publisher *Publisher) Publish(ctx context.Context, intent notificationintent.Intent) (string, error) {
if publisher == nil || publisher.inner == nil {
return "", nil
}
id, err := publisher.inner.Publish(ctx, intent)
result := "ok"
if err != nil {
result = "error"
}
publisher.telemetry.RecordNotificationPublish(ctx, string(intent.NotificationType), result)
return id, err
}
// Compile-time interface assertion.
var _ ports.IntentPublisher = (*Publisher)(nil)
@@ -0,0 +1,110 @@
package metricsintentpub_test
import (
"context"
"errors"
"testing"
"galaxy/lobby/internal/adapters/metricsintentpub"
"galaxy/lobby/internal/telemetry"
"galaxy/notificationintent"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
)
type fakePublisher struct {
id string
err error
}
func (f fakePublisher) Publish(_ context.Context, _ notificationintent.Intent) (string, error) {
return f.id, f.err
}
func TestPublisherForwardsAndRecordsOK(t *testing.T) {
t.Parallel()
reader := sdkmetric.NewManualReader()
provider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader))
t.Cleanup(func() { _ = provider.Shutdown(context.Background()) })
runtime, err := telemetry.NewWithProviders(provider, nil)
require.NoError(t, err)
pub := metricsintentpub.New(fakePublisher{id: "0-1"}, runtime)
id, err := pub.Publish(context.Background(), notificationintent.Intent{
NotificationType: notificationintent.NotificationTypeLobbyApplicationSubmitted,
})
require.NoError(t, err)
assert.Equal(t, "0-1", id)
rm := collect(t, reader)
require.Contains(t, sumValues(rm, "lobby.notification.publish_attempts"), counterPoint{
notificationType: "lobby.application.submitted",
result: "ok",
value: 1,
})
}
func TestPublisherRecordsErrorOnInnerFailure(t *testing.T) {
t.Parallel()
reader := sdkmetric.NewManualReader()
provider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader))
t.Cleanup(func() { _ = provider.Shutdown(context.Background()) })
runtime, err := telemetry.NewWithProviders(provider, nil)
require.NoError(t, err)
pub := metricsintentpub.New(fakePublisher{err: errors.New("boom")}, runtime)
_, err = pub.Publish(context.Background(), notificationintent.Intent{
NotificationType: notificationintent.NotificationTypeLobbyApplicationSubmitted,
})
require.Error(t, err)
rm := collect(t, reader)
require.Contains(t, sumValues(rm, "lobby.notification.publish_attempts"), counterPoint{
notificationType: "lobby.application.submitted",
result: "error",
value: 1,
})
}
type counterPoint struct {
notificationType string
result string
value int64
}
func collect(t *testing.T, reader sdkmetric.Reader) metricdata.ResourceMetrics {
t.Helper()
var rm metricdata.ResourceMetrics
require.NoError(t, reader.Collect(context.Background(), &rm))
return rm
}
func sumValues(rm metricdata.ResourceMetrics, name string) []counterPoint {
var points []counterPoint
for _, scope := range rm.ScopeMetrics {
for _, m := range scope.Metrics {
if m.Name != name {
continue
}
sum, ok := m.Data.(metricdata.Sum[int64])
if !ok {
continue
}
for _, point := range sum.DataPoints {
notificationType, _ := point.Attributes.Value("notification_type")
result, _ := point.Attributes.Value("result")
points = append(points, counterPoint{
notificationType: notificationType.AsString(),
result: result.AsString(),
value: point.Value,
})
}
}
}
return points
}