Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions docs/schema/v1.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://waylog.dev/schema/v1.1.json",
"title": "Waylog Wide Event v1.1 (bridge)",
"type": "object",
"required": ["schema_version", "event_name", "timestamp", "request", "system", "outcome"],
"properties": {
"schema_version": { "type": "string", "pattern": "^1\\.[0-9]+$" },
"event_name": { "type": "string" },
"timestamp": { "type": "string", "format": "date-time" },
"user": {
"type": "object",
"properties": {
"id": { "type": "string" },
"tier": { "type": "string" },
"region": { "type": "string" },
"vip": { "type": "boolean" }
},
"additionalProperties": true
},
"request": {
"type": "object",
"required": ["trace_id"],
"properties": {
"trace_id": { "type": "string", "pattern": "^[0-9a-f]{32}$" },
"span_id": { "type": "string" },
"parent_span_id": { "type": "string" },
"http_method": { "type": "string" },
"route_template": { "type": "string" },
"flow": { "type": "string" },
"feature_flags": { "type": "array", "items": { "type": "string" } },
"correlation_id": { "type": "string" },
"attempt": { "type": "integer" },
"transport_kind": { "type": "string" }
},
"additionalProperties": true
},
"system": {
"type": "object",
"required": ["service", "env"],
"properties": {
"service": { "type": "string" },
"version": { "type": "string" },
"deployment_id": { "type": "string" },
"env": { "type": "string" },
"downstream_service": { "type": "string" },
"caller_service": { "type": "string" }
},
"additionalProperties": true
},
"outcome": {
"type": "object",
"required": ["success", "status_code", "kind"],
"properties": {
"success": { "type": "boolean" },
"status_code": { "type": "integer" },
"kind": { "type": "string" }
},
"additionalProperties": true
},
"error": {
"type": "object",
"properties": {
"code": { "type": "string" },
"path": { "type": "string" },
"message": { "type": "string" },
"reason": { "type": "string" }
},
"additionalProperties": true
},
"metrics": {
"type": "object",
"properties": {
"latency_ms": { "type": "integer" }
},
"additionalProperties": true
},
"parent_request_id": { "type": "string" },
"metadata": { "type": "object" },
"retry": {
"type": "object",
"properties": {
"of": { "type": "integer" },
"previous_attempt_id": { "type": "string" }
},
"additionalProperties": true
}
},
"additionalProperties": true
}
98 changes: 98 additions & 0 deletions docs/schema/v2.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://waylog.dev/schema/v2.0.json",
"title": "Waylog Wide Event v2.0",
"type": "object",
"required": ["schema_version", "event_id", "ts_start", "ts_end", "kind", "service", "env", "trace_id", "status"],
"properties": {
"schema_version": { "const": "2.0" },
"event_id": { "type": "string", "format": "uuid" },
"ts_start": { "type": "string", "format": "date-time" },
"ts_end": { "type": "string", "format": "date-time" },
"duration_ms": { "type": "integer", "minimum": 0 },
"kind": { "const": "http" },
"service": { "type": "string", "minLength": 1 },
"env": { "type": "string", "minLength": 1 },
"version": { "type": "string" },
"trace_id": { "type": "string", "pattern": "^[0-9a-f]{32}$" },
"span_id": { "type": "string", "pattern": "^([0-9a-f]{16})?$" },
"parent_span_id": { "type": "string", "pattern": "^([0-9a-f]{16})?$" },
"status": { "enum": ["ok", "error", "timeout", "partial", "aborted", "suppressed"] },
"anchor": {
"type": "object",
"required": ["step", "error_code"],
"properties": {
"step": { "type": "string", "minLength": 1 },
"error_code": { "type": "string", "minLength": 1 },
"kind": { "type": "string" }
}
},
"steps": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "start_ms", "duration_ms", "status"],
"properties": {
"name": { "type": "string", "minLength": 1 },
"span_id": { "type": "string", "pattern": "^([0-9a-f]{16})?$" },
"start_ms": { "type": "integer", "minimum": 0 },
"duration_ms": { "type": "integer", "minimum": 0 },
"status": { "enum": ["ok", "error"] },
"downstream": {
"type": "object",
"properties": {
"service": { "type": "string" },
"endpoint": { "type": "string" },
"kind": { "type": "string" }
}
},
"error": {
"type": "object",
"properties": {
"code": { "type": "string" },
"reason": { "type": "string" },
"cause": { "type": "string" }
}
}
},
"allOf": [
{
"if": { "required": ["downstream"] },
"then": { "required": ["span_id"] }
}
]
}
},
"logs": {
"type": "array",
"items": {
"type": "object",
"required": ["ts_offset_ms", "level", "msg"],
"properties": {
"ts_offset_ms": { "type": "integer", "minimum": 0 },
"level": { "enum": ["info", "warn", "error"] },
"msg": { "type": "string" },
"fields": { "type": "object" }
}
}
},
"fields": { "type": "object" },
"errors": {
"type": "array",
"items": {
"type": "object",
"required": ["code"],
"properties": {
"code": { "type": "string" },
"reason": { "type": "string" }
}
}
}
},
"allOf": [
{
"if": { "properties": { "status": { "enum": ["error", "timeout", "partial", "aborted"] } } },
"then": { "required": ["anchor"] }
}
]
}
123 changes: 123 additions & 0 deletions pkg/event/v2/bridge_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package eventv2

import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"

"github.com/sssmaran/WaylogCLI/pkg/event"
)

// TestV11BridgeSchemaAcceptsCurrentV1Event proves the bridge schema at
// docs/schema/v1.1.json is in lockstep with the actual v1 event.WideEvent
// type. If the v1.1 type changes during the bridge window, this test will
// fail until the schema is updated to match.
func TestV11BridgeSchemaAcceptsCurrentV1Event(t *testing.T) {
e := event.WideEvent{
SchemaVersion: "1.1",
EventName: "checkout.request",
Timestamp: time.Date(2026, 4, 25, 14, 0, 0, 0, time.UTC),
User: event.UserContext{
ID: "u_123",
Tier: "standard",
},
Request: event.RequestContext{
TraceID: "11111111111111111111111111111111",
SpanID: "1111111111111111",
HTTPMethod: "POST",
RouteTemplate: "/checkout",
Flow: "purchase",
FeatureFlags: []string{},
},
System: event.SystemContext{
Service: "checkout",
Env: "test",
Version: "1.0.0",
},
Outcome: event.OutcomeContext{
Success: true,
StatusCode: 200,
Kind: "http",
},
}

raw, err := json.Marshal(&e)
if err != nil {
t.Fatalf("marshal v1 event: %v", err)
}

schemaPath, err := filepath.Abs("../../../docs/schema/v1.1.json")
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(schemaPath); err != nil {
t.Fatalf("v1.1 schema missing at %s: %v", schemaPath, err)
}

sch, err := compileSchema(schemaPath)
if err != nil {
t.Fatalf("compile v1.1 schema: %v", err)
}

var v any
if err := json.Unmarshal(raw, &v); err != nil {
t.Fatalf("re-unmarshal: %v", err)
}
if err := sch.Validate(v); err != nil {
t.Fatalf("v1.1 bridge schema must accept current v1 WideEvent: %v\npayload: %s", err, raw)
}
}

// TestV11BridgeSchemaRejectsMissingTraceID is a sanity check that the bridge
// schema is not vacuous — it should still reject a v1 event that lacks the
// one field every consumer needs (request.trace_id).
func TestV11BridgeSchemaRejectsMissingTraceID(t *testing.T) {
bad := map[string]any{
"schema_version": "1.1",
"event_name": "checkout.request",
"timestamp": "2026-04-25T14:00:00Z",
"request": map[string]any{}, // missing trace_id
"system": map[string]any{"service": "checkout", "env": "test"},
"outcome": map[string]any{"success": true, "status_code": 200, "kind": "http"},
}

schemaPath, _ := filepath.Abs("../../../docs/schema/v1.1.json")
sch, err := compileSchema(schemaPath)
if err != nil {
t.Fatalf("compile: %v", err)
}
if err := sch.Validate(bad); err == nil {
t.Fatal("v1.1 schema must reject events with empty request (no trace_id)")
}
}

// TestV20SchemaRequiresStatus locks in that the v2.0 schema treats status as a
// required top-level field, matching the rest of the implementation and the
// product spec's triage model.
func TestV20SchemaRequiresStatus(t *testing.T) {
schemaPath, err := filepath.Abs("../../../docs/schema/v2.0.json")
if err != nil {
t.Fatal(err)
}
sch, err := compileSchema(schemaPath)
if err != nil {
t.Fatalf("compile v2.0 schema: %v", err)
}

// All currently-required fields except status. Should be rejected.
bad := map[string]any{
"schema_version": "2.0",
"event_id": "00000000-0000-4000-8000-000000000099",
"ts_start": "2026-04-25T14:00:00.000Z",
"ts_end": "2026-04-25T14:00:00.010Z",
"kind": "http",
"service": "checkout",
"env": "test",
"trace_id": "99999999999999999999999999999999",
}
if err := sch.Validate(bad); err == nil {
t.Fatal("v2.0 schema must reject events missing status")
}
}
Loading