feat: add credential-safe multiplayer observability

This commit is contained in:
Josh Creek
2026-08-31 21:01:30 +01:00
parent 0364d3f172
commit 3dde0ffb79
3 changed files with 96 additions and 1 deletions
+63
View File
@@ -0,0 +1,63 @@
// Package observability provides credential-safe structured event encoding.
package observability
import (
"encoding/json"
"fmt"
"strings"
"time"
)
type Event struct {
Event string
QueueID string
ProposalID string
MatchID string
ServerID string
Stage string
OccurredAt time.Time
Fields map[string]any
}
func Encode(event Event) ([]byte, error) {
if event.Event == "" {
return nil, fmt.Errorf("event name is required")
}
fields := map[string]any{
"event": event.Event, "occurred_at": event.OccurredAt.UTC().Format(time.RFC3339Nano),
}
for key, value := range map[string]string{"queue_id": event.QueueID, "proposal_id": event.ProposalID, "match_id": event.MatchID, "server_id": event.ServerID, "stage": event.Stage} {
if value != "" {
fields[key] = value
}
}
for key, value := range event.Fields {
fields[key] = redact(key, value)
}
return json.Marshal(fields)
}
func redact(key string, value any) any {
lowered := strings.ToLower(key)
for _, secret := range []string{"token", "secret", "credential", "authorization", "private_key", "auth_ticket", "relay_ticket"} {
if strings.Contains(lowered, secret) {
return "[REDACTED]"
}
}
switch typed := value.(type) {
case map[string]any:
copy := make(map[string]any, len(typed))
for key, value := range typed {
copy[key] = redact(key, value)
}
return copy
case []any:
copy := make([]any, len(typed))
for i, value := range typed {
copy[i] = redact("item", value)
}
return copy
default:
return value
}
}
+32
View File
@@ -0,0 +1,32 @@
package observability
import (
"encoding/json"
"testing"
"time"
)
func TestEncodeCorrelatesStagesAndRedactsNestedCredentials(t *testing.T) {
payload, err := Encode(Event{Event: "assignment_ready", QueueID: "queue-1", ProposalID: "proposal-1", MatchID: "match-1", ServerID: "server-1", Stage: "assignment-ready", OccurredAt: time.Unix(1000, 0), Fields: map[string]any{"auth_ticket": "do-not-log", "nested": map[string]any{"relay_ticket": "also-secret", "attempt": 2}}})
if err != nil {
t.Fatal(err)
}
var decoded map[string]any
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatal(err)
}
for _, key := range []string{"queue_id", "proposal_id", "match_id", "server_id", "stage"} {
if decoded[key] == nil {
t.Fatalf("missing correlation field %q: %s", key, payload)
}
}
if decoded["auth_ticket"] != "[REDACTED]" || decoded["nested"].(map[string]any)["relay_ticket"] != "[REDACTED]" {
t.Fatalf("credential not redacted: %s", payload)
}
}
func TestEncodeRejectsUnnamedEvents(t *testing.T) {
if _, err := Encode(Event{}); err == nil {
t.Fatal("unnamed event accepted")
}
}