Instrument the Worker with OpenTelemetry traces + structured logs over OTLP, plus alertable signals, via a minimal hand-rolled OTLP/HTTP exporter that fits the TinyGo/Wasm Worker build. New internal/telemetry package (build-tag-free, host-tested): - Span/log shim: Telemetry provider, Span (attrs/status/events/end), Log (Info/Warn/Error), trace/span-id correlation, W3C-style ids from crypto/rand. - Exporter seam: MemoryExporter (in-memory, for tests) and OTLPHTTPExporter (OTLP/HTTP JSON over net/http). No go.opentelemetry.io/otel/sdk dependency: the full OTEL-Go SDK + OTLP exporters pull in a large, reflection-heavy tree (protobuf, grpc) that bloats the Wasm binary and is unreliable under TinyGo. The shim uses only stdlib already proven under this project's js/wasm target (net/http per #26, encoding/json, crypto/rand). OTLP is the wire format, so any OTLP backend can ingest it. - Behaviour-preserving by construction: instrumentation is threaded through context. Instrumented code pulls an optional *Telemetry from ctx; absent (or nil exporter) => every method is a no-op. No public signatures change (NewHandler, handler.New, publish.New/Publish, schedule.Run are untouched), so parallel work built on the current APIs keeps compiling. Instrumentation: - ingest: an "ingest.request" server span + correlated log per request, classifying accepted / rejected / rate_limited / error. Observe-only (wraps the response writer to read the status); the HTTP contract is unchanged. A 5xx (e.g. 503 storage-unavailable) sets the span to Error and emits the alertable ingest.error signal; 4xx client rejections are INFO, not alerts. - publish: a "publish.run" span with per-report "publish.report" child spans and a log per report (published/failed). A failed report/run sets Error and emits alert.type=publish.run_failed. The per-run cap-hit (folding in the #14 follow-up) is now emitted as a structured, alertable OTEL signal (alert.type=publish.cap_hit + counts), not merely a log line. - schedule: a "schedule.run" span parenting the publish run; a list/publish failure emits alert.type=schedule.run_failed. Config (OTLP endpoint TBD, issue #17): - OTEL_EXPORTER_OTLP_ENDPOINT (plain var) - base OTLP/HTTP URL; empty => telemetry disabled (Worker behaves as before). /v1/traces and /v1/logs are appended. - OTEL_EXPORTER_OTLP_HEADERS (Secrets Store secret) - auth header(s), never committed. OTEL_SERVICE_NAME (plain var) - service.name override. - worker/telemetry_wasm.go builds the exporter lazily per run and injects the provider into the request/scheduled context; wrangler.jsonc gains only these OTEL keys. Alerting: run-failure, cap-hit, and elevated-ingest-error are emitted as span status=Error and structured log records carrying alert=true + a specific alert.type, so a backend alert rule can key on them once the OTLP endpoint is chosen. Tests: host unit tests with the in-memory exporter assert the ingest spans+logs for accepted/rejected/error, the publish run span + per-report spans + the cap-hit and run-failed signals, the schedule run span + list-error alert, and the OTLP/JSON encoding + HTTP round trip (httptest, no real backend). No-op default verified. go vet ./... and go test ./... green; GOOS=js GOARCH=wasm go build ./... compiles. Closes #17 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
120 lines
5.9 KiB
JSON
120 lines
5.9 KiB
JSON
{
|
|
// Cloudflare Worker config for local dev and deploy.
|
|
// Docs: https://developers.cloudflare.com/workers/wrangler/configuration/
|
|
"name": "libremail-bug-report-ingest",
|
|
|
|
// Entry module. The TinyGo build (see package.json "build") produces the Wasm
|
|
// binary at ./build/app.wasm and workers-assets-gen produces this shim, which
|
|
// instantiates the Wasm module. The ./build/ artifacts are git-ignored and are
|
|
// created by `pnpm run build` (which requires TinyGo).
|
|
"main": "./build/worker.mjs",
|
|
|
|
"compatibility_date": "2025-06-01",
|
|
|
|
// Wrangler runs this before dev/deploy to (re)build the Wasm + shim.
|
|
// Requires TinyGo locally; TinyGo is installed in CI.
|
|
"build": {
|
|
"command": "pnpm run build"
|
|
},
|
|
|
|
// R2 bucket for the encrypted-at-rest bug-report objects (ADR #5 / issue #9).
|
|
// The Worker encrypts each scrubbed report with AES-256-GCM before writing, so
|
|
// only ciphertext is ever stored here. "binding" is the JS var the Worker code
|
|
// reads (internal/storage.BucketBinding); "bucket_name" matches the bucket
|
|
// provisioned by infra/ (defaultR2BucketName = "libremail-bug-reports").
|
|
"r2_buckets": [
|
|
{
|
|
"binding": "REPORTS_BUCKET",
|
|
"bucket_name": "libremail-bug-reports"
|
|
}
|
|
],
|
|
|
|
// Cloudflare Secrets Store secrets. "binding" is the JS var the Worker reads at
|
|
// runtime via env.<binding>.get(). Replace "<store-id>" with the account's
|
|
// Secrets Store id at deploy time; secrets are not needed for `pnpm run build`
|
|
// (Wasm compile) or the devserver, so CI does not require them.
|
|
//
|
|
// - BUGREPORT_ENC_KEYRING (ADR #5, Key custody): the versioned encryption
|
|
// keyring, a single JSON secret {active, keys{ver: base64-32B}}, read via
|
|
// internal/storage.KeyringBinding.
|
|
// - ADMIN_TOKEN (#11): the maintainer admin API shared secret (see the
|
|
// detailed note on its entry below).
|
|
// - GITHUB_TOKEN (#14): the token the weekly publish job authenticates to the
|
|
// GitHub API with to create issues on the target repo. Least privilege: a
|
|
// fine-grained PAT with Issues: read/write (and Metadata: read) on
|
|
// JMR-dev/LibreMail. Read via worker/scheduled_wasm.go githubTokenBinding.
|
|
"secrets_store_secrets": [
|
|
{
|
|
"binding": "BUGREPORT_ENC_KEYRING",
|
|
"store_id": "<store-id>",
|
|
"secret_name": "bugreport-enc-keyring"
|
|
},
|
|
// Maintainer admin API shared secret (issue #11). The Worker reads it per
|
|
// request via env.ADMIN_TOKEN.get() (internal/storage.AdminTokenBinding) to
|
|
// authenticate GET /v1/admin/reports and POST /v1/admin/reports/{id}/remove
|
|
// with a constant-time-compared Bearer token. Fail-closed: if this binding is
|
|
// absent or empty the admin routes reject every request (401/503). Replace
|
|
// "<store-id>" with the account's Secrets Store id at deploy time; not needed
|
|
// for `pnpm run build` (Wasm compile) or the devserver (which uses ADMIN_TOKEN
|
|
// from the environment instead).
|
|
{
|
|
"binding": "ADMIN_TOKEN",
|
|
"store_id": "<store-id>",
|
|
"secret_name": "bugreport-admin-token"
|
|
},
|
|
{
|
|
"binding": "GITHUB_TOKEN",
|
|
"store_id": "<store-id>",
|
|
"secret_name": "github-token"
|
|
},
|
|
// OpenTelemetry OTLP auth headers (#17). The OTLP exporter (internal/telemetry)
|
|
// sends these headers on every /v1/traces and /v1/logs POST — typically an auth
|
|
// token, e.g. "Authorization=Bearer <token>" or "x-api-key=<key>"; multiple are
|
|
// comma-separated ("k1=v1,k2=v2"). Kept as a Secrets Store secret so the token
|
|
// is never committed. OPTIONAL: if the OTLP backend needs no auth (or auth is in
|
|
// the endpoint URL) this can be omitted — the exporter then sends no auth header.
|
|
// The Worker reads it lazily per run via worker/telemetry_wasm.go
|
|
// otelHeadersSecret. Replace "<store-id>" at deploy time.
|
|
{
|
|
"binding": "OTEL_EXPORTER_OTLP_HEADERS",
|
|
"store_id": "<store-id>",
|
|
"secret_name": "otel-exporter-otlp-headers"
|
|
}
|
|
],
|
|
|
|
// Plain (non-secret) vars. GITHUB_REPO is the "owner/repo" the weekly publish
|
|
// job (#14) files issues on; it is configurable here without a code change and
|
|
// defaults to "JMR-dev/LibreMail" in worker/scheduled_wasm.go if unset. Read via
|
|
// cloudflare.Getenv(GITHUB_REPO).
|
|
//
|
|
// OpenTelemetry (#17): OTEL_EXPORTER_OTLP_ENDPOINT is the base OTLP/HTTP URL the
|
|
// exporter POSTs traces + logs to ("/v1/traces" and "/v1/logs" are appended).
|
|
// The backend/endpoint is deliberately TBD — left EMPTY here, which disables
|
|
// telemetry (the Worker behaves exactly as before) until a collector/backend is
|
|
// chosen and this is set (a non-secret URL; the auth token lives in the
|
|
// OTEL_EXPORTER_OTLP_HEADERS secret above). OTEL_SERVICE_NAME overrides the
|
|
// reported service.name (defaults to "libremail-bug-report-ingest").
|
|
"vars": {
|
|
"GITHUB_REPO": "JMR-dev/LibreMail",
|
|
"OTEL_EXPORTER_OTLP_ENDPOINT": "",
|
|
"OTEL_SERVICE_NAME": "libremail-bug-report-ingest"
|
|
},
|
|
|
|
// Cron Triggers for the weekly publish job (#13): "Friday 17:00 America/Chicago
|
|
// (Central), DST-correct". Cloudflare evaluates crons in UTC only and has no
|
|
// timezone support, and 17:00 Central is a different UTC hour depending on DST:
|
|
// 22:00 UTC during Central Daylight Time (CDT, UTC-5, summer) and 23:00 UTC
|
|
// during Central Standard Time (CST, UTC-6, winter). A single UTC cron therefore
|
|
// cannot express it, so we register BOTH candidate Friday UTC hours below. The
|
|
// Worker's scheduled handler (worker/scheduled_wasm.go -> internal/schedule)
|
|
// gates each fire on the real America/Chicago rule, so exactly one of the two
|
|
// does the publish work on any given Friday and the other is a no-op. This makes
|
|
// the trigger fire at 17:00 Central year-round with no manual DST maintenance.
|
|
"triggers": {
|
|
"crons": [
|
|
"0 22 * * 5", // Fridays 22:00 UTC == 17:00 Central during CDT (summer)
|
|
"0 23 * * 5" // Fridays 23:00 UTC == 17:00 Central during CST (winter)
|
|
]
|
|
}
|
|
}
|