From 8071edf50f6611c8374e68a7ff3c6bb70052906a Mon Sep 17 00:00:00 2001 From: Bruno Carvalho Date: Sun, 21 Jun 2026 23:40:02 -0300 Subject: [PATCH] fix: harden trace context propagation and collector ingest --- addons/observability/observability.go | 19 +- docs/engineering/architecture.md | 12 +- docs/product/requirements.md | 4 +- docs/product/roadmap.md | 13 +- docs/reference/observability.md | 33 ++- docs/reference/realtime.md | 4 + docs/reference/tracing.md | 45 +++- runtime/trace/collector.go | 238 ++++++++++++++++--- runtime/trace/collector_internal_test.go | 43 ++++ runtime/trace/span.go | 3 +- runtime/trace/trace_test.go | 276 ++++++++++++++++++++++- runtime/trace/tracer.go | 5 +- runtime/trace/types.go | 161 ++++++++++++- 13 files changed, 787 insertions(+), 69 deletions(-) create mode 100644 runtime/trace/collector_internal_test.go diff --git a/addons/observability/observability.go b/addons/observability/observability.go index aebc27a8..75362842 100644 --- a/addons/observability/observability.go +++ b/addons/observability/observability.go @@ -3,6 +3,8 @@ package observability import ( + "time" + "github.com/cssbruno/gowdk" gowdktrace "github.com/cssbruno/gowdk/runtime/trace" ) @@ -21,9 +23,22 @@ func Addon() gowdk.Addon { return gowdk.NewAddon("observability", gowdk.FeatureObservability) } +// CollectorOption configures a Collector. +type CollectorOption = gowdktrace.CollectorOption + // NewCollector creates a bounded in-memory trace collector. -func NewCollector(limit int) *Collector { - return gowdktrace.NewCollector(limit) +func NewCollector(limit int, options ...CollectorOption) *Collector { + return gowdktrace.NewCollector(limit, options...) +} + +// WithCollectorSSELimit configures the collector's concurrent SSE stream cap. +func WithCollectorSSELimit(limit int) CollectorOption { + return gowdktrace.WithCollectorSSELimit(limit) +} + +// WithCollectorIngestRate configures the collector's per-client POST rate. +func WithCollectorIngestRate(limit int, window time.Duration) CollectorOption { + return gowdktrace.WithCollectorIngestRate(limit, window) } // NewTracer creates a dependency-free tracer. diff --git a/docs/engineering/architecture.md b/docs/engineering/architecture.md index 75e43ff4..9e25a34c 100644 --- a/docs/engineering/architecture.md +++ b/docs/engineering/architecture.md @@ -65,13 +65,13 @@ nacked-batch backoff options while adapters retain durable retry and dead-letter policy ownership. `runtime/trace` provides the first dependency-free GOWDK Trace core. It owns -W3C-compatible trace/span IDs, `traceparent` propagation, context spans, +W3C-compatible trace/span IDs, bounded `traceparent`/`tracestate` propagation, context spans, GOWDK surface/lane/source metadata, attributes, events, status, sampling, console/JSONL/ring/multi/exporter sinks, and a bounded in-process JSON/SSE -collector. Generated app auto-instrumentation, durable storage, a browser trace -viewer, and concrete OTLP transport are later integration work. Debug-gated -generated instrumentation now covers backend, SSR/load, contract, browser, and -island lanes through `addons/observability`. +collector with hardened browser ingest. Durable storage and concrete production +telemetry backends are app-owned integration work. Debug-gated generated +instrumentation now covers backend, SSR/load, contract, browser, and island +lanes through `addons/observability`. Still partial: broad local client-side reactivity, richer hybrid streaming and data refresh, non-HTTP revalidation, generated worker/cron binary scaffolding, @@ -209,7 +209,7 @@ manifest report (`internal/lang/testdata/manifest_golden`). | `runtime/security` | Runtime-safe security text helpers. | Runtime | Provides conservative secret-like text redaction for generated app panic/error logging without importing compiler-private `internal/` packages. | | `runtime/testkit` | Generated audit test helpers. | Runtime | Provides small `httptest` helpers used by generated `gowdk_audit_test.go` files and `gowdk audit --run` to verify route status, method rejection, and configured response headers in-process against generated app handlers. | | `runtime/contracts` | Typed contract registry and in-process dispatch. | Runtime | Implemented for queries, commands, backend-owned domain and integration events, presentation events, jobs, metadata, stable observation names and labels for logs/metrics/traces, local command-buffered event dispatch, event-envelope capture/replay with stable IDs, dependency-free outbox/broker/presentation-fanout/event-source/seen-store interfaces, command event sinks, an event worker loop with ack/nack, context cancellation, optional post-ack deduplication windows, explicit nacked-batch backoff options, a dependency-free file outbox adapter, dependency-free in-memory broker/EventSource adapter, dependency-free in-memory and file-backed seen stores, and dependency-free SSE presentation fanout adapter with retry hints and drop-on-full per-client buffers. Concrete Redis Streams, Redis TTL seen-store, NATS, and WebSocket adapters are nested optional modules. Separate worker/cron binary generators and deployment recipes are platform tooling, not runtime core. | -| `runtime/trace` | Dependency-free runtime tracing core. | Runtime | Provides W3C-compatible trace/span IDs, `traceparent` inject/extract helpers, context spans, GOWDK surface/lane/source metadata, attributes, events, status, always-on/off and ratio sampling, console/JSONL/ring/multi/exporter sinks, OTLP-shaped snapshots without an OpenTelemetry dependency, and a bounded JSON/SSE collector. Generated backend, SSR/load, guard, contract, browser, and island instrumentation is opt-in and debug-gated through `addons/observability`; durable trace storage and production sampling/access policy remain app-owned. | +| `runtime/trace` | Dependency-free runtime tracing core. | Runtime | Provides W3C-compatible trace/span IDs, bounded `traceparent`/`tracestate` inject/extract helpers, context spans, GOWDK surface/lane/source metadata, attributes, events, status, always-on/off and ratio sampling, console/JSONL/ring/multi/exporter sinks, OTLP-shaped snapshots without an OpenTelemetry dependency, and a bounded JSON/SSE collector with hardened browser ingest. Generated backend, SSR/load, guard, contract, browser, and island instrumentation is opt-in and debug-gated through `addons/observability`; durable trace storage and production sampling/access policy remain app-owned. | | `runtime/actions` | Request-time action helpers. | Runtime | Owns CSRF token validation/generation, generated action registries, form decoding, and required-field validation helpers. | | `runtime/api` | Request-time API helpers. | Runtime | Owns strict JSON request decoding, query helpers, and JSON/error/no-content response helpers. | | `runtime/partial` | Request-time partial update helpers. | Runtime | Owns fragment responses, swap helpers, and partial client hook constants. | diff --git a/docs/product/requirements.md b/docs/product/requirements.md index 1ccf811b..51e613fd 100644 --- a/docs/product/requirements.md +++ b/docs/product/requirements.md @@ -55,7 +55,7 @@ language references, compiler docs, and examples. | PRD-027 | Provide opt-in browser presentation-event fanout without adding WebSocket dependencies to the root module. | Medium | Implemented | `FeatureRealtime` and `addons/realtime` provide config and `gowdk add realtime` wiring for presentation-event fanout. Dependency-free SSE fanout remains in the root module through `runtime/contracts/sse` and `realtime.NewSSE`; WebSocket fanout remains isolated in the nested `runtime/contracts/websocketfanout` module. Docs cover SSE versus WebSocket setup, deployment caveats, and the M14 boundary for live DOM reactivity. | | PRD-028 | Provide compiler-validated realtime UI subscription metadata. | Medium | Partial | ADR 0012 defines `g:subscribe` on query-owned elements. The compiler parses the directive, lowers it to `Program.RealtimeSubscriptions`, requires `realtime.Addon()`, validates referenced Go contracts as presentation events available to the web role, emits exact-span diagnostics, renders `data-gowdk-subscribe` and validated `data-gowdk-subscribe-type` markers, records build-report metadata, generated apps mount subscription-filtered SSE fanout at `/_gowdk/realtime/events` for bound subscriptions, generated stream handlers run inherited guards before opening SSE responses, the SSE adapter declares browser retry timing and drops events for full per-client buffers instead of blocking command execution, generated `gowdk.js` applies explicit `replaceHTML` realtime patches to subscribed query regions, and `examples/contracts` demonstrates the live flow. The compiler also scans explicit Go `RegisterInvalidation[event, query]` edges, lowers validated bound edges to `Program.QueryInvalidations`, rejects unknown queries/events or events no scanned command emits, records `query_invalidation` build-report events, prints `invalidates` graph edges, renders `data-gowdk-query-type` markers, emits generated `gowdk.query.invalidate` presentation events after command event dispatch, and refetches the current document to replace matching non-subscribed query regions. Custom retry/backoff/replay, active session-change stream revocation, richer patch shapes, fragment/API-specific query execution, and route-specific refresh endpoints remain planned hardening work. | | PRD-029 | Provide optional SEO build output for sitemap and robots files without making crawler policy core. | Medium | Partial | `addons/seo` registers `FeatureSEO` and `gowdk.SEOProvider`; `gowdk build` emits `sitemap.xml` and `robots.txt` only when the addon supplies a valid `BaseURL`. The sitemap includes public static and `paths {}`-expanded SPA routes plus configured extra URLs, while request-time and guardless default-denied pages are excluded and listed in `gowdk-build-report.json`. JSON-LD and request-time sitemap serving remain planned/out of scope for this slice. | -| PRD-030 | Provide dependency-free runtime trace primitives and opt-in generated app instrumentation. | Medium | Partial | ADR 0013 defines `runtime/trace` as the root-module observability core. It provides W3C-compatible trace/span IDs, `traceparent` propagation, context spans, GOWDK surface/lane/source metadata, attributes/events/status, always-on/off and ratio sampling, console/JSONL/ring/multi/exporter sinks, a bounded JSON/SSE collector, browser span ingest, and a self-contained local viewer. `addons/observability` gates debug-only generated route/guard/handler/SSR-load/browser/island tracing, `runtime/contracts` propagates trace context through events/jobs/workers/outbox records, and the nested `runtime/trace/otel` module provides optional OTLP HTTP export without root OpenTelemetry dependencies. Durable production storage, hosted analysis, and production sampling/access policy remain app-owned. | +| PRD-030 | Provide dependency-free runtime trace primitives and opt-in generated app instrumentation. | Medium | Partial | ADR 0013 defines `runtime/trace` as the root-module observability core. It provides W3C-compatible trace/span IDs, `traceparent`/`tracestate` propagation with bounded header parsing, context spans, GOWDK surface/lane/source metadata, attributes/events/status, always-on/off and ratio sampling, console/JSONL/ring/multi/exporter sinks, a bounded JSON/SSE collector, hardened browser span ingest, and a self-contained local viewer with dropped/rejected counters. `addons/observability` gates debug-only generated route/guard/handler/SSR-load/browser/island tracing, `runtime/contracts` propagates trace context through events/jobs/workers/outbox records, and the nested `runtime/trace/otel` module provides optional OTLP HTTP export without root OpenTelemetry dependencies. Durable production storage, hosted analysis, and production sampling/access policy remain app-owned. | | PRD-031 | Provide a config-owned localization contract for generated page routes and typed message catalogs. | Medium | Partial | `Config.I18N` declares locale codes, optional path prefixes, default locale, and default-prefix omission. Build-time SPA routes, dynamic `paths {}` output, request-time SSR/hybrid page routes, route metadata, site-map JSON, route manifests, and SEO sitemap output expand per locale. Build helpers receive `gowdk.BuildParams.Locale`, generated HTML receives `lang`, SSR handlers attach `runtime/app.Locale(ctx)`, and `runtime/i18n` provides typed Go catalog/bundle helpers. Message extraction, ICU/plural/date formatting, translated diagnostics, and per-endpoint locale policies remain planned or app-owned. | ## P0/P1/P2 Decision Backlog @@ -82,7 +82,7 @@ implemented. | Forms | Keep progressive-enhancement-first form behavior; full POST and enhanced POST share action result semantics; domain validation stays in user Go. | Partial | Generated enhanced forms preserve no-JavaScript POST behavior, send partial request headers, swap server fragments, expose failed enhanced response status/body/detail events, and use escaped live-region validation fragments. Domain validation stays in user Go. | | APIs | Broaden APIs through public request/response helpers and typed body/query helpers, not framework-specific adapters. | Partial — `addons/api` provides strict JSON body decoding, typed query helpers, JSON/error/no-content response helpers for current `func(context.Context, *http.Request) (response.Response, error)` handlers, and config-level CORS policy for generated API/command/query routes. Generated typed handler signatures, per-route result contracts, per-endpoint CORS syntax, and richer examples remain planned. | | Contract runtime | Add typed Go queries, commands, backend-owned domain/integration events, presentation events, and jobs after endpoint/adapter IR is stable. Frontend UI events trigger commands or queries, commands have one owner, domain events are emitted after backend state changes succeed, local in-process dispatch is default, and broker/outbox/worker roles are optional. Runtime registry, role filtering, event capture/replay, outbox/broker/fanout/EventSource/seen-store interfaces, worker ack/nack/backoff, file/in-memory/Redis/NATS adapters, SSE/WebSocket fanout, generated command event sinks, generated registries, generated worker replay helpers, Go AST scanning, `go/types` diagnostics, duplicate-owner and emitted-event diagnostics, contract/list/graph/trace CLI, `g:command`/`g:query` metadata, query-bounded `g:subscribe`, explicit `RegisterInvalidation[event, query]` metadata, import-path-aware reference/subscription/invalidation linking, `g:event` rejection, IR binding status, app adapter IR, generated web command/query adapters, page-route query JSON negotiation, stable JSON success/error response shape, formatted generated adapter source, page-guard propagation, rate-limit/guard/CSRF ordering, report metadata, enforced scan diagnostics, generated subscription-filtered guarded SSE fanout, generated client `replaceHTML` patches, generated `gowdk.query.invalidate` events, and current-document refresh for matching non-subscribed invalidated query regions are implemented. Separate worker/cron binary generators, fragment/API-specific query execution, remaining exact diagnostic spans, richer realtime patch shapes, durable retry operations, and editor-first visualizations remain planned outside the milestone-14 runtime contract. | Implemented | -| Observability | Keep root tracing dependency-free while making generated instrumentation opt-in and debug-gated. | Partial — `runtime/trace` provides W3C-compatible IDs, `traceparent` propagation, context spans, GOWDK surface/lane/source metadata, span attributes/events/status, always-on/off and ratio sampling, console/JSONL/ring/multi/exporter sinks, OTLP-shaped snapshots, bounded JSON/SSE local collection, browser ingest, and a self-contained viewer. `addons/observability` enables debug-only generated backend, SSR/load, frontend, and island tracing; contracts/outbox records carry optional trace context; `runtime/trace/otel` isolates OTLP HTTP export in a nested module. Durable storage, hosted analysis, and production sampling/access policy remain app-owned. | +| Observability | Keep root tracing dependency-free while making generated instrumentation opt-in and debug-gated. | Partial — `runtime/trace` provides W3C-compatible IDs, bounded `traceparent`/`tracestate` propagation, context spans, GOWDK surface/lane/source metadata, span attributes/events/status, always-on/off and ratio sampling, console/JSONL/ring/multi/exporter sinks, OTLP-shaped snapshots, bounded JSON/SSE local collection, hardened browser ingest, and a self-contained viewer. `addons/observability` enables debug-only generated backend, SSR/load, frontend, and island tracing; contracts/outbox records carry optional trace context; `runtime/trace/otel` isolates OTLP HTTP export in a nested module. Durable storage, hosted analysis, full metrics/log correlation, and production sampling/access policy remain app-owned or future work. | | Cache | Keep `cache` and `revalidate` as HTTP cache policy; keep action-driven data refresh explicit through redirects, fragments, JSON, or reload responses. | Partial — route reports include route/endpoint cache metadata, build reports summarize generated cache policies, generated binaries apply immutable asset cache, SPA `no-cache`, request-time `no-store`, and page `cache`/`revalidate` for successful SPA/SSR HTML. | | Guards | Extend guards with safe local redirects and response helpers before richer request-local state. | Partial — guards keep the `func(runtime/guard.Context) error` signature. Ordinary errors fail closed with 403, while `runtime/guard.RedirectTo`, `runtime/guard.Redirect`, and `runtime/guard.Respond` intentionally write no-store redirects or custom responses. Richer request-local state is still deferred. | | Component CSS | Make component CSS explicit, compiler-scoped, and documented; Tailwind and processors remain optional. | Partial | diff --git a/docs/product/roadmap.md b/docs/product/roadmap.md index d66423ab..9801958e 100644 --- a/docs/product/roadmap.md +++ b/docs/product/roadmap.md @@ -151,11 +151,12 @@ level, the current baseline already includes: command/query contract web endpoints, and frontend audit surfaces, with declared `*.audit.gwdk` policies, generated runtime audit tests, registry-backed findings, and CI-friendly JSON output. -- dependency-free `runtime/trace` primitives for W3C trace IDs, - `traceparent` propagation, context spans, sinks, sampling, local JSON/SSE - trace collection, browser span ingest, a self-contained viewer, debug-gated - generated app instrumentation through `addons/observability`, contract/event - propagation, and nested-module OTLP export. +- dependency-free `runtime/trace` primitives for W3C trace IDs, bounded + `traceparent`/`tracestate` propagation, context spans, sinks, sampling, local + JSON/SSE trace collection, hardened browser span ingest, a self-contained + viewer, debug-gated generated app instrumentation through + `addons/observability`, contract/event propagation, and nested-module OTLP + export. Do not roadmap those completed slices as future work. Future work should stabilize their contracts, remove generation debt, and fill the missing @@ -195,7 +196,7 @@ are stable. | 18 | CSS, assets, and packaging | External addon loading is hardened, richer page-aware CSS processor contracts are stable, and Tailwind/CSS deployment docs stay explicit that external tooling is user-installed. Implemented CSS asset hashing, component CSS scope/hash metadata, component non-CSS asset emission, and binary cache policy remain stable. Module selection remains artifact packaging, not runtime module orchestration. | | 19 | Framework adapters | GOWDK Runtime remains `net/http` first. Optional Chi, Echo, Gin, and Fiber adapters wrap the same generated `http.Handler`; Chi/Echo/Gin can mount routes from generated OpenAPI metadata, and generated code stays framework-neutral by default. | | 20 | Dev and tooling | `gowdk dev` can run generated app/runtime flows for backend routes and SSR, skip unchanged rebuilds, cache watched input state, show SPA/static and generated-app rebuild failures with diagnostic codes, source ranges, last-good build time, and changed files, show generic generated-app runtime 5xx failures without request details through the dev-only proxy bridge, and hot-swap changed JS island component roots when the current page has matching island boundaries. Generated app runtime mode now uses the proxy for browser overlay delivery and live reload after generated-app restarts. State-preserving/broader component HMR, richer LSP completions, and editor navigation remain tracked in [#424](https://github.com/cssbruno/GoWDK/issues/424). | -| 21 | Observability | Partial. Generated apps can opt into GOWDK trace spans across route, guard, handler, SSR, action, API, fragment, contract, job, island, nav, and user lanes while keeping the root runtime dependency-free. `runtime/trace`, `addons/observability`, a local viewer, contract/event propagation, browser propagation, WASM island bridge reuse, and nested-module OTLP HTTP export are implemented. Durable trace storage, hosted analysis, and production sampling/access policy remain app-owned hardening work. | +| 21 | Observability | Partial. Generated apps can opt into GOWDK trace spans across route, guard, handler, SSR, action, API, fragment, contract, job, island, nav, and user lanes while keeping the root runtime dependency-free. `runtime/trace`, `addons/observability`, a hardened local viewer/collector, contract/event propagation, browser propagation, WASM island bridge reuse, and nested-module OTLP HTTP export are implemented. Durable trace storage, hosted analysis, full metrics/log correlation, and production sampling/access policy remain app-owned or future work. | | 22 | Documentation sync | README, requirements, architecture, deployment, roadmap, and examples stay synchronized with implemented behavior and commands. | ## Candidate Release Order diff --git a/docs/reference/observability.md b/docs/reference/observability.md index de13691a..e97d885a 100644 --- a/docs/reference/observability.md +++ b/docs/reference/observability.md @@ -1,8 +1,12 @@ # Observability `addons/observability` enables generated GOWDK Trace wiring for debug builds. -It registers `FeatureObservability`; `runtime/trace` remains the dependency-free -root runtime, and optional OTLP export lives in the nested +The addon name stays `observability`, but the current implemented surface is +GOWDK Trace plus local inspection primitives. It is not a hosted metrics, logs, +or trace-analysis backend. + +It registers `FeatureObservability`; `runtime/trace` remains the +dependency-free root runtime, and optional OTLP export lives in the nested `runtime/trace/otel` module. Enable it: @@ -26,7 +30,8 @@ supplies an explicit `TraceAccess` function. Current generated instrumentation: -- Backend request route spans extract incoming `traceparent`. +- Backend request route spans extract incoming `traceparent` and valid + `tracestate`. - Generated SSR route and `server {}` spans record route IDs, render lane, source refs, response status, and load errors without storing raw request bodies or headers. @@ -37,14 +42,26 @@ Current generated instrumentation: - `runtime/contracts.EventEnvelope` and file outbox records carry an optional `traceparent`; old records without it remain readable. - Generated browser runtime spans partial submits and SPA navigation, injects - `traceparent`, and posts frontend spans to the local collector. + trace context, and posts frontend spans to the local collector. - JS islands, WASM island loaders, and page-level client Go WASM loaders reuse `window.__gowdkTrace`. The generated local collector keeps a bounded in-memory ring of 1024 completed -spans. Generated code records stable route/endpoint IDs and source metadata, and -uses runtime redaction helpers for query strings, error messages, and app-owned -trace events. +spans. It requires JSON POST ingest, rejects cross-origin browser ingest, caps +request/body/span/event/attribute/string sizes, limits POST ingest rate and SSE +subscriber count, and exposes `dropped` plus `rejected` health counters in the +JSON/viewer surface. Generated code records stable route/endpoint IDs and source +metadata, and uses runtime redaction helpers for query strings, error messages, +and app-owned trace events. + +## Mode Matrix + +| Mode | Generated spans | Local collector/viewer | Intended access | +| --- | --- | --- | --- | +| `gowdk dev` / local debug | Enabled when the addon is present and debug assets are on. | Mounted at `/_gowdk/traces`. | Localhost only by default. | +| Preview/debug app builds | Enabled when `Build.DebugAssets()` is true. | Mounted only when the addon is present. | Keep behind `LocalTraceAccess` or an app-owned gate. | +| Production builds | Disabled by default because debug assets are off. | Not mounted by generated code. | Use app-owned telemetry export and access policy. | +| Direct `runtime/trace` use | App-controlled. | App-controlled. | The app must wrap public routes with auth, TLS, proxy, and rate-limit policy. | For app-owned Go handlers, record a user event on the active span: @@ -67,3 +84,5 @@ tracer := trace.NewTracer(trace.WithSink(sink)) Do not treat the local collector or viewer as a production observability backend. Production deployments should set sampling deliberately, keep viewer access gated or disabled, and send spans to app-owned telemetry infrastructure. +Full slog correlation, expanded route metrics, durable storage, hosted analysis, +and production access policy remain future observability work. diff --git a/docs/reference/realtime.md b/docs/reference/realtime.md index f42da2bf..72734827 100644 --- a/docs/reference/realtime.md +++ b/docs/reference/realtime.md @@ -277,6 +277,10 @@ Each presentation event is written as one text JSON `contracts.EventEnvelope`. - In-process SSE and WebSocket hubs only know about clients connected to the same process. Multi-instance deployments should pair fanout with a broker, outbox, or external pub/sub path when all clients must see the same event. +- Generated realtime streams are guard-checked and subscription/type filtered, + but the current presentation fanout is not scoped by user, session, tenant, or + audience. Do not put user-specific payloads on the shared fanout unless the + application owns an additional filtering layer. - SSE responses set `X-Accel-Buffering: no`; reverse proxies may still need explicit buffering and timeout settings for long-lived streams. - WebSocket deployments should set origin checks and proxy upgrade headers. diff --git a/docs/reference/tracing.md b/docs/reference/tracing.md index 05a31bd4..578f0449 100644 --- a/docs/reference/tracing.md +++ b/docs/reference/tracing.md @@ -52,12 +52,19 @@ gowdktrace.Inject(ctx, headers) ctx = gowdktrace.Extract(context.Background(), headers) ``` -The wire format is W3C `traceparent`: +The wire format is W3C `traceparent`, with valid `tracestate` preserved when +present: ```text 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 ``` +`Extract` rejects malformed `traceparent` values and ignores malformed +`tracestate` values while preserving the trace identity. Header limits are +fixed: `traceparent` is capped at 256 bytes and `tracestate` at 512 bytes. The +remote sampled flag is input context only; the local sampler still decides +whether a new span is recorded. + ## Sinks Current sinks: @@ -68,8 +75,8 @@ Current sinks: overflow. - `MultiSink(...)`: sends spans to multiple sinks in order. - `ExporterSink(exporter)`: adapts an OTLP-like exporter interface. -- `NewCollector(limit)`: sink plus local JSON/SSE HTTP handler and browser span - ingest. +- `NewCollector(limit, options...)`: sink plus local JSON/SSE HTTP handler and + browser span ingest. ## Collector @@ -85,7 +92,8 @@ http.Handle("/_gowdk/traces", collector.Handler()) ```json { "spans": [], - "dropped": 0 + "dropped": 0, + "rejected": 0 } ``` @@ -94,6 +102,33 @@ streams `event: gowdk-trace` messages for existing and future spans. The viewer handler adds `GET /` for the self-contained UI and `POST /browser` for generated browser spans. +POST ingest is treated as untrusted input: + +- requests must use `Content-Type: application/json` or `application/*+json`; +- browser-originated requests must be same-origin when an `Origin` header is + present; +- request bodies are capped at 1 MiB and batches at 128 spans; +- span names, attributes, events, strings, and encoded snapshot size are + bounded before storage; +- POST ingest is rate-limited per remote address by default; +- SSE subscribers are capped by default and slow subscribers are dropped instead + of blocking span recording. + +Tune local collector limits when mounting it directly: + +```go +collector := gowdktrace.NewCollector( + 256, + gowdktrace.WithCollectorSSELimit(16), + gowdktrace.WithCollectorIngestRate(60, time.Minute), +) +``` + +Generated apps mount the viewer behind `runtime/app.LocalTraceAccess`. If an +application mounts `Collector.Handler()` or `ViewerHandler()` itself on an +internet-facing route, the application must still provide normal authentication, +authorization, TLS, reverse-proxy, and production rate-limit policy. + ## Sampling - `AlwaysOn()` @@ -109,3 +144,5 @@ The disabled `AlwaysOff` path with no start options is allocation-free. debug builds. - Concrete OTLP export lives in the nested `runtime/trace/otel` module so the root module does not depend on OpenTelemetry. +- Durable storage, hosted trace analysis, production sampling policy, and + production access policy stay app-owned. diff --git a/runtime/trace/collector.go b/runtime/trace/collector.go index 3db50527..2aba5615 100644 --- a/runtime/trace/collector.go +++ b/runtime/trace/collector.go @@ -8,16 +8,26 @@ import ( "fmt" "html/template" "io" + "mime" + "net" "net/http" + "net/url" "strings" "sync" + "sync/atomic" + "time" ) const ( - maxCollectorBodyBytes = 1 << 20 - maxCollectorBatchSpans = 128 + maxCollectorBodyBytes = 1 << 20 + maxCollectorBatchSpans = 128 + defaultCollectorSSELimit = 32 + defaultCollectorRateLimit = 120 + maxCollectorRateClients = 1024 ) +const defaultCollectorRateWindow = time.Minute + var errTracePayloadTooLarge = errors.New("trace payload exceeds byte limit") var viewerTemplate = template.Must(template.New("gowdk-trace-viewer").Parse(` @@ -38,7 +48,7 @@ main{display:grid;grid-template-columns:minmax(260px,360px) 1fr;min-height:calc( -

GOWDK Trace

0 spans · 0 dropped
+

GOWDK Trace

0 spans · 0 dropped · 0 rejected
Waiting for spans...
{}