feat: game lobby service
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package logging
|
||||
|
||||
import "context"
|
||||
|
||||
// requestIDKey is the unexported context key under which the HTTP layer
|
||||
// stores the request id propagated from the X-Request-Id header.
|
||||
type requestIDKey struct{}
|
||||
|
||||
// WithRequestID returns a child context that carries requestID. An empty
|
||||
// requestID returns ctx unchanged so callers do not have to branch.
|
||||
func WithRequestID(ctx context.Context, requestID string) context.Context {
|
||||
if ctx == nil || requestID == "" {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, requestIDKey{}, requestID)
|
||||
}
|
||||
|
||||
// RequestIDFromContext returns the request id stored on ctx by
|
||||
// WithRequestID, or an empty string when no value is present.
|
||||
func RequestIDFromContext(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
value, _ := ctx.Value(requestIDKey{}).(string)
|
||||
return value
|
||||
}
|
||||
|
||||
// ContextAttrs returns slog key-value pairs that materialise the frozen
|
||||
// `lobby/README.md` §Observability log fields `request_id`, `trace_id`,
|
||||
// and `span_id` from ctx. Pairs whose value is empty are omitted so logs
|
||||
// stay tight.
|
||||
func ContextAttrs(ctx context.Context) []any {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var attrs []any
|
||||
if requestID := RequestIDFromContext(ctx); requestID != "" {
|
||||
attrs = append(attrs, "request_id", requestID)
|
||||
}
|
||||
attrs = append(attrs, TraceAttrsFromContext(ctx)...)
|
||||
return attrs
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package logging_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"galaxy/lobby/internal/logging"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func TestRequestIDRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := logging.WithRequestID(context.Background(), "rid-1")
|
||||
assert.Equal(t, "rid-1", logging.RequestIDFromContext(ctx))
|
||||
|
||||
assert.Equal(t, "", logging.RequestIDFromContext(context.Background()))
|
||||
}
|
||||
|
||||
func TestWithRequestIDIgnoresEmptyValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
parent := context.Background()
|
||||
ctx := logging.WithRequestID(parent, "")
|
||||
assert.Equal(t, parent, ctx)
|
||||
}
|
||||
|
||||
func TestContextAttrsIncludesRequestID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := logging.WithRequestID(context.Background(), "rid-2")
|
||||
attrs := logging.ContextAttrs(ctx)
|
||||
require.Contains(t, attrs, "request_id")
|
||||
require.Contains(t, attrs, "rid-2")
|
||||
}
|
||||
|
||||
func TestContextAttrsIncludesTraceID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
traceID, err := trace.TraceIDFromHex("0123456789abcdef0123456789abcdef")
|
||||
require.NoError(t, err)
|
||||
spanID, err := trace.SpanIDFromHex("fedcba9876543210")
|
||||
require.NoError(t, err)
|
||||
spanContext := trace.NewSpanContext(trace.SpanContextConfig{
|
||||
TraceID: traceID,
|
||||
SpanID: spanID,
|
||||
})
|
||||
ctx := trace.ContextWithSpanContext(context.Background(), spanContext)
|
||||
|
||||
attrs := logging.ContextAttrs(ctx)
|
||||
require.Contains(t, attrs, "trace_id")
|
||||
require.Contains(t, attrs, traceID.String())
|
||||
require.Contains(t, attrs, "span_id")
|
||||
require.Contains(t, attrs, spanID.String())
|
||||
}
|
||||
|
||||
func TestContextAttrsEmptyContextReturnsNil(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Nil(t, logging.ContextAttrs(context.Background()))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Package logging configures the Game Lobby Service process logger and
|
||||
// provides context-aware helpers for trace fields.
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// New constructs the process-wide JSON logger from level.
|
||||
func New(level string) (*slog.Logger, error) {
|
||||
var slogLevel slog.Level
|
||||
if err := slogLevel.UnmarshalText([]byte(strings.TrimSpace(level))); err != nil {
|
||||
return nil, fmt.Errorf("build logger: %w", err)
|
||||
}
|
||||
|
||||
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slogLevel,
|
||||
})), nil
|
||||
}
|
||||
|
||||
// TraceAttrsFromContext returns slog key-value pairs for the active
|
||||
// OpenTelemetry span when ctx carries a valid span context. The keys match
|
||||
// the frozen `lobby/README.md` §Observability log fields `trace_id` and
|
||||
// `span_id`.
|
||||
func TraceAttrsFromContext(ctx context.Context) []any {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
spanContext := trace.SpanContextFromContext(ctx)
|
||||
if !spanContext.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []any{
|
||||
"trace_id", spanContext.TraceID().String(),
|
||||
"span_id", spanContext.SpanID().String(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user