Instruments the Worker with OpenTelemetry — traces + structured logs over OTLP — plus alertable operational signals, in a way that fits the TinyGo/Wasm build and changes no existing public signatures.
What is instrumented (spans + logs)
Ingest (internal/ingest) — one ingest.requestserver span + a correlated structured log per request, classified as accepted / rejected / rate_limited / error with http.response.status_code. Instrumentation is observe-only: it wraps the response writer to read the status code and never alters the 202/400/413/415/405/503 contract.
Publish (internal/publish) — a run-level publish.run span with a publish.reportchild span per report and a log per report (published / failed), plus run-level counts (attempted / published / failed).
Schedule (internal/schedule) — a schedule.run span that parents the publish run (end-to-end trace: schedule.run → publish.run → publish.report[]), with the pending count and gate outcome.
Logs carry the active span's trace/span ids, so logs and traces correlate.
Alerting signals
A backend alert rule can key on span status=Error and on structured log records carrying alert=true + a specific alert.type:
ingest.error — a 5xx (e.g. 503 storage-unavailable). 4xx client rejections are INFO, not alerts (no alert noise).
publish.run_failed / schedule.run_failed — a failed publish/scheduled run.
publish.cap_hit — folds in the #14 follow-up: the per-run cap-hit previously only logged; it is now also emitted as a structured, alertable OTEL signal (WARN log with alert.type=publish.cap_hit + cap/published/pending/deferred counts, plus a span event/attribute). The human log line is retained.
OTEL-Go SDK vs. minimal exporter (the TinyGo/Wasm decision)
I went minimal/hand-rolled, not go.opentelemetry.io/otel/sdk. The full OTEL-Go SDK + OTLP exporters pull in a large, reflection-heavy dependency tree (google.golang.org/protobuf, grpc, option plumbing) that bloats the Wasm binary and is unreliable under TinyGo — and go.mod currently has a single dependency. The new internal/telemetry package is a small span/log shim plus an OTLP/HTTP JSON exporter over net/http, using only stdlib already proven under this project's js/wasm target (net/http per #26, encoding/json, crypto/rand). The wire format is OTLP, so any OTLP-compatible collector/backend can ingest it — only the SDK is hand-rolled, not the protocol. GOOS=js GOARCH=wasm go build ./... compiles (the real fit check).
Behaviour-preserving / #16-safe
Instrumentation is threaded through context: instrumented code pulls an optional *telemetry.Telemetry from ctx; when absent (the default until an endpoint is configured) or built with a nil exporter, every method is a no-op and behaviour is byte-identical. Consequently no public signatures changed — ingest.NewHandler, handler.New, publish.New/Publish, and schedule.Run are untouched, so the parallel #16 integration tests that construct these via their current APIs keep compiling. Every pre-existing test passes unchanged.
OTLP endpoint config (TBD)
The endpoint/backend is deliberately TBD and never hardcoded:
OTEL_EXPORTER_OTLP_ENDPOINT (plain var) — base OTLP/HTTP URL; /v1/traces and /v1/logs are appended. Empty ⇒ telemetry disabled, Worker behaves exactly as before.
OTEL_EXPORTER_OTLP_HEADERS (Secrets Store secret) — auth header(s) in k=v,k2=v2 form; never committed.
worker/telemetry_wasm.go builds the exporter lazily per run (secret read + cached, like the encryption keyring) and injects the provider into the request/scheduled context. wrangler.jsonc gains only these OTEL keys.
Test evidence
Host unit tests use the in-memory MemoryExporter and are deterministic (injected clock + id generator). New coverage: ingest accepted/rejected/error spans+logs and the ingest.error alert; publish run + per-report spans, the publish.cap_hit and publish.run_failed alerts, empty-run tracing; schedule run span, gate-closed inertness, schedule.run_failed alert, and parent→child trace linkage; the OTLP/JSON encoding (hex ids, ns timestamps, int-as-string) + an httptest round trip (local mock, not a real backend); and the no-op default.
$ go vet ./...
(ok)
$ go test ./...
ok .../internal/crypto
ok .../internal/handler
ok .../internal/ingest
ok .../internal/lifecycle
ok .../internal/publish
ok .../internal/schedule
ok .../internal/scrub
ok .../internal/storage
ok .../internal/telemetry
$ GOOS=js GOARCH=wasm go build ./...
(wasm build ok)
Not done here (out of scope / per instructions): no real OTLP export (tests use the in-memory exporter), no deploy, no ci.yml change, no integration tests under internal/integration or test/ (that's #16). TinyGo was not run locally (not installed); the standard GOOS=js GOARCH=wasm build is the compile gate and the shim uses only stdlib already exercised by the project's TinyGo build.
## What this does
Instruments the Worker with **OpenTelemetry** — traces + structured logs over OTLP — plus alertable operational signals, in a way that fits the TinyGo/Wasm build and changes **no existing public signatures**.
### What is instrumented (spans + logs)
- **Ingest (`internal/ingest`)** — one `ingest.request` **server span** + a correlated structured log per request, classified as `accepted` / `rejected` / `rate_limited` / `error` with `http.response.status_code`. Instrumentation is **observe-only**: it wraps the response writer to read the status code and never alters the 202/400/413/415/405/503 contract.
- **Publish (`internal/publish`)** — a run-level `publish.run` span with a `publish.report` **child span per report** and a log per report (`published` / `failed`), plus run-level counts (`attempted` / `published` / `failed`).
- **Schedule (`internal/schedule`)** — a `schedule.run` span that **parents the publish run** (end-to-end trace: `schedule.run` → `publish.run` → `publish.report[]`), with the pending count and gate outcome.
Logs carry the active span's trace/span ids, so logs and traces correlate.
### Alerting signals
A backend alert rule can key on **span `status=Error`** and on structured log records carrying **`alert=true` + a specific `alert.type`**:
- `ingest.error` — a 5xx (e.g. 503 storage-unavailable). 4xx client rejections are INFO, **not** alerts (no alert noise).
- `publish.run_failed` / `schedule.run_failed` — a failed publish/scheduled run.
- **`publish.cap_hit`** — folds in the **#14 follow-up**: the per-run cap-hit previously *only logged*; it is now also emitted as a structured, alertable OTEL signal (WARN log with `alert.type=publish.cap_hit` + cap/published/pending/deferred counts, plus a span event/attribute). The human log line is retained.
### OTEL-Go SDK vs. minimal exporter (the TinyGo/Wasm decision)
I went **minimal/hand-rolled**, not `go.opentelemetry.io/otel/sdk`. The full OTEL-Go SDK + OTLP exporters pull in a large, reflection-heavy dependency tree (`google.golang.org/protobuf`, grpc, option plumbing) that bloats the Wasm binary and is unreliable under TinyGo — and `go.mod` currently has a single dependency. The new `internal/telemetry` package is a small span/log shim plus an **OTLP/HTTP JSON exporter over `net/http`**, using only stdlib already proven under this project's js/wasm target (`net/http` per #26, `encoding/json`, `crypto/rand`). The wire format is OTLP, so any OTLP-compatible collector/backend can ingest it — only the SDK is hand-rolled, not the protocol. **`GOOS=js GOARCH=wasm go build ./...` compiles** (the real fit check).
### Behaviour-preserving / #16-safe
Instrumentation is threaded through **context**: instrumented code pulls an optional `*telemetry.Telemetry` from `ctx`; when absent (the default until an endpoint is configured) or built with a nil exporter, **every method is a no-op** and behaviour is byte-identical. Consequently **no public signatures changed** — `ingest.NewHandler`, `handler.New`, `publish.New`/`Publish`, and `schedule.Run` are untouched, so the parallel #16 integration tests that construct these via their current APIs keep compiling. Every pre-existing test passes unchanged.
### OTLP endpoint config (TBD)
The endpoint/backend is deliberately **TBD** and never hardcoded:
- `OTEL_EXPORTER_OTLP_ENDPOINT` (plain var) — base OTLP/HTTP URL; `/v1/traces` and `/v1/logs` are appended. **Empty ⇒ telemetry disabled**, Worker behaves exactly as before.
- `OTEL_EXPORTER_OTLP_HEADERS` (**Secrets Store secret**) — auth header(s) in `k=v,k2=v2` form; never committed.
- `OTEL_SERVICE_NAME` (plain var) — `service.name` override.
`worker/telemetry_wasm.go` builds the exporter lazily per run (secret read + cached, like the encryption keyring) and injects the provider into the request/scheduled context. `wrangler.jsonc` gains only these OTEL keys.
## Test evidence
Host unit tests use the in-memory `MemoryExporter` and are deterministic (injected clock + id generator). New coverage: ingest accepted/rejected/error spans+logs and the `ingest.error` alert; publish run + per-report spans, the `publish.cap_hit` and `publish.run_failed` alerts, empty-run tracing; schedule run span, gate-closed inertness, `schedule.run_failed` alert, and parent→child trace linkage; the OTLP/JSON encoding (hex ids, ns timestamps, int-as-string) + an httptest round trip (local mock, **not** a real backend); and the no-op default.
```
$ go vet ./...
(ok)
$ go test ./...
ok .../internal/crypto
ok .../internal/handler
ok .../internal/ingest
ok .../internal/lifecycle
ok .../internal/publish
ok .../internal/schedule
ok .../internal/scrub
ok .../internal/storage
ok .../internal/telemetry
$ GOOS=js GOARCH=wasm go build ./...
(wasm build ok)
```
Not done here (out of scope / per instructions): no real OTLP export (tests use the in-memory exporter), no deploy, no `ci.yml` change, no integration tests under `internal/integration` or `test/` (that's #16). TinyGo was not run locally (not installed); the standard `GOOS=js GOARCH=wasm` build is the compile gate and the shim uses only stdlib already exercised by the project's TinyGo build.
Closes #17
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What this does
Instruments the Worker with OpenTelemetry — traces + structured logs over OTLP — plus alertable operational signals, in a way that fits the TinyGo/Wasm build and changes no existing public signatures.
What is instrumented (spans + logs)
internal/ingest) — oneingest.requestserver span + a correlated structured log per request, classified asaccepted/rejected/rate_limited/errorwithhttp.response.status_code. Instrumentation is observe-only: it wraps the response writer to read the status code and never alters the 202/400/413/415/405/503 contract.internal/publish) — a run-levelpublish.runspan with apublish.reportchild span per report and a log per report (published/failed), plus run-level counts (attempted/published/failed).internal/schedule) — aschedule.runspan that parents the publish run (end-to-end trace:schedule.run→publish.run→publish.report[]), with the pending count and gate outcome.Logs carry the active span's trace/span ids, so logs and traces correlate.
Alerting signals
A backend alert rule can key on span
status=Errorand on structured log records carryingalert=true+ a specificalert.type:ingest.error— a 5xx (e.g. 503 storage-unavailable). 4xx client rejections are INFO, not alerts (no alert noise).publish.run_failed/schedule.run_failed— a failed publish/scheduled run.publish.cap_hit— folds in the #14 follow-up: the per-run cap-hit previously only logged; it is now also emitted as a structured, alertable OTEL signal (WARN log withalert.type=publish.cap_hit+ cap/published/pending/deferred counts, plus a span event/attribute). The human log line is retained.OTEL-Go SDK vs. minimal exporter (the TinyGo/Wasm decision)
I went minimal/hand-rolled, not
go.opentelemetry.io/otel/sdk. The full OTEL-Go SDK + OTLP exporters pull in a large, reflection-heavy dependency tree (google.golang.org/protobuf, grpc, option plumbing) that bloats the Wasm binary and is unreliable under TinyGo — andgo.modcurrently has a single dependency. The newinternal/telemetrypackage is a small span/log shim plus an OTLP/HTTP JSON exporter overnet/http, using only stdlib already proven under this project's js/wasm target (net/httpper #26,encoding/json,crypto/rand). The wire format is OTLP, so any OTLP-compatible collector/backend can ingest it — only the SDK is hand-rolled, not the protocol.GOOS=js GOARCH=wasm go build ./...compiles (the real fit check).Behaviour-preserving / #16-safe
Instrumentation is threaded through context: instrumented code pulls an optional
*telemetry.Telemetryfromctx; when absent (the default until an endpoint is configured) or built with a nil exporter, every method is a no-op and behaviour is byte-identical. Consequently no public signatures changed —ingest.NewHandler,handler.New,publish.New/Publish, andschedule.Runare untouched, so the parallel #16 integration tests that construct these via their current APIs keep compiling. Every pre-existing test passes unchanged.OTLP endpoint config (TBD)
The endpoint/backend is deliberately TBD and never hardcoded:
OTEL_EXPORTER_OTLP_ENDPOINT(plain var) — base OTLP/HTTP URL;/v1/tracesand/v1/logsare appended. Empty ⇒ telemetry disabled, Worker behaves exactly as before.OTEL_EXPORTER_OTLP_HEADERS(Secrets Store secret) — auth header(s) ink=v,k2=v2form; never committed.OTEL_SERVICE_NAME(plain var) —service.nameoverride.worker/telemetry_wasm.gobuilds the exporter lazily per run (secret read + cached, like the encryption keyring) and injects the provider into the request/scheduled context.wrangler.jsoncgains only these OTEL keys.Test evidence
Host unit tests use the in-memory
MemoryExporterand are deterministic (injected clock + id generator). New coverage: ingest accepted/rejected/error spans+logs and theingest.erroralert; publish run + per-report spans, thepublish.cap_hitandpublish.run_failedalerts, empty-run tracing; schedule run span, gate-closed inertness,schedule.run_failedalert, and parent→child trace linkage; the OTLP/JSON encoding (hex ids, ns timestamps, int-as-string) + an httptest round trip (local mock, not a real backend); and the no-op default.Not done here (out of scope / per instructions): no real OTLP export (tests use the in-memory exporter), no deploy, no
ci.ymlchange, no integration tests underinternal/integrationortest/(that's #16). TinyGo was not run locally (not installed); the standardGOOS=js GOARCH=wasmbuild is the compile gate and the shim uses only stdlib already exercised by the project's TinyGo build.Closes #17