mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-13 18:22:04 +00:00
feat: add credential-safe multiplayer observability
This commit is contained in:
+1
-1
@@ -1236,7 +1236,7 @@ the local/CI/community transport, not a silent production fallback.
|
|||||||
|
|
||||||
| # | Task | Acceptance |
|
| # | Task | Acceptance |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 8.44 `[D:8.3,8.4,8.28,8.31]` | Propagate queue/proposal/match/server IDs and process-ready/assignment-ready through logs, metrics, traces and replay metadata; redact credentials | One ID traces queue→result across components and automated secret-canary tests find no auth/relay ticket |
|
| 8.44 `[D:8.3,8.4,8.28,8.31]` | **IN PROGRESS.** Go observability package encodes queue/proposal/match/server IDs and lifecycle stage in structured events while recursively redacting auth/relay tokens and credentials | `server/observability/` covers correlation fields, nested secret redaction and unnamed-event rejection; production logger/metrics/traces/replay integration and secret-canary coverage remain |
|
||||||
| 8.45 `[D:8.2,8.44]` | Dashboards/alerts for wait/MMR/RTT, proposals, allocation/Ready/image pull, connect/no-show, tick/crash/flood, result conflict/lag, abandons and cost | Each SLO and security/cost signal has an exercised alert and runbook |
|
| 8.45 `[D:8.2,8.44]` | Dashboards/alerts for wait/MMR/RTT, proposals, allocation/Ready/image pull, connect/no-show, tick/crash/flood, result conflict/lag, abandons and cost | Each SLO and security/cost signal has an exercised alert and runbook |
|
||||||
| 8.46 `[D:8.5,8.7,8.9,8.10,8.14,8.18,8.21,8.23,8.25]` | **IN PROGRESS.** Go unit/race coverage spans the current domain/store/supervisor policies, and fuzz targets now exercise queue input, result payload hashing and revision events | `server/domain/*_test.go`, `server/store/*_test.go`, `server/supervisor/*_test.go` and `server/domain/fuzz_test.go` pass normal/race suites; `go test -fuzz`, PostgreSQL concurrency/migration execution, fake Steam/allocator and full lost-Redis/transaction fixtures remain |
|
| 8.46 `[D:8.5,8.7,8.9,8.10,8.14,8.18,8.21,8.23,8.25]` | **IN PROGRESS.** Go unit/race coverage spans the current domain/store/supervisor policies, and fuzz targets now exercise queue input, result payload hashing and revision events | `server/domain/*_test.go`, `server/store/*_test.go`, `server/supervisor/*_test.go` and `server/domain/fuzz_test.go` pass normal/race suites; `go test -fuzz`, PostgreSQL concurrency/migration execution, fake Steam/allocator and full lost-Redis/transaction fixtures remain |
|
||||||
| 8.47 `[D:8.7,8.30]` | **IN PROGRESS.** Offline testkit provides deterministic fake Steam verification and fake allocation with forced failure injection | `server/testkit/` covers verified identity/replay and cloud-free forced allocation failure; API/Compose integration and exhaustive success/failure matrix remain |
|
| 8.47 `[D:8.7,8.30]` | **IN PROGRESS.** Offline testkit provides deterministic fake Steam verification and fake allocation with forced failure injection | `server/testkit/` covers verified identity/replay and cloud-free forced allocation failure; API/Compose integration and exhaustive success/failure matrix remain |
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user