From 7e46b34406ff06de89bdfa17f5083be29920592f Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Tue, 6 Oct 2026 20:11:46 -0400 Subject: [PATCH 01/15] build(deps): move to otel 1.47 and hold grpc off the 1.84 line otel/log 1.47 deprecates log/global; the replacement otel.GetLoggerProvider ships with otel 1.47, so the module set and the ten call sites move together. grpc stays at 1.83.2. Dependabot ignores grpc >= 1.84.0, < 1.85.0 (GO-2026-6443, no 1.84.x fix) and gets a 7-day npm cooldown to match pnpm's minimumReleaseAge. Co-Authored-By: Claude Opus 5.5 --- .github/dependabot.yml | 28 ++++++ CHANGELOG.md | 4 + go.mod | 52 +++++------ go.sum | 112 ++++++++++++------------ internal/observability/logger.go | 4 +- internal/observability/provider.go | 7 +- internal/observability/provider_test.go | 7 +- 7 files changed, 122 insertions(+), 92 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 249810fb..bd0631d2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,24 @@ updates: groups: go-deps: patterns: ["*"] + ignore: + # grpc 1.84.x carries GO-2026-6443 (a server panic on requests with no + # :authority/Host header) and `make vulncheck` fails on it, which turns + # the whole go-deps group red. The advisory marks 1.84.0 onward as + # affected until 1.85.0-dev.0.20260825072537-93e31b48545e, and no 1.84.x + # patch exists. Only that range is held, so the fixed 1.85.0 release is + # proposed as soon as it ships. Drop this ignore once grpc 1.85.0 is + # tagged, or earlier if a 1.84.x release lists GO-2026-6443 as fixed. + # Tracked in #643. + - dependency-name: "google.golang.org/grpc" + versions: [">= 1.84.0, < 1.85.0"] + # These releases require grpc >= 1.84.0, so bumping either would pull + # the vulnerable grpc in transitively despite the hold above. Drop both + # together with it. + - dependency-name: "go.opentelemetry.io/proto/otlp" + versions: [">= 1.11.1"] + - dependency-name: "github.com/grpc-ecosystem/grpc-gateway/v2" + versions: [">= 2.31.0"] commit-message: prefix: "deps" @@ -94,5 +112,15 @@ updates: # the MINOR bump that crosses the wire boundary. - dependency-name: "@wavehouse/sdk" update-types: ["version-update:semver-minor", "version-update:semver-major"] + # Dependabot knows nothing of pnpm's minimumReleaseAge (7 days, see + # pnpm-workspace.yaml), so its lockfiles failed our install. Matching the + # window means a release younger than it is not proposed at all. Cooldown + # covers the dependency being bumped, not the transitive packages + # Dependabot re-resolves, so a lockfile can still trip pnpm's policy check; + # that remainder stays open in #441. Security updates bypass cooldown. + # Not applied to gomod or github-actions: neither has an install-time age + # gate to stay consistent with. + cooldown: + default-days: 7 commit-message: prefix: "deps" diff --git a/CHANGELOG.md b/CHANGELOG.md index 005abb55..aa4a6510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Each tenant has a message queue of its own** (`internal/mq/{mq,subject,embedded}.go` (+ tests), `internal/ingest/{sweeper,worker}.go` (+ tests), `internal/api/{dlq,ingest}.go` (+ tests), `internal/app/{app,wire}.go` (+ tests), `internal/stream/{subscriber,hub}.go`, `internal/settings/{settings,store,registry}.go` (+ tests), `internal/testutil/{mocks,testutil}.go`, `clients/ts/src/{dlq,types}.ts` (+ tests), `docs/src/content/docs/{deployment,api,architecture,ingest-pipeline,durability,why-wavehouse}.md`, `docs/src/content/docs/sdk/{admin,reference,streaming}.md`, `docs/src/content/docs/{settings-directory,configuration}.mdx`, `AGENTS.md`): story 5b of the multi-tenant epic ([#583](https://github.com/Wave-RF/WaveHouse/issues/583)). The embedded NATS server keeps each tenant's events on a pair of JetStream streams of its own — `INGEST_` (`ingest..>`, `DiscardNew`) at the tenant's own `mq.max_bytes_gb`, and `DLQ_` (`dlq..>`, `DiscardOld`) at a tenth of it — opened when the tenant is first served and kept, at the budget it last had, when its folder is rejected or removed; subjects are unchanged, and nothing outside `internal/mq` names a stream. A tenant at its budget gets `503` while the others keep publishing, and the ingest worker's and the hub bridge's durables are held on every tenant's stream, each with its own ack floor and `MaxAckPending`, so one tenant's backlog holds back neither another's delivery nor its purge; the worker's prefetch and the hub bridge's fetch-ahead are each shared across the tenants' streams. A removed or rejected tenant's stream is still consumed, so its queued rows reach the worker and are parked on its own dead-letter queue. The sweeper purges each tenant's stream at that tenant's own `stream.gap_window_minutes` — a rejected tenant's as its folder last had it (`settings.Registry.Known` now yields each tenant's last adopted store), and all of its history if the folder has been rejected since boot, so its clients resume once the folder is fixed — keeping no acknowledged history for a removed tenant, and every served tenant's `mq.max_bytes_gb` is applied after each reload rather than tenant `0`'s alone (the boot warning about a nested directory with no tenant `0` is gone with it, and so is the tracking of tenant `0`'s last adopted store: a flat directory's ops gate, its one reader left, reads tenant `0` through the registry). One tenant's failed purge holds up no other tenant's, and the sweep logs it at `ERROR` unless every failure in it is a buffer consumer not created yet. A reload that shrinks a budget no longer deletes dead letters: a dead-letter stream holding more than a tenth of the new budget keeps what it holds, and that is logged — the interim guard of [#532](https://github.com/Wave-RF/WaveHouse/issues/532). JetStream's own check of the streams' caps against the disk, which made them fit 75% of the free disk at boot together, is lifted: a budget is a cap and never a reservation, what the budgets add up to against the disk is [#138](https://github.com/Wave-RF/WaveHouse/issues/138)'s, and a flat directory whose budget exceeds three quarters of the free disk now boots where it used to be refused. A queue that cannot be opened refuses a flat boot like any other store and, over a nested directory, costs its tenant alone — its ingest answers `503`, each reload trying again, and a publish at most once every five seconds, so its clients retrying never hold up another tenant's queue. `GET /v1/ops/dlq/stats` reads one tenant's dead-letter queue — the one `?tenant=` names, parsed strictly like the other admin reads, and tenant `0`'s without it, no longer the sum across tenants — answering for a rejected or removed tenant too and `404` for a tenant with no queue; the SDK's `wh.dlq.list()` and `.table()` take a `tenant` option. The streams an earlier build kept for every tenant together (`WAVEHOUSE`, `WAVEHOUSE_DLQ`) overlap every tenant's subjects and are deleted at boot, what they held with them, and a subject with no tenant token no longer reads as tenant `0`'s. +- **OpenTelemetry 1.47 and the logger provider moved off `otel/log/global`** (`go.mod`, `go.sum`, `internal/observability/{logger,provider}.go`, `internal/observability/provider_test.go`): closes [#732](https://github.com/Wave-RF/WaveHouse/issues/732). `otel/log` 1.47.0 deprecates the `log/global` package in favour of `otel.GetLoggerProvider` and `otel.SetLoggerProvider`, which only exist from `otel` 1.47, so the module set (`otel`, `log`, `metric`, `trace`, `sdk`, `sdk/log`, `sdk/metric`, the OTLP and Prometheus exporters, `proto/otlp`) is bumped together with every call site, ten in all. gRPC stays at 1.83.2: nothing in the set requires 1.84. + +- **Dependabot holds gRPC back from the vulnerable 1.84 line, and npm updates honour the release-age window** (`.github/dependabot.yml`): closes [#643](https://github.com/Wave-RF/WaveHouse/issues/643) and refs [#441](https://github.com/Wave-RF/WaveHouse/issues/441). `google.golang.org/grpc` 1.84.x is affected by GO-2026-6443 and has no patched 1.84 release, so the `gomod` entry ignores `>= 1.84.0, < 1.85.0`; the fixed 1.85.0 is proposed automatically, and the comment says when to drop the ignore. The `npm` entry gets `cooldown: default-days: 7` to match `minimumReleaseAge` in `pnpm-workspace.yaml`. Cooldown covers the bumped package, not the transitive ones Dependabot re-resolves, so a lockfile can still fail pnpm's policy check; that remainder stays open in #441. + - **Message-queue subjects lead with the tenant, and the async paths read it off each message** (`internal/mq/{mq,subject,embedded}.go`, `internal/api/{ingest,stream}.go`, `internal/stream/hub.go`, `internal/ingest/{worker,sweeper}.go`, `internal/app/wire.go`, `docs/src/content/docs/{architecture,ingest-pipeline,deployment,api}.md`, `docs/src/content/docs/{settings-directory,access-control}.mdx`, `AGENTS.md`): story 5a of the multi-tenant epic ([#583](https://github.com/Wave-RF/WaveHouse/issues/583)). `mq.Topic` gains a `Tenant`, the leading token of every subject — `ingest..[.]`, `dlq..
[.]` — placed verbatim, since the tenant-id grammar makes it one token, so one wildcard selects a tenant's traffic (`ingest.acme.>`); a settings directory that holds the four files produces the same subjects with `0` as the token, and nothing else about them changes. The ingest and stream handlers address the request's tenant, which the resolved store carries (`settings.Store.Tenant`, from story 8). The stream hub indexes subscribers by the full topic and evaluates each event under its own tenant's policy, so a subscriber on one tenant's table never receives another tenant's rows for a table of the same name; a gap-fill and the opening schema frame read the connection's tenant. The ingest worker reads each message's tenant off its topic, batches per tenant table, and resolves the dead-letter switch under the row's own tenant — an envelope it cannot read is parked or dropped under the topic's tenant too — and bumps that tenant's cache namespaces (story 8's `invalidate` now receives the message's tenant rather than tenant `0`; the wiring's `sharedTables` still repeats each bump under every tenant the directory holds, since every tenant reads the same ClickHouse table until story 6). `Publish` and gap-fill refuse a topic whose tenant is empty or outside the grammar, so nothing lands on tenant `0` by omission. - **The query cache and its singleflight are keyed by tenant** (`internal/cache/{cache,local,version_manager}.go` (+ tests), `internal/settings/{store,registry}.go` (+ tests), `internal/api/{cache_key,pipes,structured_query}.go` (+ tests; `cache_tenant_test.go` new), `internal/ingest/worker.go` (+ tests), `docs/src/content/docs/{architecture,deployment,api,ingest-pipeline}.md`, `AGENTS.md`): story 8 of the multi-tenant epic ([#583](https://github.com/Wave-RF/WaveHouse/issues/583)), with no behavior change for a settings directory that holds the four files — every key simply gains tenant `0`'s prefix. The tenant leads every key the cached read paths build: the key `POST /v1/query?table={table}` and `GET/POST /v1/pipes/{name}` cache a result under is `:query:` and is their singleflight key too, and a version namespace is `.
.
.` (`cache.Namespace` gains `Tenant`; scope stays where it was, and inert). So identical requests from two tenants are two entries and two flights to ClickHouse, a tenant is never served another tenant's cached rows, and a batch the ingest worker inserts bumps the namespaces of the tenant it was inserted for and no other's (`IngestWorker.invalidate` takes the tenant as a parameter — the worker's own until story 5 reads it off the message, so over a nested directory it is still tenant `0`'s namespaces every batch bumps, and another tenant's cached query results expire on their TTL alone until then). The pool stays one Ristretto instance sized by `cache.l1_max_cost`. The handlers read the tenant off the request's store — `settings.Store.Tenant`, stamped by the registry when it creates the store (the commit is shared with story 7) — so nothing new rides the request context. This lands ahead of story 6 on purpose: once each tenant has its own ClickHouse connection, a tenant-blind key would be a silent cross-tenant read. diff --git a/go.mod b/go.mod index 9ff8605e..6360b8b5 100644 --- a/go.mod +++ b/go.mod @@ -48,17 +48,17 @@ require ( go.opentelemetry.io/contrib/bridges/otelslog v0.20.1 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 go.opentelemetry.io/contrib/instrumentation/runtime v0.71.0 - go.opentelemetry.io/otel v1.46.0 - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 - go.opentelemetry.io/otel/exporters/prometheus v0.68.0 - go.opentelemetry.io/otel/log v0.22.0 - go.opentelemetry.io/otel/metric v1.46.0 - go.opentelemetry.io/otel/sdk v1.46.0 - go.opentelemetry.io/otel/sdk/log v0.22.0 - go.opentelemetry.io/otel/sdk/metric v1.46.0 - go.opentelemetry.io/otel/trace v1.46.0 + go.opentelemetry.io/otel v1.47.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.23.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 + go.opentelemetry.io/otel/exporters/prometheus v0.69.0 + go.opentelemetry.io/otel/log v1.47.0 + go.opentelemetry.io/otel/metric v1.47.0 + go.opentelemetry.io/otel/sdk v1.47.0 + go.opentelemetry.io/otel/sdk/log v1.47.0 + go.opentelemetry.io/otel/sdk/metric v1.47.0 + go.opentelemetry.io/otel/trace v1.47.0 go.opentelemetry.io/proto/otlp v1.11.0 golang.org/x/sync v0.23.0 golang.org/x/time v0.15.0 @@ -189,10 +189,10 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.70.1 // indirect + github.com/prometheus/client_model v0.6.3 // indirect + github.com/prometheus/common v0.72.0 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect - github.com/prometheus/procfs v0.21.1 // indirect + github.com/prometheus/procfs v0.22.0 // indirect github.com/puzpuzpuz/xsync/v4 v4.5.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect @@ -209,22 +209,22 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/arch v0.26.0 // indirect - golang.org/x/crypto v0.56.0 // indirect - golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect golang.org/x/image v0.41.0 // indirect - golang.org/x/mod v0.38.0 // indirect - golang.org/x/net v0.58.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 // indirect - golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.41.0 // indirect - golang.org/x/tools v0.48.0 // indirect + golang.org/x/mod v0.41.0 // indirect + golang.org/x/net v0.59.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 // indirect + golang.org/x/term v0.46.0 // indirect + golang.org/x/text v0.42.0 // indirect + golang.org/x/tools v0.50.0 // indirect golang.org/x/vuln v1.3.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect google.golang.org/protobuf v1.36.12 // indirect gotest.tools/gotestsum v1.13.0 // indirect mvdan.cc/gofumpt v0.9.2 // indirect diff --git a/go.sum b/go.sum index bf6e503b..6809e066 100644 --- a/go.sum +++ b/go.sum @@ -344,14 +344,14 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= -github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= +github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= +github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= +github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= +github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= -github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= -github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= +github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= +github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg= github.com/puzpuzpuz/xsync/v4 v4.5.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= github.com/redis/rueidis v1.0.78 h1:hJXpEgC9IYfdwY4hCdaGYsfK+oUaAqvhI/GMy5akVJI= @@ -415,34 +415,34 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90U go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU= go.opentelemetry.io/contrib/instrumentation/runtime v0.71.0 h1:v4KkRLVvE1cWqTJDfZZkTCG+Z4aolsa6RVos0FX7vqE= go.opentelemetry.io/contrib/instrumentation/runtime v0.71.0/go.mod h1:g/xbuPC0XbgwMdKuyF5sKOUUEsorSkN6APydyFP/H9E= -go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= -go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0 h1:Bu39F5tzJct+f2IZbB8989fwyTps3c8e7EsUQsz+vs8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0/go.mod h1:dJUwod88EsFgYCqrDHaSPzhiY9pBUpt0d85/qSfua7k= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 h1:qkDYCAFiZXLcs1L4aY+tP2wguQ4kURANqHOQMA2et2s= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0/go.mod h1:tkipS4DRzmpAmvg+Gw4++O1IdDq6TVDnvnYU6cmbQVs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY= -go.opentelemetry.io/otel/exporters/prometheus v0.68.0 h1:QOf2IftqQwITVRJpnn0M7M9ZCbgWfxz4P7i9C9yc2N4= -go.opentelemetry.io/otel/exporters/prometheus v0.68.0/go.mod h1:bgSvqu2TWGXiz7yr5UTMfObH8oqxJWHTnubQ3ef9BO4= -go.opentelemetry.io/otel/log v0.22.0 h1:5DBNnfvaJ6CVdkJ+Jle8Tzs50aSSv49TXGj9XRsEYw0= -go.opentelemetry.io/otel/log v0.22.0/go.mod h1:gzOt/R67vF2GniAqWu8Qv0SXy89f71muHcrkz76PCdc= -go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= -go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= -go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU= -go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4= -go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI= -go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM= -go.opentelemetry.io/otel/sdk/log v0.22.0 h1:PRL+s6P63XT4E/bheEflopPUpVxuvANqZwtt89yhoGk= -go.opentelemetry.io/otel/sdk/log v0.22.0/go.mod h1:JNp0sBELrjCTcu5W3GzABVypeU6vDJjBS+X0JISuz+g= -go.opentelemetry.io/otel/sdk/log/logtest v0.22.0 h1:infPnfNrhCNgOUZRs3gWUg8vhoBUHihq02gwK05gzlg= -go.opentelemetry.io/otel/sdk/log/logtest v0.22.0/go.mod h1:gkQZA3z15Bv3KU9vigBTi8dFechSozRP7v94X4VZv+s= -go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= -go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= -go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= -go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.23.0 h1:5Ek4qQ5XX0PSQfY7lMW3CvcR36yy8YFDfGnYnaku8zE= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.23.0/go.mod h1:NUKIMOKSexQdTmS1pSxOQBDiVes3DHRier2ePsGW/gQ= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0 h1:LaLVGJtIzHcvHjVs2OftmKcc+M9mdOqS8vZ1fBD1Yes= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0/go.mod h1:vF6H5sNDuQBJWgSX00iZgbIqk2MZZoVMH0ar5t5bOpw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 h1:UFpxOpYPmMNUtOWhdb+nC1WELIgVf8z9higURrbzYU4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0/go.mod h1:YBGjxe3lt0jtXoEXxGrGhv/jM7oUBUDVK7zhAvNyEjc= +go.opentelemetry.io/otel/exporters/prometheus v0.69.0 h1:LQVBTGnHvzrfGJpDoCSVTY7eaLZArvaTSq5l84Skxew= +go.opentelemetry.io/otel/exporters/prometheus v0.69.0/go.mod h1:moif0nlgj/M16NRT8lDZVt8MC52+EOAJfR8EKRJ8LyU= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM= +go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/log v1.47.0 h1:W4N/ZfgpkRRnmARnqYoQ6BrWA0mrj8IzVDNLlpmUJZY= +go.opentelemetry.io/otel/sdk/log v1.47.0/go.mod h1:Wu74Z2EmOKXi2FleSNAb0Hyau2G+r5kubSEMJw5aAMs= +go.opentelemetry.io/otel/sdk/log/logtest v0.23.0 h1:j3pm42QCrKYYLLkI6OrV1QHFkUfDA6/4EP+Y1PzggLg= +go.opentelemetry.io/otel/sdk/log/logtest v0.23.0/go.mod h1:zt0gEnuTZgDvqvczHwYswz93eqMaIr6upTTsdV/HUlU= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -456,22 +456,22 @@ golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= -golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -487,24 +487,24 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 h1:RJhm5l6Fo4rmEIcndxDllNhhf/fAx8qIm4t6A7vpm2A= -golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 h1:F5BWKvW126NXR74uxkxuc1jQHhm/rwm/J3rSiFyuRs4= +golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518/go.mod h1:i+ivNqjDnTF3WTElsdk5g9V5DTSBYgdNo7xTU9SDwYA= +golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= +golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= +golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= @@ -517,10 +517,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 h1:ax2KzoSRIZU/M0cIxri3pKxy99vniH1PVxWC6si/eZI= -google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688/go.mod h1:1RJ9BQGyNdZwkGc1eTqkErfRZ6RJyYPHZo73BZ1vQqI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= diff --git a/internal/observability/logger.go b/internal/observability/logger.go index 9e744dd9..567735ff 100644 --- a/internal/observability/logger.go +++ b/internal/observability/logger.go @@ -8,7 +8,7 @@ import ( slogmulti "github.com/samber/slog-multi" slogsampling "github.com/samber/slog-sampling" "go.opentelemetry.io/contrib/bridges/otelslog" - "go.opentelemetry.io/otel/log/global" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" ) @@ -74,7 +74,7 @@ func NewLogger(component string, level *slog.LevelVar, isJSON bool, otlpSampleRa consoleHandler = slog.NewTextHandler(os.Stdout, opts) } - otelHandler := otelslog.NewHandler(component, otelslog.WithLoggerProvider(global.GetLoggerProvider())) + otelHandler := otelslog.NewHandler(component, otelslog.WithLoggerProvider(otel.GetLoggerProvider())) sampler := slogsampling.CustomSamplingOption{ Sampler: otlpSamplerFn(otlpSampleRate), diff --git a/internal/observability/provider.go b/internal/observability/provider.go index 81eb7e1b..31c69cbb 100644 --- a/internal/observability/provider.go +++ b/internal/observability/provider.go @@ -17,7 +17,6 @@ import ( "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" otelprom "go.opentelemetry.io/otel/exporters/prometheus" - "go.opentelemetry.io/otel/log/global" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/log" "go.opentelemetry.io/otel/sdk/metric" @@ -72,7 +71,7 @@ func InitProvider(ctx context.Context, serviceName string, cfg ProviderConfig) ( prevProp := otel.GetTextMapPropagator() prevTP := otel.GetTracerProvider() prevMP := otel.GetMeterProvider() - prevLP := global.GetLoggerProvider() + prevLP := otel.GetLoggerProvider() var shutdownFuncs []func(context.Context) error @@ -128,7 +127,7 @@ func InitProvider(ctx context.Context, serviceName string, cfg ProviderConfig) ( otel.SetTextMapPropagator(prevProp) otel.SetTracerProvider(prevTP) otel.SetMeterProvider(prevMP) - global.SetLoggerProvider(prevLP) + otel.SetLoggerProvider(prevLP) if shutErr := shutdown(ctx); shutErr != nil { slog.Warn("observability shutdown error during init cleanup", "shutdown_err", shutErr, "cause", inErr) @@ -242,7 +241,7 @@ func InitProvider(ctx context.Context, serviceName string, cfg ProviderConfig) ( log.WithResource(res), ) shutdownFuncs = append(shutdownFuncs, loggerProvider.Shutdown) - global.SetLoggerProvider(loggerProvider) + otel.SetLoggerProvider(loggerProvider) } return shutdown, promHandler, nil diff --git a/internal/observability/provider_test.go b/internal/observability/provider_test.go index cf890f7b..9941fb46 100644 --- a/internal/observability/provider_test.go +++ b/internal/observability/provider_test.go @@ -10,7 +10,6 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" otellog "go.opentelemetry.io/otel/log" - "go.opentelemetry.io/otel/log/global" ) // TestInitProvider_Shutdown verifies that the provider pipeline initializes @@ -76,12 +75,12 @@ func TestInitProvider_ShutdownParallelBounded(t *testing.T) { savedProp := otel.GetTextMapPropagator() savedTP := otel.GetTracerProvider() savedMP := otel.GetMeterProvider() - savedLP := global.GetLoggerProvider() + savedLP := otel.GetLoggerProvider() t.Cleanup(func() { otel.SetTextMapPropagator(savedProp) otel.SetTracerProvider(savedTP) otel.SetMeterProvider(savedMP) - global.SetLoggerProvider(savedLP) + otel.SetLoggerProvider(savedLP) }) shutdown, _, err := InitProvider(context.Background(), "wavehouse-test", ProviderConfig{ @@ -100,7 +99,7 @@ func TestInitProvider_ShutdownParallelBounded(t *testing.T) { ctr.Add(context.Background(), 1) var rec otellog.Record rec.SetBody(attribute.StringValue("shutdown-regression")) - global.GetLoggerProvider().Logger("test").Emit(context.Background(), rec) + otel.GetLoggerProvider().Logger("test").Emit(context.Background(), rec) shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() From 01196f2e9ddea535176ae415a2e82ce7b480a396 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Tue, 6 Oct 2026 20:32:37 -0400 Subject: [PATCH 02/15] fix(deps): finish the otel migration, fold in the logs TLS fix, document the holds The integration test still used otel/log/global. otlploggrpc 0.23.0 honours the TLS env vars, so the known-gap notes are corrected and the TLS test now covers logs. Dependabot gets cooldown excludes matching pnpm's, and the docs describe the holds, the cooldown and the endpoint handling accurately. Co-Authored-By: Claude Opus 5.5 --- .github/dependabot.yml | 7 ++++++- AGENTS.md | 2 +- CHANGELOG.md | 5 ++--- docs/src/content/docs/architecture.md | 2 +- docs/src/content/docs/configuration.mdx | 2 +- docs/src/content/docs/deployment.md | 2 +- docs/src/content/docs/development.md | 6 ++++-- internal/observability/provider.go | 6 +----- tests/integration/otel_test.go | 26 ++++++++++++------------- 9 files changed, 30 insertions(+), 28 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bd0631d2..34515ea4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,7 +17,8 @@ updates: # patch exists. Only that range is held, so the fixed 1.85.0 release is # proposed as soon as it ships. Drop this ignore once grpc 1.85.0 is # tagged, or earlier if a 1.84.x release lists GO-2026-6443 as fixed. - # Tracked in #643. + # Drop all three holds in this block together then. Tracked in #643, + # which stays open until they are gone. - dependency-name: "google.golang.org/grpc" versions: [">= 1.84.0, < 1.85.0"] # These releases require grpc >= 1.84.0, so bumping either would pull @@ -120,7 +121,11 @@ updates: # that remainder stays open in #441. Security updates bypass cooldown. # Not applied to gomod or github-actions: neither has an install-time age # gate to stay consistent with. + # The excludes mirror minimumReleaseAgeExclude: pnpm installs those at once. cooldown: default-days: 7 + exclude: + - "@wave-rf/*" + - "@wavehouse/*" commit-message: prefix: "deps" diff --git a/AGENTS.md b/AGENTS.md index face706d..40079c22 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,7 +67,7 @@ The invariant index — what must stay true. Full narrative and rationale live i 12. **Structured queries: column authz fail-closed (security)** — `POST /v1/query?table={table}`: typed AST validated against schema, permission-enforced, timestamp-bucketed for cache, `DefaultMaxRows` (10,000) cap. Every column reference — projection, aggregation args, `filters`, `group_by`, `order_by`, `time_range` — is authorized inside `query.Build` (the single chokepoint that enumerates them all), so no clause can skip the role's `allow_columns`/`deny_columns` check (#223). A `select_all` read by a *column-restricted* role expands to its allowed columns via `policy.AllowedProjection`, never a bare `SELECT *`; *unrestricted*/admin roles keep `SELECT *` (`policy.RestrictsColumns` decides). Omitting `columns` selects nothing (`ErrEmptyProjection` → `200 []`); `["*"]` is the literal column `*` (schema-gated, not a wildcard); a table-granted role with no readable columns fails closed (`ErrNoReadableColumns` → `403`). Structured and live-stream (`stream.projectIndices`) reads share the one per-column decision `policy.IsColumnAllowed`, so column visibility can't drift. Row visibility has the same one-source guarantee (#319): `Evaluate` resolves a role's row-`filter` once (`resolvePredicates`), and both surfaces consume that single resolution — the query path renders it to SQL (`predicatesToSQL`), the stream evaluates it in memory per subscriber (`ResolvedPermissions.RowVisible`, whose type-aware comparison fails closed on anything it can't prove about the ingested payload — `policy.ColumnSpec`, with `DateTime`/`DateTime64` operands compared as instants through the ingest grammar (`discovery.Column.TimeParser`) and claim constants rendered canonically and digit-exact by the one shared rule `policy.CanonicalScalar` (#457 — which also refuses a float64 at/past 2^53 rather than match a neighboring ID, and whose ok=false — an absent claim, a structured value, no canonical form — makes the predicate match no rows on BOTH surfaces: `1 = 0` in SQL, every row withheld in memory); numeric comparison runs in the column's STORAGE domain (`policy.NumericSpec`, classified by `discovery.NumericStorageOf` — Float width rounding, Decimal scale truncation, integer exactness, both operands narrowed as ClickHouse narrows stored value and bound constant, out-of-range operands refused rather than modeled; the `tests/integration` differential oracle holds in-range verdicts equal to a live ClickHouse's and the never-admit-where-SQL-hides direction for the refused out-of-range ones); an event whose insert later fails into the DLQ is the one residual payload-vs-stored asymmetry, documented in the access-control enforcement caution) — so row visibility can't drift either. Preserve when touching `internal/query` or the structured-query handler. Detail: architecture.md § `query/`. 13. **Named query pipes: fail-closed (security)** — pre-defined SQL templates (Tinybird-style) with param binding + caching — reads only: a pipe whose SQL `IsMutation` classifies as a write bypasses the cache and singleflight, since a cached or coalesced write is a dropped one (#386); `GET/POST /v1/pipes/{name}` sit outside `RequireAdmin`, so per-pipe `allowed_roles` is the *only* execute-path gate, via `policy.RoleAllowed`: exact allowlist membership (no `"*"`), admin always passes, empty/absent role and empty-string entries authorize nobody, and no `allowed_roles` → admin-only. Preserve and exercise via `testutil.RunRoleMatrix` / `StandardRoleMatrix` (see #159). Detail: architecture.md § `pipes/`. 14. **TypeScript SDK** — `@wavehouse/sdk`: typed query builder, real-time SSE over `fetch`, live queries (incrementable/decomposable/poll aggregation), codegen CLI. Exactly one runtime dependency — `eventsource-parser` (SSE framing, itself dependency-free); adding a second needs the same scrutiny the first got. The canonical client (see §SDK Sync). -15. **Observability invariants** — stdout always 100% (sampling is OTLP-push-only); WARN+ERROR always export at 100% (a non-configurable floor — don't expose it); gRPC OTel exporters dial lazily so an unreachable collector never blocks startup; the OTel Prometheus exporter uses a **private** `prometheus.Registry`. The OTLP endpoint/TLS/custom-CA/mTLS/headers are delegated to the OpenTelemetry SDK's standard `OTEL_EXPORTER_OTLP_*` env vars — `InitProvider` passes **no** endpoint/header options. Known gap, intentionally not patched in WaveHouse app code: the pinned gRPC logs exporter (`otlploggrpc` v0.19/v0.20) ignores the env TLS-cert vars, so a custom/private CA and mutual TLS apply to traces/metrics but **not** the logs signal (public-CA/system-roots TLS and plaintext still work for logs) — upstream bug open-telemetry/opentelemetry-go#6661. A malformed `OTEL_EXPORTER_OTLP_HEADERS` is logged and skipped by the SDK (fail-soft), not fatal. Preserve when touching the logger/sampler/provider. Detail: architecture.md § `observability/`. +15. **Observability invariants** — stdout always 100% (sampling is OTLP-push-only); WARN+ERROR always export at 100% (a non-configurable floor — don't expose it); gRPC OTel exporters dial lazily so an unreachable collector never blocks startup; the OTel Prometheus exporter uses a **private** `prometheus.Registry`. The OTLP endpoint/TLS/custom-CA/mTLS/headers are delegated to the OpenTelemetry SDK's standard `OTEL_EXPORTER_OTLP_*` env vars — `InitProvider` passes **no** endpoint/header options. A custom/private CA and mutual TLS apply to all three signals, logs included (the `otlploggrpc` exporter honours the env TLS-cert vars from v0.23.0). A malformed `OTEL_EXPORTER_OTLP_HEADERS` is logged and skipped by the SDK (fail-soft), not fatal. Preserve when touching the logger/sampler/provider. Detail: architecture.md § `observability/`. 16. **Bearer-token-only CORS posture (security)** — Bearer JWT on every request, no cookies/sessions; `corsMiddleware` deliberately **never** emits `Access-Control-Allow-Credentials` (not needed, and `*` + credentials is a spec violation browsers reject). `cors.allowed_origins` (settings directory, per tenant: a tenant route is decorated from the list of the tenant it names, everything else from tenant `0`'s — `corsOrigins`) controls who can *read* responses, not cookie scope; CSRF protection is structural. Don't reintroduce cookie auth or `Allow-Credentials` without a design discussion — answers GitHub #29/#30. Code: `internal/api/router.go`. 17. **Non-fatal boot** — schema-discovery failure on boot is non-fatal: `internal/app` records an `api.BootState`, binds `:8080`, serves 503 on `/livez`/`/readyz` with the diagnostic, and retries via `SchemaRegistry.RetryRefresh` (jittered backoff 2s → 60s), per tenant over a nested directory: `/livez` is 503 while no tenant has completed a first discovery, then sticky 200, and a tenant's outage after that is its log line and counter, never a probe failure. Until a tenant's first discovery its table lookups are a 503 with `Retry-After`, not a 404. Bounds supervisor restart loops. 18. **Health endpoints** — liveness `/livez`, readiness `/readyz` (k8s convention; `/readyz` pings every open ClickHouse pool at once and is ready at the first answer, 503 naming each when none answers); `/healthz` is a permanent alias of `/livez`; `/health` + `/ready` are deprecated (removal v0.2.0, CHANGELOG #144). `/v1/health` is the SDK's content-free public ping (no ClickHouse check), a `/v1` route so it survives reverse-proxy probe-path filtering. Point k8s at `/livez`/`/readyz`, SDK/online-checks at `/v1/health`, never the deprecated aliases. diff --git a/CHANGELOG.md b/CHANGELOG.md index aa4a6510..2feded95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,9 +53,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Each tenant has a message queue of its own** (`internal/mq/{mq,subject,embedded}.go` (+ tests), `internal/ingest/{sweeper,worker}.go` (+ tests), `internal/api/{dlq,ingest}.go` (+ tests), `internal/app/{app,wire}.go` (+ tests), `internal/stream/{subscriber,hub}.go`, `internal/settings/{settings,store,registry}.go` (+ tests), `internal/testutil/{mocks,testutil}.go`, `clients/ts/src/{dlq,types}.ts` (+ tests), `docs/src/content/docs/{deployment,api,architecture,ingest-pipeline,durability,why-wavehouse}.md`, `docs/src/content/docs/sdk/{admin,reference,streaming}.md`, `docs/src/content/docs/{settings-directory,configuration}.mdx`, `AGENTS.md`): story 5b of the multi-tenant epic ([#583](https://github.com/Wave-RF/WaveHouse/issues/583)). The embedded NATS server keeps each tenant's events on a pair of JetStream streams of its own — `INGEST_` (`ingest..>`, `DiscardNew`) at the tenant's own `mq.max_bytes_gb`, and `DLQ_` (`dlq..>`, `DiscardOld`) at a tenth of it — opened when the tenant is first served and kept, at the budget it last had, when its folder is rejected or removed; subjects are unchanged, and nothing outside `internal/mq` names a stream. A tenant at its budget gets `503` while the others keep publishing, and the ingest worker's and the hub bridge's durables are held on every tenant's stream, each with its own ack floor and `MaxAckPending`, so one tenant's backlog holds back neither another's delivery nor its purge; the worker's prefetch and the hub bridge's fetch-ahead are each shared across the tenants' streams. A removed or rejected tenant's stream is still consumed, so its queued rows reach the worker and are parked on its own dead-letter queue. The sweeper purges each tenant's stream at that tenant's own `stream.gap_window_minutes` — a rejected tenant's as its folder last had it (`settings.Registry.Known` now yields each tenant's last adopted store), and all of its history if the folder has been rejected since boot, so its clients resume once the folder is fixed — keeping no acknowledged history for a removed tenant, and every served tenant's `mq.max_bytes_gb` is applied after each reload rather than tenant `0`'s alone (the boot warning about a nested directory with no tenant `0` is gone with it, and so is the tracking of tenant `0`'s last adopted store: a flat directory's ops gate, its one reader left, reads tenant `0` through the registry). One tenant's failed purge holds up no other tenant's, and the sweep logs it at `ERROR` unless every failure in it is a buffer consumer not created yet. A reload that shrinks a budget no longer deletes dead letters: a dead-letter stream holding more than a tenth of the new budget keeps what it holds, and that is logged — the interim guard of [#532](https://github.com/Wave-RF/WaveHouse/issues/532). JetStream's own check of the streams' caps against the disk, which made them fit 75% of the free disk at boot together, is lifted: a budget is a cap and never a reservation, what the budgets add up to against the disk is [#138](https://github.com/Wave-RF/WaveHouse/issues/138)'s, and a flat directory whose budget exceeds three quarters of the free disk now boots where it used to be refused. A queue that cannot be opened refuses a flat boot like any other store and, over a nested directory, costs its tenant alone — its ingest answers `503`, each reload trying again, and a publish at most once every five seconds, so its clients retrying never hold up another tenant's queue. `GET /v1/ops/dlq/stats` reads one tenant's dead-letter queue — the one `?tenant=` names, parsed strictly like the other admin reads, and tenant `0`'s without it, no longer the sum across tenants — answering for a rejected or removed tenant too and `404` for a tenant with no queue; the SDK's `wh.dlq.list()` and `.table()` take a `tenant` option. The streams an earlier build kept for every tenant together (`WAVEHOUSE`, `WAVEHOUSE_DLQ`) overlap every tenant's subjects and are deleted at boot, what they held with them, and a subject with no tenant token no longer reads as tenant `0`'s. -- **OpenTelemetry 1.47 and the logger provider moved off `otel/log/global`** (`go.mod`, `go.sum`, `internal/observability/{logger,provider}.go`, `internal/observability/provider_test.go`): closes [#732](https://github.com/Wave-RF/WaveHouse/issues/732). `otel/log` 1.47.0 deprecates the `log/global` package in favour of `otel.GetLoggerProvider` and `otel.SetLoggerProvider`, which only exist from `otel` 1.47, so the module set (`otel`, `log`, `metric`, `trace`, `sdk`, `sdk/log`, `sdk/metric`, the OTLP and Prometheus exporters, `proto/otlp`) is bumped together with every call site, ten in all. gRPC stays at 1.83.2: nothing in the set requires 1.84. - -- **Dependabot holds gRPC back from the vulnerable 1.84 line, and npm updates honour the release-age window** (`.github/dependabot.yml`): closes [#643](https://github.com/Wave-RF/WaveHouse/issues/643) and refs [#441](https://github.com/Wave-RF/WaveHouse/issues/441). `google.golang.org/grpc` 1.84.x is affected by GO-2026-6443 and has no patched 1.84 release, so the `gomod` entry ignores `>= 1.84.0, < 1.85.0`; the fixed 1.85.0 is proposed automatically, and the comment says when to drop the ignore. The `npm` entry gets `cooldown: default-days: 7` to match `minimumReleaseAge` in `pnpm-workspace.yaml`. Cooldown covers the bumped package, not the transitive ones Dependabot re-resolves, so a lockfile can still fail pnpm's policy check; that remainder stays open in #441. +- **OpenTelemetry 1.47: the logger provider moves off `otel/log/global`, and the logs exporter honours the TLS env vars** (`go.mod`, `go.sum`, `internal/observability/{logger,provider}.go`, `internal/observability/provider_test.go`, `tests/integration/otel_test.go`, `AGENTS.md`, `docs/src/content/docs/{configuration.mdx,deployment.md,architecture.md}`): closes [#732](https://github.com/Wave-RF/WaveHouse/issues/732). `otel/log` 1.47.0 deprecates the `log/global` package in favour of `otel.GetLoggerProvider` and `otel.SetLoggerProvider`, which only exist from `otel` 1.47, so the `otel` modules (`otel`, `log`, `metric`, `trace`, `sdk`, `sdk/log`, `sdk/metric`, the OTLP and Prometheus exporters) are bumped together and every use of `log/global` (the import and its calls, in `internal/observability` and the integration test) moves with them. gRPC stays at 1.83.2, and so do `proto/otlp` (1.11.0) and `grpc-gateway` (2.30.0), whose newer releases require gRPC 1.84. `otlploggrpc` v0.23.0 also applies `OTEL_EXPORTER_OTLP_CERTIFICATE` and the client-certificate variables, so a private CA now works for logs as it does for traces and metrics; the docs no longer list that as a gap, and `TestOTel_TLSPath_AllSignals` covers all three signals. A scheme-less `OTEL_EXPORTER_OTLP_ENDPOINT` is never plaintext, but what it does now varies by host form and signal, so the docs say to always include the scheme. The architecture doc no longer describes an `Endpoint` field that `ProviderConfig` does not have. +- **Dependabot holds the Go modules that need the vulnerable gRPC 1.84, and npm updates honour the release-age window** (`.github/dependabot.yml`, `docs/src/content/docs/development.md`): refs [#643](https://github.com/Wave-RF/WaveHouse/issues/643) and [#441](https://github.com/Wave-RF/WaveHouse/issues/441). `google.golang.org/grpc` 1.84.x is affected by GO-2026-6443 and has no patched 1.84 release, so the `gomod` entry ignores `>= 1.84.0, < 1.85.0`, plus `go.opentelemetry.io/proto/otlp >= 1.11.1` and `grpc-gateway/v2 >= 2.31.0`, which require it. All three holds are dropped together when gRPC 1.85.0 is tagged; #643 tracks that. The `npm` entry gets `cooldown` with `default-days: 7` to match `minimumReleaseAge`, excluding the same `@wave-rf/*` and `@wavehouse/*` packages as `minimumReleaseAgeExclude`. Cooldown covers the bumped package, not the transitive ones Dependabot re-resolves, so a lockfile can still fail pnpm's policy check; that remainder stays open in #441. - **Message-queue subjects lead with the tenant, and the async paths read it off each message** (`internal/mq/{mq,subject,embedded}.go`, `internal/api/{ingest,stream}.go`, `internal/stream/hub.go`, `internal/ingest/{worker,sweeper}.go`, `internal/app/wire.go`, `docs/src/content/docs/{architecture,ingest-pipeline,deployment,api}.md`, `docs/src/content/docs/{settings-directory,access-control}.mdx`, `AGENTS.md`): story 5a of the multi-tenant epic ([#583](https://github.com/Wave-RF/WaveHouse/issues/583)). `mq.Topic` gains a `Tenant`, the leading token of every subject — `ingest..
[.]`, `dlq..
[.]` — placed verbatim, since the tenant-id grammar makes it one token, so one wildcard selects a tenant's traffic (`ingest.acme.>`); a settings directory that holds the four files produces the same subjects with `0` as the token, and nothing else about them changes. The ingest and stream handlers address the request's tenant, which the resolved store carries (`settings.Store.Tenant`, from story 8). The stream hub indexes subscribers by the full topic and evaluates each event under its own tenant's policy, so a subscriber on one tenant's table never receives another tenant's rows for a table of the same name; a gap-fill and the opening schema frame read the connection's tenant. The ingest worker reads each message's tenant off its topic, batches per tenant table, and resolves the dead-letter switch under the row's own tenant — an envelope it cannot read is parked or dropped under the topic's tenant too — and bumps that tenant's cache namespaces (story 8's `invalidate` now receives the message's tenant rather than tenant `0`; the wiring's `sharedTables` still repeats each bump under every tenant the directory holds, since every tenant reads the same ClickHouse table until story 6). `Publish` and gap-fill refuse a topic whose tenant is empty or outside the grammar, so nothing lands on tenant `0` by omission. diff --git a/docs/src/content/docs/architecture.md b/docs/src/content/docs/architecture.md index 7f42931b..13487ff9 100644 --- a/docs/src/content/docs/architecture.md +++ b/docs/src/content/docs/architecture.md @@ -188,7 +188,7 @@ The **only** package that imports NATS/JetStream — a `depguard` rule in `.gola ### `observability/` — OpenTelemetry Pipeline -- **provider.go** — `InitProvider(ctx, serviceName, ProviderConfig)` wires the OTel pipeline. Each output is independently gated; the W3C TraceContext + Baggage propagator is always installed (cheap, harmless when traces are off). Returns `(shutdown, promHandler http.Handler, err)` — `promHandler` is non-nil only when `PrometheusEnabled` is true and reads from a *private* `prometheus.Registry` to avoid leaking the process/Go collectors that `prometheus.DefaultRegisterer` auto-registers. OTLP-metrics push (`MetricsEnabled`) and Prometheus exposition (`PrometheusEnabled`) are independent: either, both, or neither may be set, and any combination produces a single MeterProvider feeding the active readers. The Endpoint field is only dialed by the OTLP exporters (traces / metrics-OTLP / logs); Prometheus-only operation leaves it untouched. Provider init in `internal/app` runs whenever `otel.enabled` OR `prometheus.enabled` is true, so Prometheus-only operation (Alloy/scrape, no collector) is a first-class mode. +- **provider.go** — `InitProvider(ctx, serviceName, ProviderConfig)` wires the OTel pipeline. Each output is independently gated; the W3C TraceContext + Baggage propagator is always installed (cheap, harmless when traces are off). Returns `(shutdown, promHandler http.Handler, err)` — `promHandler` is non-nil only when `PrometheusEnabled` is true and reads from a *private* `prometheus.Registry` to avoid leaking the process/Go collectors that `prometheus.DefaultRegisterer` auto-registers. OTLP-metrics push (`MetricsEnabled`) and Prometheus exposition (`PrometheusEnabled`) are independent: either, both, or neither may be set, and any combination produces a single MeterProvider feeding the active readers. The OTLP exporters (traces / metrics-OTLP / logs) take their endpoint from the `OTEL_EXPORTER_OTLP_*` env vars, not from `ProviderConfig`, so Prometheus-only operation dials nothing. Provider init in `internal/app` runs whenever `otel.enabled` OR `prometheus.enabled` is true, so Prometheus-only operation (Alloy/scrape, no collector) is a first-class mode. - **logger.go** — `NewLogger(component, level, isJSON, otlpSampleRate)` produces a slog logger that fans out to stdout (always 100%) and the OTLP log exporter (DEBUG/INFO sampled at `otlpSampleRate`, WARN/ERROR always 100% as a non-configurable safety floor). `TraceHandler` injects `trace_id`/`span_id` from the active span when one exists. `otlpSamplerFn` is exposed (lowercase) for unit testing the per-level rate logic without driving through the slogmulti middleware. - **metrics.go** — `RegisterSystemMetrics(mqStats, pebbleStats)` registers observable gauges for the MQ's NATS connections and in-msgs (the embedded server's, or under `nats` this process's client connection), and Pebble dedupe storage stats. Both are functions read on every scrape — `mqStats` a `func() (MQStats, error)` (`mq.Broker.Stats` in production; nil skips the MQ gauges), `pebbleStats` a `func() map[string]int64` (`dedupe.Embedded.Stats`, the one instance's figures; nil, or a nil map while it is closed, skips the Pebble gauges) — this package never holds the NATS server or a store. Wired in `internal/app` after the providers are up. - **tracer.go** — W3C TraceContext propagation over message headers (`InjectHeaders` / `ExtractHeaders` on a plain `map[string][]string`, the shape NATS and HTTP headers share) — `internal/mq` injects on every publish and extracts onto the delivered `Message.Ctx` on the `Subscribe` path; no consumer reads it yet (the SSE hub bridge forwards the bytes and starts no span), and the ingest worker's `Consumer` path skips extraction entirely (see `embedded.go` above), so nothing downstream of the queue is linked to the originating request span. diff --git a/docs/src/content/docs/configuration.mdx b/docs/src/content/docs/configuration.mdx index 69dfdcc2..c8935598 100644 --- a/docs/src/content/docs/configuration.mdx +++ b/docs/src/content/docs/configuration.mdx @@ -289,7 +289,7 @@ The master switch is `otel.enabled`. When `true`, each signal (traces/metrics/lo **Sampling rates apply only to the OTLP push path.** Stdout always emits 100% of records — operators using a scraping-style pipeline (Promtail/Grafana Alloy → Loki, Vector, Fluent Bit, etc.) set the collection rate at the scraper, not the application. WaveHouse pushes telemetry to an OTel collector; the scraper world owns its own ingest policy. If you want to throttle OTLP volume for cost, lower the rates below. If you want to throttle Loki/Datadog Logs/etc., do it at that pipeline. -**Direct-to-cloud OTLP.** The OTLP destination is configured through the standard [`OTEL_EXPORTER_OTLP_*` environment variables](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) read by the OpenTelemetry SDK, not WaveHouse config: `OTEL_EXPORTER_OTLP_ENDPOINT` (always include a scheme — `https://` selects TLS via system root CAs, `http://` selects plaintext; a scheme-less `host:port` is **not** plaintext, it is mis-parsed and falls back to the default. With the endpoint unset that default is **TLS** to `localhost:4317`, so a plaintext local collector needs `http://localhost:4317` set explicitly or `OTEL_EXPORTER_OTLP_INSECURE=true`), `OTEL_EXPORTER_OTLP_HEADERS` for per-RPC auth, `OTEL_EXPORTER_OTLP_CERTIFICATE` to trust a custom/private CA, and `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_KEY` for mutual TLS. Custom/private CA and mutual TLS apply to the trace and metric signals only: the pinned gRPC logs exporter ignores the env TLS-cert vars (upstream bug [open-telemetry/opentelemetry-go#6661](https://github.com/open-telemetry/opentelemetry-go/issues/6661)), so the logs signal falls back to system roots — route logs through a local collector if your gateway uses a private CA. With these set, WaveHouse ships telemetry straight to a TLS-protected cloud gateway (Grafana Cloud's OTLP gateway, Honeycomb, etc.) — no sidecar required (a sidecar is still useful for egress queuing, batching, and tail-based sampling; it's just no longer mandatory). A malformed `OTEL_EXPORTER_OTLP_HEADERS` entry is logged and skipped by the OpenTelemetry SDK (fail-soft) rather than failing startup. Datadog has no public direct-to-cloud OTLP endpoint — use the local DDOT Collector path in the [deployment guide](/deployment#observability). See [Deployment → Observability](/deployment#observability) for worked Honeycomb / Grafana Cloud examples. +**Direct-to-cloud OTLP.** The OTLP destination is configured through the standard [`OTEL_EXPORTER_OTLP_*` environment variables](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) read by the OpenTelemetry SDK, not WaveHouse config: `OTEL_EXPORTER_OTLP_ENDPOINT` (always include a scheme — `https://` selects TLS via system root CAs, `http://` selects plaintext; a scheme-less `host:port` is never plaintext, but what it does varies by host form and signal: traces and metrics dial a hostname over TLS, logs get an empty target, and an IP literal fails to parse and falls back to the default. With the endpoint unset that default is **TLS** to `localhost:4317`, so a plaintext local collector needs `http://localhost:4317` set explicitly or `OTEL_EXPORTER_OTLP_INSECURE=true`), `OTEL_EXPORTER_OTLP_HEADERS` for per-RPC auth, `OTEL_EXPORTER_OTLP_CERTIFICATE` to trust a custom/private CA, and `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_KEY` for mutual TLS. The custom/private CA and mutual TLS variables apply to all three signals (traces, metrics and logs). With these set, WaveHouse ships telemetry straight to a TLS-protected cloud gateway (Grafana Cloud's OTLP gateway, Honeycomb, etc.) — no sidecar required (a sidecar is still useful for egress queuing, batching, and tail-based sampling; it's just no longer mandatory). A malformed `OTEL_EXPORTER_OTLP_HEADERS` entry is logged and skipped by the OpenTelemetry SDK (fail-soft) rather than failing startup. Datadog has no public direct-to-cloud OTLP endpoint — use the local DDOT Collector path in the [deployment guide](/deployment#observability). See [Deployment → Observability](/deployment#observability) for worked Honeycomb / Grafana Cloud examples. | YAML Key | Env Var | Default | Description | | -------- | ------- | ------- | ----------- | diff --git a/docs/src/content/docs/deployment.md b/docs/src/content/docs/deployment.md index 05faba53..9e36991f 100644 --- a/docs/src/content/docs/deployment.md +++ b/docs/src/content/docs/deployment.md @@ -756,7 +756,7 @@ otel: ### Pattern: Direct-to-cloud OTLP (Honeycomb, Grafana Cloud) -Set `OTEL_EXPORTER_OTLP_ENDPOINT` to an `https://` URL to select TLS (system root CAs), and `OTEL_EXPORTER_OTLP_HEADERS` for the per-RPC auth every cloud OTLP gateway expects — no sidecar required to terminate TLS or inject auth. For a private or self-signed gateway, point `OTEL_EXPORTER_OTLP_CERTIFICATE` at the CA certificate; for mutual TLS, add `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` and `OTEL_EXPORTER_OTLP_CLIENT_KEY`. These apply to the **trace and metric** signals only — the pinned gRPC logs exporter ignores the env TLS-cert vars (upstream bug [open-telemetry/opentelemetry-go#6661](https://github.com/open-telemetry/opentelemetry-go/issues/6661)), so against a private-CA gateway the logs signal falls back to system roots and won't connect; route logs through a local collector (which terminates TLS itself) until the fix lands upstream. +Set `OTEL_EXPORTER_OTLP_ENDPOINT` to an `https://` URL to select TLS (system root CAs), and `OTEL_EXPORTER_OTLP_HEADERS` for the per-RPC auth every cloud OTLP gateway expects — no sidecar required to terminate TLS or inject auth. For a private or self-signed gateway, point `OTEL_EXPORTER_OTLP_CERTIFICATE` at the CA certificate; for mutual TLS, add `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` and `OTEL_EXPORTER_OTLP_CLIENT_KEY`. These apply to the trace, metric and log signals alike. **Honeycomb** (single endpoint, per-RPC auth): diff --git a/docs/src/content/docs/development.md b/docs/src/content/docs/development.md index f27cbe63..2e905138 100644 --- a/docs/src/content/docs/development.md +++ b/docs/src/content/docs/development.md @@ -584,7 +584,7 @@ For a combined security scan, run `make verify` — it runs `vulncheck` alongsid Dependabot is configured in `.github/dependabot.yml` to open weekly grouped PRs for three update configs: -- **Go modules** (root) — outdated or vulnerable Go dependencies, commit prefix `deps:` +- **Go modules** (root) — outdated or vulnerable Go dependencies, commit prefix `deps:`. `google.golang.org/grpc` 1.84.x is held back (GO-2026-6443, no 1.84.x fix), along with the two releases that require it, `go.opentelemetry.io/proto/otlp` 1.11.1 and `grpc-gateway/v2` 2.31.0; drop the three holds together when gRPC 1.85.0 ships ([#643](https://github.com/Wave-RF/WaveHouse/issues/643)) - **GitHub Actions** (root **and** `/.github/actions/setup-env`) — outdated action versions tracked against the SHA pins across `.github/workflows/*` and the `setup-env` composite action, commit prefix `ci:` - **npm — pnpm workspace** (root) — covers all three TypeScript packages (the docs site, the SDK, and the E2E tests) in one grouped PR, commit prefix `deps:` @@ -592,6 +592,8 @@ PRs are grouped per config to reduce noise. The npm config is pointed at the wor The GitHub Actions config names **two** directories. `directory: /` reaches `.github/workflows/` but does not descend into `.github/actions/*/action.yml`, so the `setup-env` composite action — which owns every cache in `ci.yml` — was invisible to Dependabot, and its pins went stale against upstream and diverged from `publish-npm.yml`, which Dependabot *does* track and which doesn't call `setup-env`. Listing its directory under `directories:` brings it into the same weekly group; **adding a composite action means adding its directory there**, because nothing else catches the drift. +The npm config sets a 7-day `cooldown` to match pnpm's `minimumReleaseAge`, with the same `@wave-rf/*` and `@wavehouse/*` exclusions as `minimumReleaseAgeExclude`. It covers only the package Dependabot bumps, so transitive packages can still fail pnpm's check ([#441](https://github.com/Wave-RF/WaveHouse/issues/441)), and security updates bypass it. + `typescript` majors are held back (`ignore: version-update:semver-major`) because `tsup` vendors a `rollup-plugin-dts` that crashes on TypeScript 7 during `clients/ts`'s `prepare` script — i.e. inside `pnpm install`, which takes every Node job down at once. See the comment in `.github/dependabot.yml` for the condition that lets it be removed. **No auto-merge.** Dependabot PRs go through the same merge gate as any other PR — an approval from the `@Wave-RF/wavehouse-admins` team (the ruleset's `required_reviewers` rule) plus the required checks. (The former `dependabot-automerge.yml`, which auto-approved and merged patch/minor bumps hands-off, was removed — every bump now gets a human admin review.) @@ -609,7 +611,7 @@ make release-sdk-ts VERSION=0.1.0 # tag clients/ts/v0.1.0 → @wavehouse/sdk o The one thing to do *before* tagging is promote the changelog: `AGENTS.md` requires every PR to add its entry under `## Unreleased`, so open a PR renaming that heading to `## [X.Y.Z] - YYYY-MM-DD` and adding the matching link reference at the foot of the file. Nothing in the release pipeline reads `CHANGELOG.md` — this is for the file's own readers. -**After** tagging an SDK release, raise `docs/package.json`'s `@wavehouse/sdk` range to the new line. The pin floats within `0.1.x` and stops at `0.2.0`, so a new **minor** does not reach the docs site — including its live hero demo — until someone bumps it, which is the point: the bump is where you decide that the deployed demo backend and the site's SDK move together. `minimumReleaseAgeExclude` already exempts `@wavehouse/*` from the 7-day cooldown, so the freshly published version installs immediately. Dependabot will **not** propose this one for you — `@wavehouse/sdk` is in the npm `ignore` list in `.github/dependabot.yml` for exactly this reason, since a grouped Monday `deps:` PR is reviewed by someone thinking about dependency hygiene, not about which wire the demo backend speaks. +**After** tagging an SDK release, raise `docs/package.json`'s `@wavehouse/sdk` range to the new line. The pin floats within `0.1.x` and stops at `0.2.0`, so a new **minor** does not reach the docs site — including its live hero demo — until someone bumps it, which is the point: the bump is where you decide that the deployed demo backend and the site's SDK move together. `minimumReleaseAgeExclude` already exempts `@wavehouse/*` from pnpm's 7-day `minimumReleaseAge` (and the Dependabot `cooldown` excludes it too), so the freshly published version installs immediately. Dependabot will **not** propose this one for you — `@wavehouse/sdk` is in the npm `ignore` list in `.github/dependabot.yml` for exactly this reason, since a grouped Monday `deps:` PR is reviewed by someone thinking about dependency hygiene, not about which wire the demo backend speaks. Each runs [`scripts/release.sh`](https://github.com/Wave-RF/WaveHouse/blob/main/scripts/release.sh), which preflights (on `main`, clean tree, in sync with `origin/main`, the tag free both locally and on the remote, the required `CI` check green on *this exact commit*), prints exactly what will be published, and asks before pushing. `DRY_RUN=1 make release-…` stops after the plan. Tag creation is admin-only via the `release tag protection` ruleset. diff --git a/internal/observability/provider.go b/internal/observability/provider.go index 31c69cbb..974832e3 100644 --- a/internal/observability/provider.go +++ b/internal/observability/provider.go @@ -225,11 +225,7 @@ func InitProvider(ctx context.Context, serviceName string, cfg ProviderConfig) ( if cfg.LogsEnabled { // Endpoint, TLS, and headers come from the SDK's OTEL_EXPORTER_OTLP_* - // env vars, same as traces/metrics. Known gap: the pinned otlploggrpc - // (v0.19) ignores the env TLS-cert vars, so a custom/private CA and - // mutual TLS do not apply to the logs signal (public-CA TLS and - // plaintext still work). Upstream bug, not worked around here: - // open-telemetry/opentelemetry-go#6661. + // env vars, same as traces/metrics. logExporter, err := otlploggrpc.New(ctx) if err != nil { handleErr(err) diff --git a/tests/integration/otel_test.go b/tests/integration/otel_test.go index 17aa0d0e..fb71db45 100644 --- a/tests/integration/otel_test.go +++ b/tests/integration/otel_test.go @@ -31,7 +31,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/log/global" "google.golang.org/grpc/metadata" "github.com/Wave-RF/WaveHouse/internal/observability" @@ -45,12 +44,12 @@ func guardOTelGlobals(t *testing.T) { savedProp := otel.GetTextMapPropagator() savedTP := otel.GetTracerProvider() savedMP := otel.GetMeterProvider() - savedLP := global.GetLoggerProvider() + savedLP := otel.GetLoggerProvider() t.Cleanup(func() { otel.SetTextMapPropagator(savedProp) otel.SetTracerProvider(savedTP) otel.SetMeterProvider(savedMP) - global.SetLoggerProvider(savedLP) + otel.SetLoggerProvider(savedLP) }) } @@ -320,16 +319,11 @@ func TestOTel_UnreachableEndpoint_DoesNotBlockStartupOrEmits(t *testing.T) { }() } -// TestOTel_TLSPath_TracesAndMetrics pins the https:// → TLS dial path with -// trust supplied via OTEL_EXPORTER_OTLP_CERTIFICATE — the same custom-CA path -// a real operator uses for a private gateway. A regression that broke the TLS -// dial would surface as a missing count on the corresponding receiver. -// -// Logs are deliberately excluded: the pinned otlploggrpc exporter ignores the -// env TLS-cert vars (upstream bug open-telemetry/opentelemetry-go#6661), so a -// custom CA does not apply to the logs signal. Log delivery + header -// propagation are covered by TestOTel_Headers_AppliedToAllSignals (plaintext). -func TestOTel_TLSPath_TracesAndMetrics(t *testing.T) { +// TestOTel_TLSPath_AllSignals pins the https:// → TLS dial path with trust +// supplied via OTEL_EXPORTER_OTLP_CERTIFICATE — the same custom-CA path a real +// operator uses for a private gateway. A regression that broke the TLS dial +// would surface as a missing count on the corresponding receiver. +func TestOTel_TLSPath_AllSignals(t *testing.T) { guardOTelGlobals(t) r := testutil.NewFakeOTLPTLS(t) t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "https://"+r.Addr()) @@ -339,6 +333,7 @@ func TestOTel_TLSPath_TracesAndMetrics(t *testing.T) { TracesEnabled: true, TracesSampleRate: 1.0, MetricsEnabled: true, + LogsEnabled: true, }) _, span := otel.Tracer("test").Start(context.Background(), "tls-op") @@ -348,11 +343,16 @@ func TestOTel_TLSPath_TracesAndMetrics(t *testing.T) { require.NoError(t, err) counter.Add(context.Background(), 1) + lvl := &slog.LevelVar{} + lvl.Set(slog.LevelInfo) + observability.NewLogger("wavehouse-test", lvl, true, 1.0).Info("tls-log") + drainCtx, drainCancel := context.WithTimeout(context.Background(), 5*time.Second) defer drainCancel() require.NoError(t, shutdown(drainCtx)) assert.Equal(t, 1, r.SpanCount(), "TLS path must deliver the span end-to-end") + assert.GreaterOrEqual(t, r.LogCount(), 1, "TLS path must deliver logs end-to-end") assert.GreaterOrEqual(t, r.MetricCount(), 1, "TLS path must deliver metrics end-to-end") } From 543b1a65df009ab1b738c08bdee5617149ea85d1 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 7 Oct 2026 06:38:02 -0400 Subject: [PATCH 03/15] docs(deps): date the logs TLS support to otlploggrpc v0.21.0 The logs exporter has applied the OTLP TLS env vars since v0.21.0, which v0.1.0 already shipped; this branch removes a stale docs gap rather than changing behaviour. Also say the companion holds cover later releases, what a scheme-less endpoint does to logs, and why Prometheus-only operation dials nothing. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- CHANGELOG.md | 2 +- docs/src/content/docs/architecture.md | 2 +- docs/src/content/docs/configuration.mdx | 2 +- docs/src/content/docs/development.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 40079c22..8e6fae9f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,7 +67,7 @@ The invariant index — what must stay true. Full narrative and rationale live i 12. **Structured queries: column authz fail-closed (security)** — `POST /v1/query?table={table}`: typed AST validated against schema, permission-enforced, timestamp-bucketed for cache, `DefaultMaxRows` (10,000) cap. Every column reference — projection, aggregation args, `filters`, `group_by`, `order_by`, `time_range` — is authorized inside `query.Build` (the single chokepoint that enumerates them all), so no clause can skip the role's `allow_columns`/`deny_columns` check (#223). A `select_all` read by a *column-restricted* role expands to its allowed columns via `policy.AllowedProjection`, never a bare `SELECT *`; *unrestricted*/admin roles keep `SELECT *` (`policy.RestrictsColumns` decides). Omitting `columns` selects nothing (`ErrEmptyProjection` → `200 []`); `["*"]` is the literal column `*` (schema-gated, not a wildcard); a table-granted role with no readable columns fails closed (`ErrNoReadableColumns` → `403`). Structured and live-stream (`stream.projectIndices`) reads share the one per-column decision `policy.IsColumnAllowed`, so column visibility can't drift. Row visibility has the same one-source guarantee (#319): `Evaluate` resolves a role's row-`filter` once (`resolvePredicates`), and both surfaces consume that single resolution — the query path renders it to SQL (`predicatesToSQL`), the stream evaluates it in memory per subscriber (`ResolvedPermissions.RowVisible`, whose type-aware comparison fails closed on anything it can't prove about the ingested payload — `policy.ColumnSpec`, with `DateTime`/`DateTime64` operands compared as instants through the ingest grammar (`discovery.Column.TimeParser`) and claim constants rendered canonically and digit-exact by the one shared rule `policy.CanonicalScalar` (#457 — which also refuses a float64 at/past 2^53 rather than match a neighboring ID, and whose ok=false — an absent claim, a structured value, no canonical form — makes the predicate match no rows on BOTH surfaces: `1 = 0` in SQL, every row withheld in memory); numeric comparison runs in the column's STORAGE domain (`policy.NumericSpec`, classified by `discovery.NumericStorageOf` — Float width rounding, Decimal scale truncation, integer exactness, both operands narrowed as ClickHouse narrows stored value and bound constant, out-of-range operands refused rather than modeled; the `tests/integration` differential oracle holds in-range verdicts equal to a live ClickHouse's and the never-admit-where-SQL-hides direction for the refused out-of-range ones); an event whose insert later fails into the DLQ is the one residual payload-vs-stored asymmetry, documented in the access-control enforcement caution) — so row visibility can't drift either. Preserve when touching `internal/query` or the structured-query handler. Detail: architecture.md § `query/`. 13. **Named query pipes: fail-closed (security)** — pre-defined SQL templates (Tinybird-style) with param binding + caching — reads only: a pipe whose SQL `IsMutation` classifies as a write bypasses the cache and singleflight, since a cached or coalesced write is a dropped one (#386); `GET/POST /v1/pipes/{name}` sit outside `RequireAdmin`, so per-pipe `allowed_roles` is the *only* execute-path gate, via `policy.RoleAllowed`: exact allowlist membership (no `"*"`), admin always passes, empty/absent role and empty-string entries authorize nobody, and no `allowed_roles` → admin-only. Preserve and exercise via `testutil.RunRoleMatrix` / `StandardRoleMatrix` (see #159). Detail: architecture.md § `pipes/`. 14. **TypeScript SDK** — `@wavehouse/sdk`: typed query builder, real-time SSE over `fetch`, live queries (incrementable/decomposable/poll aggregation), codegen CLI. Exactly one runtime dependency — `eventsource-parser` (SSE framing, itself dependency-free); adding a second needs the same scrutiny the first got. The canonical client (see §SDK Sync). -15. **Observability invariants** — stdout always 100% (sampling is OTLP-push-only); WARN+ERROR always export at 100% (a non-configurable floor — don't expose it); gRPC OTel exporters dial lazily so an unreachable collector never blocks startup; the OTel Prometheus exporter uses a **private** `prometheus.Registry`. The OTLP endpoint/TLS/custom-CA/mTLS/headers are delegated to the OpenTelemetry SDK's standard `OTEL_EXPORTER_OTLP_*` env vars — `InitProvider` passes **no** endpoint/header options. A custom/private CA and mutual TLS apply to all three signals, logs included (the `otlploggrpc` exporter honours the env TLS-cert vars from v0.23.0). A malformed `OTEL_EXPORTER_OTLP_HEADERS` is logged and skipped by the SDK (fail-soft), not fatal. Preserve when touching the logger/sampler/provider. Detail: architecture.md § `observability/`. +15. **Observability invariants** — stdout always 100% (sampling is OTLP-push-only); WARN+ERROR always export at 100% (a non-configurable floor — don't expose it); gRPC OTel exporters dial lazily so an unreachable collector never blocks startup; the OTel Prometheus exporter uses a **private** `prometheus.Registry`. The OTLP endpoint/TLS/custom-CA/mTLS/headers are delegated to the OpenTelemetry SDK's standard `OTEL_EXPORTER_OTLP_*` env vars — `InitProvider` passes **no** endpoint/header options. A custom/private CA and mutual TLS apply to all three signals, logs included (the `otlploggrpc` exporter has honoured the env TLS-cert vars since v0.21.0). A malformed `OTEL_EXPORTER_OTLP_HEADERS` is logged and skipped by the SDK (fail-soft), not fatal. Preserve when touching the logger/sampler/provider. Detail: architecture.md § `observability/`. 16. **Bearer-token-only CORS posture (security)** — Bearer JWT on every request, no cookies/sessions; `corsMiddleware` deliberately **never** emits `Access-Control-Allow-Credentials` (not needed, and `*` + credentials is a spec violation browsers reject). `cors.allowed_origins` (settings directory, per tenant: a tenant route is decorated from the list of the tenant it names, everything else from tenant `0`'s — `corsOrigins`) controls who can *read* responses, not cookie scope; CSRF protection is structural. Don't reintroduce cookie auth or `Allow-Credentials` without a design discussion — answers GitHub #29/#30. Code: `internal/api/router.go`. 17. **Non-fatal boot** — schema-discovery failure on boot is non-fatal: `internal/app` records an `api.BootState`, binds `:8080`, serves 503 on `/livez`/`/readyz` with the diagnostic, and retries via `SchemaRegistry.RetryRefresh` (jittered backoff 2s → 60s), per tenant over a nested directory: `/livez` is 503 while no tenant has completed a first discovery, then sticky 200, and a tenant's outage after that is its log line and counter, never a probe failure. Until a tenant's first discovery its table lookups are a 503 with `Retry-After`, not a 404. Bounds supervisor restart loops. 18. **Health endpoints** — liveness `/livez`, readiness `/readyz` (k8s convention; `/readyz` pings every open ClickHouse pool at once and is ready at the first answer, 503 naming each when none answers); `/healthz` is a permanent alias of `/livez`; `/health` + `/ready` are deprecated (removal v0.2.0, CHANGELOG #144). `/v1/health` is the SDK's content-free public ping (no ClickHouse check), a `/v1` route so it survives reverse-proxy probe-path filtering. Point k8s at `/livez`/`/readyz`, SDK/online-checks at `/v1/health`, never the deprecated aliases. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2feded95..fb516589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Each tenant has a message queue of its own** (`internal/mq/{mq,subject,embedded}.go` (+ tests), `internal/ingest/{sweeper,worker}.go` (+ tests), `internal/api/{dlq,ingest}.go` (+ tests), `internal/app/{app,wire}.go` (+ tests), `internal/stream/{subscriber,hub}.go`, `internal/settings/{settings,store,registry}.go` (+ tests), `internal/testutil/{mocks,testutil}.go`, `clients/ts/src/{dlq,types}.ts` (+ tests), `docs/src/content/docs/{deployment,api,architecture,ingest-pipeline,durability,why-wavehouse}.md`, `docs/src/content/docs/sdk/{admin,reference,streaming}.md`, `docs/src/content/docs/{settings-directory,configuration}.mdx`, `AGENTS.md`): story 5b of the multi-tenant epic ([#583](https://github.com/Wave-RF/WaveHouse/issues/583)). The embedded NATS server keeps each tenant's events on a pair of JetStream streams of its own — `INGEST_` (`ingest..>`, `DiscardNew`) at the tenant's own `mq.max_bytes_gb`, and `DLQ_` (`dlq..>`, `DiscardOld`) at a tenth of it — opened when the tenant is first served and kept, at the budget it last had, when its folder is rejected or removed; subjects are unchanged, and nothing outside `internal/mq` names a stream. A tenant at its budget gets `503` while the others keep publishing, and the ingest worker's and the hub bridge's durables are held on every tenant's stream, each with its own ack floor and `MaxAckPending`, so one tenant's backlog holds back neither another's delivery nor its purge; the worker's prefetch and the hub bridge's fetch-ahead are each shared across the tenants' streams. A removed or rejected tenant's stream is still consumed, so its queued rows reach the worker and are parked on its own dead-letter queue. The sweeper purges each tenant's stream at that tenant's own `stream.gap_window_minutes` — a rejected tenant's as its folder last had it (`settings.Registry.Known` now yields each tenant's last adopted store), and all of its history if the folder has been rejected since boot, so its clients resume once the folder is fixed — keeping no acknowledged history for a removed tenant, and every served tenant's `mq.max_bytes_gb` is applied after each reload rather than tenant `0`'s alone (the boot warning about a nested directory with no tenant `0` is gone with it, and so is the tracking of tenant `0`'s last adopted store: a flat directory's ops gate, its one reader left, reads tenant `0` through the registry). One tenant's failed purge holds up no other tenant's, and the sweep logs it at `ERROR` unless every failure in it is a buffer consumer not created yet. A reload that shrinks a budget no longer deletes dead letters: a dead-letter stream holding more than a tenth of the new budget keeps what it holds, and that is logged — the interim guard of [#532](https://github.com/Wave-RF/WaveHouse/issues/532). JetStream's own check of the streams' caps against the disk, which made them fit 75% of the free disk at boot together, is lifted: a budget is a cap and never a reservation, what the budgets add up to against the disk is [#138](https://github.com/Wave-RF/WaveHouse/issues/138)'s, and a flat directory whose budget exceeds three quarters of the free disk now boots where it used to be refused. A queue that cannot be opened refuses a flat boot like any other store and, over a nested directory, costs its tenant alone — its ingest answers `503`, each reload trying again, and a publish at most once every five seconds, so its clients retrying never hold up another tenant's queue. `GET /v1/ops/dlq/stats` reads one tenant's dead-letter queue — the one `?tenant=` names, parsed strictly like the other admin reads, and tenant `0`'s without it, no longer the sum across tenants — answering for a rejected or removed tenant too and `404` for a tenant with no queue; the SDK's `wh.dlq.list()` and `.table()` take a `tenant` option. The streams an earlier build kept for every tenant together (`WAVEHOUSE`, `WAVEHOUSE_DLQ`) overlap every tenant's subjects and are deleted at boot, what they held with them, and a subject with no tenant token no longer reads as tenant `0`'s. -- **OpenTelemetry 1.47: the logger provider moves off `otel/log/global`, and the logs exporter honours the TLS env vars** (`go.mod`, `go.sum`, `internal/observability/{logger,provider}.go`, `internal/observability/provider_test.go`, `tests/integration/otel_test.go`, `AGENTS.md`, `docs/src/content/docs/{configuration.mdx,deployment.md,architecture.md}`): closes [#732](https://github.com/Wave-RF/WaveHouse/issues/732). `otel/log` 1.47.0 deprecates the `log/global` package in favour of `otel.GetLoggerProvider` and `otel.SetLoggerProvider`, which only exist from `otel` 1.47, so the `otel` modules (`otel`, `log`, `metric`, `trace`, `sdk`, `sdk/log`, `sdk/metric`, the OTLP and Prometheus exporters) are bumped together and every use of `log/global` (the import and its calls, in `internal/observability` and the integration test) moves with them. gRPC stays at 1.83.2, and so do `proto/otlp` (1.11.0) and `grpc-gateway` (2.30.0), whose newer releases require gRPC 1.84. `otlploggrpc` v0.23.0 also applies `OTEL_EXPORTER_OTLP_CERTIFICATE` and the client-certificate variables, so a private CA now works for logs as it does for traces and metrics; the docs no longer list that as a gap, and `TestOTel_TLSPath_AllSignals` covers all three signals. A scheme-less `OTEL_EXPORTER_OTLP_ENDPOINT` is never plaintext, but what it does now varies by host form and signal, so the docs say to always include the scheme. The architecture doc no longer describes an `Endpoint` field that `ProviderConfig` does not have. +- **OpenTelemetry 1.47: the logger provider moves off `otel/log/global`** (`go.mod`, `go.sum`, `internal/observability/{logger,provider}.go`, `internal/observability/provider_test.go`, `tests/integration/otel_test.go`, `AGENTS.md`, `docs/src/content/docs/{configuration.mdx,deployment.md,architecture.md}`): closes [#732](https://github.com/Wave-RF/WaveHouse/issues/732). `otel/log` 1.47.0 deprecates the `log/global` package in favour of `otel.GetLoggerProvider` and `otel.SetLoggerProvider`, which only exist from `otel` 1.47, so the `otel` modules (`otel`, `log`, `metric`, `trace`, `sdk`, `sdk/log`, `sdk/metric`, the OTLP and Prometheus exporters) are bumped together and every use of `log/global` (the import and its calls, in `internal/observability` and the integration test) moves with them. gRPC stays at 1.83.2, and so do `proto/otlp` (1.11.0) and `grpc-gateway` (2.30.0), whose newer releases require gRPC 1.84. The docs' "logs ignore the TLS env vars" gap was stale: `otlploggrpc` has applied `OTEL_EXPORTER_OTLP_CERTIFICATE` and the client-certificate variables since v0.21.0, already in v0.1.0, so the docs no longer list it, and `TestOTel_TLSPath_AllSignals` now pins it for all three signals. A scheme-less `OTEL_EXPORTER_OTLP_ENDPOINT` is never plaintext, but what it does now varies by host form and signal, so the docs say to always include the scheme. The architecture doc no longer describes an `Endpoint` field that `ProviderConfig` does not have. - **Dependabot holds the Go modules that need the vulnerable gRPC 1.84, and npm updates honour the release-age window** (`.github/dependabot.yml`, `docs/src/content/docs/development.md`): refs [#643](https://github.com/Wave-RF/WaveHouse/issues/643) and [#441](https://github.com/Wave-RF/WaveHouse/issues/441). `google.golang.org/grpc` 1.84.x is affected by GO-2026-6443 and has no patched 1.84 release, so the `gomod` entry ignores `>= 1.84.0, < 1.85.0`, plus `go.opentelemetry.io/proto/otlp >= 1.11.1` and `grpc-gateway/v2 >= 2.31.0`, which require it. All three holds are dropped together when gRPC 1.85.0 is tagged; #643 tracks that. The `npm` entry gets `cooldown` with `default-days: 7` to match `minimumReleaseAge`, excluding the same `@wave-rf/*` and `@wavehouse/*` packages as `minimumReleaseAgeExclude`. Cooldown covers the bumped package, not the transitive ones Dependabot re-resolves, so a lockfile can still fail pnpm's policy check; that remainder stays open in #441. - **Message-queue subjects lead with the tenant, and the async paths read it off each message** (`internal/mq/{mq,subject,embedded}.go`, `internal/api/{ingest,stream}.go`, `internal/stream/hub.go`, `internal/ingest/{worker,sweeper}.go`, `internal/app/wire.go`, `docs/src/content/docs/{architecture,ingest-pipeline,deployment,api}.md`, `docs/src/content/docs/{settings-directory,access-control}.mdx`, `AGENTS.md`): story 5a of the multi-tenant epic ([#583](https://github.com/Wave-RF/WaveHouse/issues/583)). `mq.Topic` gains a `Tenant`, the leading token of every subject — `ingest..
[.]`, `dlq..
[.]` — placed verbatim, since the tenant-id grammar makes it one token, so one wildcard selects a tenant's traffic (`ingest.acme.>`); a settings directory that holds the four files produces the same subjects with `0` as the token, and nothing else about them changes. The ingest and stream handlers address the request's tenant, which the resolved store carries (`settings.Store.Tenant`, from story 8). The stream hub indexes subscribers by the full topic and evaluates each event under its own tenant's policy, so a subscriber on one tenant's table never receives another tenant's rows for a table of the same name; a gap-fill and the opening schema frame read the connection's tenant. The ingest worker reads each message's tenant off its topic, batches per tenant table, and resolves the dead-letter switch under the row's own tenant — an envelope it cannot read is parked or dropped under the topic's tenant too — and bumps that tenant's cache namespaces (story 8's `invalidate` now receives the message's tenant rather than tenant `0`; the wiring's `sharedTables` still repeats each bump under every tenant the directory holds, since every tenant reads the same ClickHouse table until story 6). `Publish` and gap-fill refuse a topic whose tenant is empty or outside the grammar, so nothing lands on tenant `0` by omission. diff --git a/docs/src/content/docs/architecture.md b/docs/src/content/docs/architecture.md index 13487ff9..67aa34e0 100644 --- a/docs/src/content/docs/architecture.md +++ b/docs/src/content/docs/architecture.md @@ -188,7 +188,7 @@ The **only** package that imports NATS/JetStream — a `depguard` rule in `.gola ### `observability/` — OpenTelemetry Pipeline -- **provider.go** — `InitProvider(ctx, serviceName, ProviderConfig)` wires the OTel pipeline. Each output is independently gated; the W3C TraceContext + Baggage propagator is always installed (cheap, harmless when traces are off). Returns `(shutdown, promHandler http.Handler, err)` — `promHandler` is non-nil only when `PrometheusEnabled` is true and reads from a *private* `prometheus.Registry` to avoid leaking the process/Go collectors that `prometheus.DefaultRegisterer` auto-registers. OTLP-metrics push (`MetricsEnabled`) and Prometheus exposition (`PrometheusEnabled`) are independent: either, both, or neither may be set, and any combination produces a single MeterProvider feeding the active readers. The OTLP exporters (traces / metrics-OTLP / logs) take their endpoint from the `OTEL_EXPORTER_OTLP_*` env vars, not from `ProviderConfig`, so Prometheus-only operation dials nothing. Provider init in `internal/app` runs whenever `otel.enabled` OR `prometheus.enabled` is true, so Prometheus-only operation (Alloy/scrape, no collector) is a first-class mode. +- **provider.go** — `InitProvider(ctx, serviceName, ProviderConfig)` wires the OTel pipeline. Each output is independently gated; the W3C TraceContext + Baggage propagator is always installed (cheap, harmless when traces are off). Returns `(shutdown, promHandler http.Handler, err)` — `promHandler` is non-nil only when `PrometheusEnabled` is true and reads from a *private* `prometheus.Registry` to avoid leaking the process/Go collectors that `prometheus.DefaultRegisterer` auto-registers. OTLP-metrics push (`MetricsEnabled`) and Prometheus exposition (`PrometheusEnabled`) are independent: either, both, or neither may be set, and any combination produces a single MeterProvider feeding the active readers. The OTLP exporters (traces / metrics-OTLP / logs) take their endpoint from the `OTEL_EXPORTER_OTLP_*` env vars, not from `ProviderConfig`, and are only constructed when their signal is enabled, so Prometheus-only operation dials nothing even with those vars set. Provider init in `internal/app` runs whenever `otel.enabled` OR `prometheus.enabled` is true, so Prometheus-only operation (Alloy/scrape, no collector) is a first-class mode. - **logger.go** — `NewLogger(component, level, isJSON, otlpSampleRate)` produces a slog logger that fans out to stdout (always 100%) and the OTLP log exporter (DEBUG/INFO sampled at `otlpSampleRate`, WARN/ERROR always 100% as a non-configurable safety floor). `TraceHandler` injects `trace_id`/`span_id` from the active span when one exists. `otlpSamplerFn` is exposed (lowercase) for unit testing the per-level rate logic without driving through the slogmulti middleware. - **metrics.go** — `RegisterSystemMetrics(mqStats, pebbleStats)` registers observable gauges for the MQ's NATS connections and in-msgs (the embedded server's, or under `nats` this process's client connection), and Pebble dedupe storage stats. Both are functions read on every scrape — `mqStats` a `func() (MQStats, error)` (`mq.Broker.Stats` in production; nil skips the MQ gauges), `pebbleStats` a `func() map[string]int64` (`dedupe.Embedded.Stats`, the one instance's figures; nil, or a nil map while it is closed, skips the Pebble gauges) — this package never holds the NATS server or a store. Wired in `internal/app` after the providers are up. - **tracer.go** — W3C TraceContext propagation over message headers (`InjectHeaders` / `ExtractHeaders` on a plain `map[string][]string`, the shape NATS and HTTP headers share) — `internal/mq` injects on every publish and extracts onto the delivered `Message.Ctx` on the `Subscribe` path; no consumer reads it yet (the SSE hub bridge forwards the bytes and starts no span), and the ingest worker's `Consumer` path skips extraction entirely (see `embedded.go` above), so nothing downstream of the queue is linked to the originating request span. diff --git a/docs/src/content/docs/configuration.mdx b/docs/src/content/docs/configuration.mdx index c8935598..a77cfb7d 100644 --- a/docs/src/content/docs/configuration.mdx +++ b/docs/src/content/docs/configuration.mdx @@ -289,7 +289,7 @@ The master switch is `otel.enabled`. When `true`, each signal (traces/metrics/lo **Sampling rates apply only to the OTLP push path.** Stdout always emits 100% of records — operators using a scraping-style pipeline (Promtail/Grafana Alloy → Loki, Vector, Fluent Bit, etc.) set the collection rate at the scraper, not the application. WaveHouse pushes telemetry to an OTel collector; the scraper world owns its own ingest policy. If you want to throttle OTLP volume for cost, lower the rates below. If you want to throttle Loki/Datadog Logs/etc., do it at that pipeline. -**Direct-to-cloud OTLP.** The OTLP destination is configured through the standard [`OTEL_EXPORTER_OTLP_*` environment variables](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) read by the OpenTelemetry SDK, not WaveHouse config: `OTEL_EXPORTER_OTLP_ENDPOINT` (always include a scheme — `https://` selects TLS via system root CAs, `http://` selects plaintext; a scheme-less `host:port` is never plaintext, but what it does varies by host form and signal: traces and metrics dial a hostname over TLS, logs get an empty target, and an IP literal fails to parse and falls back to the default. With the endpoint unset that default is **TLS** to `localhost:4317`, so a plaintext local collector needs `http://localhost:4317` set explicitly or `OTEL_EXPORTER_OTLP_INSECURE=true`), `OTEL_EXPORTER_OTLP_HEADERS` for per-RPC auth, `OTEL_EXPORTER_OTLP_CERTIFICATE` to trust a custom/private CA, and `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_KEY` for mutual TLS. The custom/private CA and mutual TLS variables apply to all three signals (traces, metrics and logs). With these set, WaveHouse ships telemetry straight to a TLS-protected cloud gateway (Grafana Cloud's OTLP gateway, Honeycomb, etc.) — no sidecar required (a sidecar is still useful for egress queuing, batching, and tail-based sampling; it's just no longer mandatory). A malformed `OTEL_EXPORTER_OTLP_HEADERS` entry is logged and skipped by the OpenTelemetry SDK (fail-soft) rather than failing startup. Datadog has no public direct-to-cloud OTLP endpoint — use the local DDOT Collector path in the [deployment guide](/deployment#observability). See [Deployment → Observability](/deployment#observability) for worked Honeycomb / Grafana Cloud examples. +**Direct-to-cloud OTLP.** The OTLP destination is configured through the standard [`OTEL_EXPORTER_OTLP_*` environment variables](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) read by the OpenTelemetry SDK, not WaveHouse config: `OTEL_EXPORTER_OTLP_ENDPOINT` (always include a scheme — `https://` selects TLS via system root CAs, `http://` selects plaintext; a scheme-less `host:port` is never plaintext, but what it does varies by host form and signal: traces and metrics dial a hostname over TLS, logs are not exported at all (the logs exporter is left with no destination and each export fails), and an IP literal fails to parse and falls back to the default. With the endpoint unset that default is **TLS** to `localhost:4317`, so a plaintext local collector needs `http://localhost:4317` set explicitly or `OTEL_EXPORTER_OTLP_INSECURE=true`), `OTEL_EXPORTER_OTLP_HEADERS` for per-RPC auth, `OTEL_EXPORTER_OTLP_CERTIFICATE` to trust a custom/private CA, and `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_KEY` for mutual TLS. The custom/private CA and mutual TLS variables apply to all three signals (traces, metrics and logs). With these set, WaveHouse ships telemetry straight to a TLS-protected cloud gateway (Grafana Cloud's OTLP gateway, Honeycomb, etc.) — no sidecar required (a sidecar is still useful for egress queuing, batching, and tail-based sampling; it's just no longer mandatory). A malformed `OTEL_EXPORTER_OTLP_HEADERS` entry is logged and skipped by the OpenTelemetry SDK (fail-soft) rather than failing startup. Datadog has no public direct-to-cloud OTLP endpoint — use the local DDOT Collector path in the [deployment guide](/deployment#observability). See [Deployment → Observability](/deployment#observability) for worked Honeycomb / Grafana Cloud examples. | YAML Key | Env Var | Default | Description | | -------- | ------- | ------- | ----------- | diff --git a/docs/src/content/docs/development.md b/docs/src/content/docs/development.md index 2e905138..0a7cd9fe 100644 --- a/docs/src/content/docs/development.md +++ b/docs/src/content/docs/development.md @@ -584,7 +584,7 @@ For a combined security scan, run `make verify` — it runs `vulncheck` alongsid Dependabot is configured in `.github/dependabot.yml` to open weekly grouped PRs for three update configs: -- **Go modules** (root) — outdated or vulnerable Go dependencies, commit prefix `deps:`. `google.golang.org/grpc` 1.84.x is held back (GO-2026-6443, no 1.84.x fix), along with the two releases that require it, `go.opentelemetry.io/proto/otlp` 1.11.1 and `grpc-gateway/v2` 2.31.0; drop the three holds together when gRPC 1.85.0 ships ([#643](https://github.com/Wave-RF/WaveHouse/issues/643)) +- **Go modules** (root) — outdated or vulnerable Go dependencies, commit prefix `deps:`. `google.golang.org/grpc` 1.84.x is held back (GO-2026-6443, no 1.84.x fix), along with the releases that require it, `go.opentelemetry.io/proto/otlp` 1.11.1 and later and `grpc-gateway/v2` 2.31.0 and later; drop the three holds together when gRPC 1.85.0 ships ([#643](https://github.com/Wave-RF/WaveHouse/issues/643)) - **GitHub Actions** (root **and** `/.github/actions/setup-env`) — outdated action versions tracked against the SHA pins across `.github/workflows/*` and the `setup-env` composite action, commit prefix `ci:` - **npm — pnpm workspace** (root) — covers all three TypeScript packages (the docs site, the SDK, and the E2E tests) in one grouped PR, commit prefix `deps:` From 6b61cc6463d2f872de060b1a2160f21aef115ff4 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 7 Oct 2026 06:49:06 -0400 Subject: [PATCH 04/15] docs(otel): say how each signal handles a malformed header or scheme-less endpoint The logs exporter discards the whole OTEL_EXPORTER_OTLP_HEADERS value when one entry is malformed, so logs go out unauthenticated while traces and metrics skip only that entry; the docs and comments said every signal skips the entry. Move the scheme-less endpoint behaviour out of the env-var list into its own sentences. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- docs/src/content/docs/configuration.mdx | 2 +- internal/app/wire.go | 3 ++- internal/observability/provider.go | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8e6fae9f..2d5da034 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,7 +67,7 @@ The invariant index — what must stay true. Full narrative and rationale live i 12. **Structured queries: column authz fail-closed (security)** — `POST /v1/query?table={table}`: typed AST validated against schema, permission-enforced, timestamp-bucketed for cache, `DefaultMaxRows` (10,000) cap. Every column reference — projection, aggregation args, `filters`, `group_by`, `order_by`, `time_range` — is authorized inside `query.Build` (the single chokepoint that enumerates them all), so no clause can skip the role's `allow_columns`/`deny_columns` check (#223). A `select_all` read by a *column-restricted* role expands to its allowed columns via `policy.AllowedProjection`, never a bare `SELECT *`; *unrestricted*/admin roles keep `SELECT *` (`policy.RestrictsColumns` decides). Omitting `columns` selects nothing (`ErrEmptyProjection` → `200 []`); `["*"]` is the literal column `*` (schema-gated, not a wildcard); a table-granted role with no readable columns fails closed (`ErrNoReadableColumns` → `403`). Structured and live-stream (`stream.projectIndices`) reads share the one per-column decision `policy.IsColumnAllowed`, so column visibility can't drift. Row visibility has the same one-source guarantee (#319): `Evaluate` resolves a role's row-`filter` once (`resolvePredicates`), and both surfaces consume that single resolution — the query path renders it to SQL (`predicatesToSQL`), the stream evaluates it in memory per subscriber (`ResolvedPermissions.RowVisible`, whose type-aware comparison fails closed on anything it can't prove about the ingested payload — `policy.ColumnSpec`, with `DateTime`/`DateTime64` operands compared as instants through the ingest grammar (`discovery.Column.TimeParser`) and claim constants rendered canonically and digit-exact by the one shared rule `policy.CanonicalScalar` (#457 — which also refuses a float64 at/past 2^53 rather than match a neighboring ID, and whose ok=false — an absent claim, a structured value, no canonical form — makes the predicate match no rows on BOTH surfaces: `1 = 0` in SQL, every row withheld in memory); numeric comparison runs in the column's STORAGE domain (`policy.NumericSpec`, classified by `discovery.NumericStorageOf` — Float width rounding, Decimal scale truncation, integer exactness, both operands narrowed as ClickHouse narrows stored value and bound constant, out-of-range operands refused rather than modeled; the `tests/integration` differential oracle holds in-range verdicts equal to a live ClickHouse's and the never-admit-where-SQL-hides direction for the refused out-of-range ones); an event whose insert later fails into the DLQ is the one residual payload-vs-stored asymmetry, documented in the access-control enforcement caution) — so row visibility can't drift either. Preserve when touching `internal/query` or the structured-query handler. Detail: architecture.md § `query/`. 13. **Named query pipes: fail-closed (security)** — pre-defined SQL templates (Tinybird-style) with param binding + caching — reads only: a pipe whose SQL `IsMutation` classifies as a write bypasses the cache and singleflight, since a cached or coalesced write is a dropped one (#386); `GET/POST /v1/pipes/{name}` sit outside `RequireAdmin`, so per-pipe `allowed_roles` is the *only* execute-path gate, via `policy.RoleAllowed`: exact allowlist membership (no `"*"`), admin always passes, empty/absent role and empty-string entries authorize nobody, and no `allowed_roles` → admin-only. Preserve and exercise via `testutil.RunRoleMatrix` / `StandardRoleMatrix` (see #159). Detail: architecture.md § `pipes/`. 14. **TypeScript SDK** — `@wavehouse/sdk`: typed query builder, real-time SSE over `fetch`, live queries (incrementable/decomposable/poll aggregation), codegen CLI. Exactly one runtime dependency — `eventsource-parser` (SSE framing, itself dependency-free); adding a second needs the same scrutiny the first got. The canonical client (see §SDK Sync). -15. **Observability invariants** — stdout always 100% (sampling is OTLP-push-only); WARN+ERROR always export at 100% (a non-configurable floor — don't expose it); gRPC OTel exporters dial lazily so an unreachable collector never blocks startup; the OTel Prometheus exporter uses a **private** `prometheus.Registry`. The OTLP endpoint/TLS/custom-CA/mTLS/headers are delegated to the OpenTelemetry SDK's standard `OTEL_EXPORTER_OTLP_*` env vars — `InitProvider` passes **no** endpoint/header options. A custom/private CA and mutual TLS apply to all three signals, logs included (the `otlploggrpc` exporter has honoured the env TLS-cert vars since v0.21.0). A malformed `OTEL_EXPORTER_OTLP_HEADERS` is logged and skipped by the SDK (fail-soft), not fatal. Preserve when touching the logger/sampler/provider. Detail: architecture.md § `observability/`. +15. **Observability invariants** — stdout always 100% (sampling is OTLP-push-only); WARN+ERROR always export at 100% (a non-configurable floor — don't expose it); gRPC OTel exporters dial lazily so an unreachable collector never blocks startup; the OTel Prometheus exporter uses a **private** `prometheus.Registry`. The OTLP endpoint/TLS/custom-CA/mTLS/headers are delegated to the OpenTelemetry SDK's standard `OTEL_EXPORTER_OTLP_*` env vars — `InitProvider` passes **no** endpoint/header options. A custom/private CA and mutual TLS apply to all three signals, logs included (the `otlploggrpc` exporter has honoured the env TLS-cert vars since v0.21.0). A malformed `OTEL_EXPORTER_OTLP_HEADERS` entry is never fatal: traces and metrics skip just that entry, while the logs exporter discards the whole variable. Preserve when touching the logger/sampler/provider. Detail: architecture.md § `observability/`. 16. **Bearer-token-only CORS posture (security)** — Bearer JWT on every request, no cookies/sessions; `corsMiddleware` deliberately **never** emits `Access-Control-Allow-Credentials` (not needed, and `*` + credentials is a spec violation browsers reject). `cors.allowed_origins` (settings directory, per tenant: a tenant route is decorated from the list of the tenant it names, everything else from tenant `0`'s — `corsOrigins`) controls who can *read* responses, not cookie scope; CSRF protection is structural. Don't reintroduce cookie auth or `Allow-Credentials` without a design discussion — answers GitHub #29/#30. Code: `internal/api/router.go`. 17. **Non-fatal boot** — schema-discovery failure on boot is non-fatal: `internal/app` records an `api.BootState`, binds `:8080`, serves 503 on `/livez`/`/readyz` with the diagnostic, and retries via `SchemaRegistry.RetryRefresh` (jittered backoff 2s → 60s), per tenant over a nested directory: `/livez` is 503 while no tenant has completed a first discovery, then sticky 200, and a tenant's outage after that is its log line and counter, never a probe failure. Until a tenant's first discovery its table lookups are a 503 with `Retry-After`, not a 404. Bounds supervisor restart loops. 18. **Health endpoints** — liveness `/livez`, readiness `/readyz` (k8s convention; `/readyz` pings every open ClickHouse pool at once and is ready at the first answer, 503 naming each when none answers); `/healthz` is a permanent alias of `/livez`; `/health` + `/ready` are deprecated (removal v0.2.0, CHANGELOG #144). `/v1/health` is the SDK's content-free public ping (no ClickHouse check), a `/v1` route so it survives reverse-proxy probe-path filtering. Point k8s at `/livez`/`/readyz`, SDK/online-checks at `/v1/health`, never the deprecated aliases. diff --git a/docs/src/content/docs/configuration.mdx b/docs/src/content/docs/configuration.mdx index a77cfb7d..c6803704 100644 --- a/docs/src/content/docs/configuration.mdx +++ b/docs/src/content/docs/configuration.mdx @@ -289,7 +289,7 @@ The master switch is `otel.enabled`. When `true`, each signal (traces/metrics/lo **Sampling rates apply only to the OTLP push path.** Stdout always emits 100% of records — operators using a scraping-style pipeline (Promtail/Grafana Alloy → Loki, Vector, Fluent Bit, etc.) set the collection rate at the scraper, not the application. WaveHouse pushes telemetry to an OTel collector; the scraper world owns its own ingest policy. If you want to throttle OTLP volume for cost, lower the rates below. If you want to throttle Loki/Datadog Logs/etc., do it at that pipeline. -**Direct-to-cloud OTLP.** The OTLP destination is configured through the standard [`OTEL_EXPORTER_OTLP_*` environment variables](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) read by the OpenTelemetry SDK, not WaveHouse config: `OTEL_EXPORTER_OTLP_ENDPOINT` (always include a scheme — `https://` selects TLS via system root CAs, `http://` selects plaintext; a scheme-less `host:port` is never plaintext, but what it does varies by host form and signal: traces and metrics dial a hostname over TLS, logs are not exported at all (the logs exporter is left with no destination and each export fails), and an IP literal fails to parse and falls back to the default. With the endpoint unset that default is **TLS** to `localhost:4317`, so a plaintext local collector needs `http://localhost:4317` set explicitly or `OTEL_EXPORTER_OTLP_INSECURE=true`), `OTEL_EXPORTER_OTLP_HEADERS` for per-RPC auth, `OTEL_EXPORTER_OTLP_CERTIFICATE` to trust a custom/private CA, and `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_KEY` for mutual TLS. The custom/private CA and mutual TLS variables apply to all three signals (traces, metrics and logs). With these set, WaveHouse ships telemetry straight to a TLS-protected cloud gateway (Grafana Cloud's OTLP gateway, Honeycomb, etc.) — no sidecar required (a sidecar is still useful for egress queuing, batching, and tail-based sampling; it's just no longer mandatory). A malformed `OTEL_EXPORTER_OTLP_HEADERS` entry is logged and skipped by the OpenTelemetry SDK (fail-soft) rather than failing startup. Datadog has no public direct-to-cloud OTLP endpoint — use the local DDOT Collector path in the [deployment guide](/deployment#observability). See [Deployment → Observability](/deployment#observability) for worked Honeycomb / Grafana Cloud examples. +**Direct-to-cloud OTLP.** The OTLP destination is configured through the standard [`OTEL_EXPORTER_OTLP_*` environment variables](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) read by the OpenTelemetry SDK, not WaveHouse config: `OTEL_EXPORTER_OTLP_ENDPOINT` (always include a scheme — `https://` selects TLS via system root CAs, `http://` selects plaintext), `OTEL_EXPORTER_OTLP_HEADERS` for per-RPC auth, `OTEL_EXPORTER_OTLP_CERTIFICATE` to trust a custom/private CA, and `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_KEY` for mutual TLS. Without a scheme the endpoint is never used for plaintext, and the result depends on its form: a hostname (`collector:4317`) is dialed over TLS by traces and metrics but leaves the logs exporter with no destination, so every log export fails; an IP literal (`10.0.0.5:4317`) fails to parse, and all three signals fall back to the default. That default, which is also what an unset endpoint means, is **TLS** to `localhost:4317`, so a plaintext local collector needs `http://localhost:4317` or `OTEL_EXPORTER_OTLP_INSECURE=true`. The custom/private CA and mutual TLS variables apply to all three signals (traces, metrics and logs). With these set, WaveHouse ships telemetry straight to a TLS-protected cloud gateway (Grafana Cloud's OTLP gateway, Honeycomb, etc.) — no sidecar required (a sidecar is still useful for egress queuing, batching, and tail-based sampling; it's just no longer mandatory). A malformed entry in `OTEL_EXPORTER_OTLP_HEADERS` never fails startup, but the signals treat it differently: traces and metrics log and skip just that entry, while the logs exporter logs it and discards the whole variable, so logs are sent with no headers at all and an authenticating gateway rejects them. Datadog has no public direct-to-cloud OTLP endpoint — use the local DDOT Collector path in the [deployment guide](/deployment#observability). See [Deployment → Observability](/deployment#observability) for worked Honeycomb / Grafana Cloud examples. | YAML Key | Env Var | Default | Description | | -------- | ------- | ------- | ----------- | diff --git a/internal/app/wire.go b/internal/app/wire.go index beae8a8b..c0e86c80 100644 --- a/internal/app/wire.go +++ b/internal/app/wire.go @@ -225,7 +225,8 @@ func (s sharedTables) Invalidate(ctx context.Context, namespaces []cache.Namespa // (Alloy/scrape, no collector) is a first-class mode, and the OTel SDK // MeterProvider is the shared substrate. Endpoint, TLS, and auth headers // come from the standard OTEL_EXPORTER_OTLP_* env vars, read by the SDK. A -// malformed header is logged and skipped by the SDK (fail-soft); +// malformed header is never fatal (logs drop the whole variable, traces and +// metrics just that entry); // InitProvider's own error is likewise non-fatal — logged, stdout-only from // there on. func (a *App) wireObservability(ctx context.Context) { diff --git a/internal/observability/provider.go b/internal/observability/provider.go index 974832e3..6b0bdfbf 100644 --- a/internal/observability/provider.go +++ b/internal/observability/provider.go @@ -45,8 +45,8 @@ var runtimeStartOnce sync.Once // OpenTelemetry SDK reads those from the standard OTEL_EXPORTER_OTLP_* env vars // — endpoint (with `https://` selecting TLS via system root CAs), a custom CA // via _CERTIFICATE, mutual TLS via _CLIENT_CERTIFICATE/_CLIENT_KEY, and auth -// _HEADERS. A malformed header is logged and skipped by the SDK (fail-soft), -// not fatal. +// _HEADERS. A malformed header is never fatal: traces and metrics skip that +// entry, while the logs exporter drops the whole variable. type ProviderConfig struct { TracesEnabled bool TracesSampleRate float64 From 04052c3975bc15148b5833f528067cb990c539ef Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 7 Oct 2026 07:03:10 -0400 Subject: [PATCH 05/15] docs(otel): stop enumerating scheme-less endpoint cases; fix the make dev OTel steps A scheme-less endpoint's behaviour depends on how Go parses the host and on OTEL_EXPORTER_OTLP_INSECURE, so say only that the signals misread it differently and to always write the scheme. OTEL_EXPORTER_OTLP_INSECURE needs the endpoint unset; the three signals share an endpoint, not a connection; make dev leaves OTel off, so give the variables it needs; and date the logs shutdown bound to sdk/log v0.20.0. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- docs/src/content/docs/configuration.mdx | 2 +- docs/src/content/docs/deployment.md | 2 +- docs/src/content/docs/development.md | 4 ++-- internal/observability/provider.go | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb516589..9e56b2e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Each tenant has a message queue of its own** (`internal/mq/{mq,subject,embedded}.go` (+ tests), `internal/ingest/{sweeper,worker}.go` (+ tests), `internal/api/{dlq,ingest}.go` (+ tests), `internal/app/{app,wire}.go` (+ tests), `internal/stream/{subscriber,hub}.go`, `internal/settings/{settings,store,registry}.go` (+ tests), `internal/testutil/{mocks,testutil}.go`, `clients/ts/src/{dlq,types}.ts` (+ tests), `docs/src/content/docs/{deployment,api,architecture,ingest-pipeline,durability,why-wavehouse}.md`, `docs/src/content/docs/sdk/{admin,reference,streaming}.md`, `docs/src/content/docs/{settings-directory,configuration}.mdx`, `AGENTS.md`): story 5b of the multi-tenant epic ([#583](https://github.com/Wave-RF/WaveHouse/issues/583)). The embedded NATS server keeps each tenant's events on a pair of JetStream streams of its own — `INGEST_` (`ingest..>`, `DiscardNew`) at the tenant's own `mq.max_bytes_gb`, and `DLQ_` (`dlq..>`, `DiscardOld`) at a tenth of it — opened when the tenant is first served and kept, at the budget it last had, when its folder is rejected or removed; subjects are unchanged, and nothing outside `internal/mq` names a stream. A tenant at its budget gets `503` while the others keep publishing, and the ingest worker's and the hub bridge's durables are held on every tenant's stream, each with its own ack floor and `MaxAckPending`, so one tenant's backlog holds back neither another's delivery nor its purge; the worker's prefetch and the hub bridge's fetch-ahead are each shared across the tenants' streams. A removed or rejected tenant's stream is still consumed, so its queued rows reach the worker and are parked on its own dead-letter queue. The sweeper purges each tenant's stream at that tenant's own `stream.gap_window_minutes` — a rejected tenant's as its folder last had it (`settings.Registry.Known` now yields each tenant's last adopted store), and all of its history if the folder has been rejected since boot, so its clients resume once the folder is fixed — keeping no acknowledged history for a removed tenant, and every served tenant's `mq.max_bytes_gb` is applied after each reload rather than tenant `0`'s alone (the boot warning about a nested directory with no tenant `0` is gone with it, and so is the tracking of tenant `0`'s last adopted store: a flat directory's ops gate, its one reader left, reads tenant `0` through the registry). One tenant's failed purge holds up no other tenant's, and the sweep logs it at `ERROR` unless every failure in it is a buffer consumer not created yet. A reload that shrinks a budget no longer deletes dead letters: a dead-letter stream holding more than a tenth of the new budget keeps what it holds, and that is logged — the interim guard of [#532](https://github.com/Wave-RF/WaveHouse/issues/532). JetStream's own check of the streams' caps against the disk, which made them fit 75% of the free disk at boot together, is lifted: a budget is a cap and never a reservation, what the budgets add up to against the disk is [#138](https://github.com/Wave-RF/WaveHouse/issues/138)'s, and a flat directory whose budget exceeds three quarters of the free disk now boots where it used to be refused. A queue that cannot be opened refuses a flat boot like any other store and, over a nested directory, costs its tenant alone — its ingest answers `503`, each reload trying again, and a publish at most once every five seconds, so its clients retrying never hold up another tenant's queue. `GET /v1/ops/dlq/stats` reads one tenant's dead-letter queue — the one `?tenant=` names, parsed strictly like the other admin reads, and tenant `0`'s without it, no longer the sum across tenants — answering for a rejected or removed tenant too and `404` for a tenant with no queue; the SDK's `wh.dlq.list()` and `.table()` take a `tenant` option. The streams an earlier build kept for every tenant together (`WAVEHOUSE`, `WAVEHOUSE_DLQ`) overlap every tenant's subjects and are deleted at boot, what they held with them, and a subject with no tenant token no longer reads as tenant `0`'s. -- **OpenTelemetry 1.47: the logger provider moves off `otel/log/global`** (`go.mod`, `go.sum`, `internal/observability/{logger,provider}.go`, `internal/observability/provider_test.go`, `tests/integration/otel_test.go`, `AGENTS.md`, `docs/src/content/docs/{configuration.mdx,deployment.md,architecture.md}`): closes [#732](https://github.com/Wave-RF/WaveHouse/issues/732). `otel/log` 1.47.0 deprecates the `log/global` package in favour of `otel.GetLoggerProvider` and `otel.SetLoggerProvider`, which only exist from `otel` 1.47, so the `otel` modules (`otel`, `log`, `metric`, `trace`, `sdk`, `sdk/log`, `sdk/metric`, the OTLP and Prometheus exporters) are bumped together and every use of `log/global` (the import and its calls, in `internal/observability` and the integration test) moves with them. gRPC stays at 1.83.2, and so do `proto/otlp` (1.11.0) and `grpc-gateway` (2.30.0), whose newer releases require gRPC 1.84. The docs' "logs ignore the TLS env vars" gap was stale: `otlploggrpc` has applied `OTEL_EXPORTER_OTLP_CERTIFICATE` and the client-certificate variables since v0.21.0, already in v0.1.0, so the docs no longer list it, and `TestOTel_TLSPath_AllSignals` now pins it for all three signals. A scheme-less `OTEL_EXPORTER_OTLP_ENDPOINT` is never plaintext, but what it does now varies by host form and signal, so the docs say to always include the scheme. The architecture doc no longer describes an `Endpoint` field that `ProviderConfig` does not have. +- **OpenTelemetry 1.47: the logger provider moves off `otel/log/global`** (`go.mod`, `go.sum`, `internal/observability/{logger,provider}.go`, `internal/observability/provider_test.go`, `tests/integration/otel_test.go`, `AGENTS.md`, `docs/src/content/docs/{configuration.mdx,deployment.md,architecture.md}`): closes [#732](https://github.com/Wave-RF/WaveHouse/issues/732). `otel/log` 1.47.0 deprecates the `log/global` package in favour of `otel.GetLoggerProvider` and `otel.SetLoggerProvider`, which only exist from `otel` 1.47, so the `otel` modules (`otel`, `log`, `metric`, `trace`, `sdk`, `sdk/log`, `sdk/metric`, the OTLP and Prometheus exporters) are bumped together and every use of `log/global` (the import and its calls, in `internal/observability` and the integration test) moves with them. gRPC stays at 1.83.2, and so do `proto/otlp` (1.11.0) and `grpc-gateway` (2.30.0), whose newer releases require gRPC 1.84. The docs' "logs ignore the TLS env vars" gap was stale: `otlploggrpc` has applied `OTEL_EXPORTER_OTLP_CERTIFICATE` and the client-certificate variables since v0.21.0, already in v0.1.0, so the docs no longer list it, and `TestOTel_TLSPath_AllSignals` now pins it for all three signals. What a scheme-less `OTEL_EXPORTER_OTLP_ENDPOINT` does now varies by signal (the logs exporter can be left with no destination), so the docs say to always include the scheme. The architecture doc no longer describes an `Endpoint` field that `ProviderConfig` does not have. - **Dependabot holds the Go modules that need the vulnerable gRPC 1.84, and npm updates honour the release-age window** (`.github/dependabot.yml`, `docs/src/content/docs/development.md`): refs [#643](https://github.com/Wave-RF/WaveHouse/issues/643) and [#441](https://github.com/Wave-RF/WaveHouse/issues/441). `google.golang.org/grpc` 1.84.x is affected by GO-2026-6443 and has no patched 1.84 release, so the `gomod` entry ignores `>= 1.84.0, < 1.85.0`, plus `go.opentelemetry.io/proto/otlp >= 1.11.1` and `grpc-gateway/v2 >= 2.31.0`, which require it. All three holds are dropped together when gRPC 1.85.0 is tagged; #643 tracks that. The `npm` entry gets `cooldown` with `default-days: 7` to match `minimumReleaseAge`, excluding the same `@wave-rf/*` and `@wavehouse/*` packages as `minimumReleaseAgeExclude`. Cooldown covers the bumped package, not the transitive ones Dependabot re-resolves, so a lockfile can still fail pnpm's policy check; that remainder stays open in #441. - **Message-queue subjects lead with the tenant, and the async paths read it off each message** (`internal/mq/{mq,subject,embedded}.go`, `internal/api/{ingest,stream}.go`, `internal/stream/hub.go`, `internal/ingest/{worker,sweeper}.go`, `internal/app/wire.go`, `docs/src/content/docs/{architecture,ingest-pipeline,deployment,api}.md`, `docs/src/content/docs/{settings-directory,access-control}.mdx`, `AGENTS.md`): story 5a of the multi-tenant epic ([#583](https://github.com/Wave-RF/WaveHouse/issues/583)). `mq.Topic` gains a `Tenant`, the leading token of every subject — `ingest..
[.]`, `dlq..
[.]` — placed verbatim, since the tenant-id grammar makes it one token, so one wildcard selects a tenant's traffic (`ingest.acme.>`); a settings directory that holds the four files produces the same subjects with `0` as the token, and nothing else about them changes. The ingest and stream handlers address the request's tenant, which the resolved store carries (`settings.Store.Tenant`, from story 8). The stream hub indexes subscribers by the full topic and evaluates each event under its own tenant's policy, so a subscriber on one tenant's table never receives another tenant's rows for a table of the same name; a gap-fill and the opening schema frame read the connection's tenant. The ingest worker reads each message's tenant off its topic, batches per tenant table, and resolves the dead-letter switch under the row's own tenant — an envelope it cannot read is parked or dropped under the topic's tenant too — and bumps that tenant's cache namespaces (story 8's `invalidate` now receives the message's tenant rather than tenant `0`; the wiring's `sharedTables` still repeats each bump under every tenant the directory holds, since every tenant reads the same ClickHouse table until story 6). `Publish` and gap-fill refuse a topic whose tenant is empty or outside the grammar, so nothing lands on tenant `0` by omission. diff --git a/docs/src/content/docs/configuration.mdx b/docs/src/content/docs/configuration.mdx index c6803704..5c582d5c 100644 --- a/docs/src/content/docs/configuration.mdx +++ b/docs/src/content/docs/configuration.mdx @@ -289,7 +289,7 @@ The master switch is `otel.enabled`. When `true`, each signal (traces/metrics/lo **Sampling rates apply only to the OTLP push path.** Stdout always emits 100% of records — operators using a scraping-style pipeline (Promtail/Grafana Alloy → Loki, Vector, Fluent Bit, etc.) set the collection rate at the scraper, not the application. WaveHouse pushes telemetry to an OTel collector; the scraper world owns its own ingest policy. If you want to throttle OTLP volume for cost, lower the rates below. If you want to throttle Loki/Datadog Logs/etc., do it at that pipeline. -**Direct-to-cloud OTLP.** The OTLP destination is configured through the standard [`OTEL_EXPORTER_OTLP_*` environment variables](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) read by the OpenTelemetry SDK, not WaveHouse config: `OTEL_EXPORTER_OTLP_ENDPOINT` (always include a scheme — `https://` selects TLS via system root CAs, `http://` selects plaintext), `OTEL_EXPORTER_OTLP_HEADERS` for per-RPC auth, `OTEL_EXPORTER_OTLP_CERTIFICATE` to trust a custom/private CA, and `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_KEY` for mutual TLS. Without a scheme the endpoint is never used for plaintext, and the result depends on its form: a hostname (`collector:4317`) is dialed over TLS by traces and metrics but leaves the logs exporter with no destination, so every log export fails; an IP literal (`10.0.0.5:4317`) fails to parse, and all three signals fall back to the default. That default, which is also what an unset endpoint means, is **TLS** to `localhost:4317`, so a plaintext local collector needs `http://localhost:4317` or `OTEL_EXPORTER_OTLP_INSECURE=true`. The custom/private CA and mutual TLS variables apply to all three signals (traces, metrics and logs). With these set, WaveHouse ships telemetry straight to a TLS-protected cloud gateway (Grafana Cloud's OTLP gateway, Honeycomb, etc.) — no sidecar required (a sidecar is still useful for egress queuing, batching, and tail-based sampling; it's just no longer mandatory). A malformed entry in `OTEL_EXPORTER_OTLP_HEADERS` never fails startup, but the signals treat it differently: traces and metrics log and skip just that entry, while the logs exporter logs it and discards the whole variable, so logs are sent with no headers at all and an authenticating gateway rejects them. Datadog has no public direct-to-cloud OTLP endpoint — use the local DDOT Collector path in the [deployment guide](/deployment#observability). See [Deployment → Observability](/deployment#observability) for worked Honeycomb / Grafana Cloud examples. +**Direct-to-cloud OTLP.** The OTLP destination is configured through the standard [`OTEL_EXPORTER_OTLP_*` environment variables](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) read by the OpenTelemetry SDK, not WaveHouse config: `OTEL_EXPORTER_OTLP_ENDPOINT` (always include a scheme — `https://` selects TLS via system root CAs, `http://` selects plaintext), `OTEL_EXPORTER_OTLP_HEADERS` for per-RPC auth, `OTEL_EXPORTER_OTLP_CERTIFICATE` to trust a custom/private CA, and `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_KEY` for mutual TLS. Without a scheme the SDK misreads the endpoint and each signal handles it differently (the logs exporter can be left with no destination while traces and metrics still export), so always write the scheme. An unset endpoint means **TLS** to `localhost:4317`, so a plaintext local collector needs `http://localhost:4317`, or `OTEL_EXPORTER_OTLP_INSECURE=true` with the endpoint left unset. The custom/private CA and mutual TLS variables apply to all three signals (traces, metrics and logs). With these set, WaveHouse ships telemetry straight to a TLS-protected cloud gateway (Grafana Cloud's OTLP gateway, Honeycomb, etc.) — no sidecar required (a sidecar is still useful for egress queuing, batching, and tail-based sampling; it's just no longer mandatory). A malformed entry in `OTEL_EXPORTER_OTLP_HEADERS` never fails startup, but the signals treat it differently: traces and metrics log and skip just that entry, while the logs exporter logs it and discards the whole variable, so logs are sent with no headers at all and an authenticating gateway rejects them. Datadog has no public direct-to-cloud OTLP endpoint — use the local DDOT Collector path in the [deployment guide](/deployment#observability). See [Deployment → Observability](/deployment#observability) for worked Honeycomb / Grafana Cloud examples. | YAML Key | Env Var | Default | Description | | -------- | ------- | ------- | ----------- | diff --git a/docs/src/content/docs/deployment.md b/docs/src/content/docs/deployment.md index 9e36991f..a692d197 100644 --- a/docs/src/content/docs/deployment.md +++ b/docs/src/content/docs/deployment.md @@ -747,7 +747,7 @@ WaveHouse **pushes** to an OTel collector; scraping-style pipelines (Promtail/Gr ### Pattern: Local collector (SigNoz, OTel Collector, Alloy) -A local collector almost always speaks **plaintext** gRPC, but the SDK's unset default endpoint is **TLS** at `localhost:4317` — so enabling OTel alone is not enough. Point it at the collector with an explicit `http://` scheme: `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317` (or set `OTEL_EXPORTER_OTLP_INSECURE=true`). All three signals (traces, metrics, logs) push through the same connection. This is the simplest setup. +A local collector almost always speaks **plaintext** gRPC, but the SDK's unset default endpoint is **TLS** at `localhost:4317` — so enabling OTel alone is not enough. Point it at the collector with an explicit `http://` scheme: `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317` (or leave the endpoint unset and set `OTEL_EXPORTER_OTLP_INSECURE=true`). All three signals (traces, metrics, logs) push to the same endpoint, each over its own connection. This is the simplest setup. ```yaml otel: diff --git a/docs/src/content/docs/development.md b/docs/src/content/docs/development.md index 0a7cd9fe..bfd08605 100644 --- a/docs/src/content/docs/development.md +++ b/docs/src/content/docs/development.md @@ -162,7 +162,7 @@ These are the small targets behind `make dev` — useful directly when you want ### Running with observability -WaveHouse natively exports standard OpenTelemetry (OTLP) data to `127.0.0.1:4317`. Rather than coupling a heavy observability database stack to the dev server, we provide three lightweight, single-container dashboard options. +With `otel.enabled` on, WaveHouse exports standard OpenTelemetry (OTLP) data to the endpoint in `OTEL_EXPORTER_OTLP_ENDPOINT`. Rather than coupling a heavy observability database stack to the dev server, we provide three lightweight, single-container dashboard options. You run these in a separate terminal tab alongside `make dev` or your test suites (`make test-e2e`). @@ -177,7 +177,7 @@ They block the terminal and stream logs; simply press `Ctrl+C` to instantly tear **Typical Workflow:** 1. Open Tab 1: run `make obs-aspire` (UI opens automatically) -2. Open Tab 2: run `make dev` (or `make test-e2e`) +2. Open Tab 2: run `make test-e2e`, which enables OTel and points it at `http://127.0.0.1:4317` for you, or `WH_OTEL_ENABLED=true OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 make dev` (`make dev` leaves OTel off, and the SDK's unset default is TLS, which these local receivers reject) 3. View traces, metrics, and logs flowing into the UI instantly. No accounts or auth tokens required. ### Using the SDK against `make dev` diff --git a/internal/observability/provider.go b/internal/observability/provider.go index 6b0bdfbf..4d9353b6 100644 --- a/internal/observability/provider.go +++ b/internal/observability/provider.go @@ -94,8 +94,8 @@ func InitProvider(ctx context.Context, serviceName string, cfg ProviderConfig) ( // Fan the providers out concurrently AND bound the whole thing by // ctx: with an unreachable collector, traces and metrics honor the - // deadline but the experimental logs SDK's BatchProcessor.Shutdown - // (sdk/log v0.20.0) does not — while an export is mid-flight in gRPC + // deadline but the logs SDK's BatchProcessor.Shutdown did not at + // sdk/log v0.20.0 (#366; the bound stays as a backstop) — while an export is mid-flight in gRPC // backoff it blocks for the exporter's full ~10s timeout, ignoring // ctx. Waiting on all of them (wg.Wait) would drag the whole shutdown // out to that ~10s. Every provider already got the same deadline, so From 330c158095537c5a1899ab2da218ae45c4f1fa0f Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 7 Oct 2026 07:14:12 -0400 Subject: [PATCH 06/15] docs(deps): warn that go get -u bypasses the gRPC hold; trim the scheme-less claim A manual go get -u pulls the held gRPC 1.84 and fails make vulncheck, so say so next to the instructions. Also drop the claim that traces and metrics still export with a scheme-less endpoint, and complete the CHANGELOG file list. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- docs/src/content/docs/configuration.mdx | 2 +- docs/src/content/docs/development.md | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e56b2e6..afae531c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Each tenant has a message queue of its own** (`internal/mq/{mq,subject,embedded}.go` (+ tests), `internal/ingest/{sweeper,worker}.go` (+ tests), `internal/api/{dlq,ingest}.go` (+ tests), `internal/app/{app,wire}.go` (+ tests), `internal/stream/{subscriber,hub}.go`, `internal/settings/{settings,store,registry}.go` (+ tests), `internal/testutil/{mocks,testutil}.go`, `clients/ts/src/{dlq,types}.ts` (+ tests), `docs/src/content/docs/{deployment,api,architecture,ingest-pipeline,durability,why-wavehouse}.md`, `docs/src/content/docs/sdk/{admin,reference,streaming}.md`, `docs/src/content/docs/{settings-directory,configuration}.mdx`, `AGENTS.md`): story 5b of the multi-tenant epic ([#583](https://github.com/Wave-RF/WaveHouse/issues/583)). The embedded NATS server keeps each tenant's events on a pair of JetStream streams of its own — `INGEST_` (`ingest..>`, `DiscardNew`) at the tenant's own `mq.max_bytes_gb`, and `DLQ_` (`dlq..>`, `DiscardOld`) at a tenth of it — opened when the tenant is first served and kept, at the budget it last had, when its folder is rejected or removed; subjects are unchanged, and nothing outside `internal/mq` names a stream. A tenant at its budget gets `503` while the others keep publishing, and the ingest worker's and the hub bridge's durables are held on every tenant's stream, each with its own ack floor and `MaxAckPending`, so one tenant's backlog holds back neither another's delivery nor its purge; the worker's prefetch and the hub bridge's fetch-ahead are each shared across the tenants' streams. A removed or rejected tenant's stream is still consumed, so its queued rows reach the worker and are parked on its own dead-letter queue. The sweeper purges each tenant's stream at that tenant's own `stream.gap_window_minutes` — a rejected tenant's as its folder last had it (`settings.Registry.Known` now yields each tenant's last adopted store), and all of its history if the folder has been rejected since boot, so its clients resume once the folder is fixed — keeping no acknowledged history for a removed tenant, and every served tenant's `mq.max_bytes_gb` is applied after each reload rather than tenant `0`'s alone (the boot warning about a nested directory with no tenant `0` is gone with it, and so is the tracking of tenant `0`'s last adopted store: a flat directory's ops gate, its one reader left, reads tenant `0` through the registry). One tenant's failed purge holds up no other tenant's, and the sweep logs it at `ERROR` unless every failure in it is a buffer consumer not created yet. A reload that shrinks a budget no longer deletes dead letters: a dead-letter stream holding more than a tenth of the new budget keeps what it holds, and that is logged — the interim guard of [#532](https://github.com/Wave-RF/WaveHouse/issues/532). JetStream's own check of the streams' caps against the disk, which made them fit 75% of the free disk at boot together, is lifted: a budget is a cap and never a reservation, what the budgets add up to against the disk is [#138](https://github.com/Wave-RF/WaveHouse/issues/138)'s, and a flat directory whose budget exceeds three quarters of the free disk now boots where it used to be refused. A queue that cannot be opened refuses a flat boot like any other store and, over a nested directory, costs its tenant alone — its ingest answers `503`, each reload trying again, and a publish at most once every five seconds, so its clients retrying never hold up another tenant's queue. `GET /v1/ops/dlq/stats` reads one tenant's dead-letter queue — the one `?tenant=` names, parsed strictly like the other admin reads, and tenant `0`'s without it, no longer the sum across tenants — answering for a rejected or removed tenant too and `404` for a tenant with no queue; the SDK's `wh.dlq.list()` and `.table()` take a `tenant` option. The streams an earlier build kept for every tenant together (`WAVEHOUSE`, `WAVEHOUSE_DLQ`) overlap every tenant's subjects and are deleted at boot, what they held with them, and a subject with no tenant token no longer reads as tenant `0`'s. -- **OpenTelemetry 1.47: the logger provider moves off `otel/log/global`** (`go.mod`, `go.sum`, `internal/observability/{logger,provider}.go`, `internal/observability/provider_test.go`, `tests/integration/otel_test.go`, `AGENTS.md`, `docs/src/content/docs/{configuration.mdx,deployment.md,architecture.md}`): closes [#732](https://github.com/Wave-RF/WaveHouse/issues/732). `otel/log` 1.47.0 deprecates the `log/global` package in favour of `otel.GetLoggerProvider` and `otel.SetLoggerProvider`, which only exist from `otel` 1.47, so the `otel` modules (`otel`, `log`, `metric`, `trace`, `sdk`, `sdk/log`, `sdk/metric`, the OTLP and Prometheus exporters) are bumped together and every use of `log/global` (the import and its calls, in `internal/observability` and the integration test) moves with them. gRPC stays at 1.83.2, and so do `proto/otlp` (1.11.0) and `grpc-gateway` (2.30.0), whose newer releases require gRPC 1.84. The docs' "logs ignore the TLS env vars" gap was stale: `otlploggrpc` has applied `OTEL_EXPORTER_OTLP_CERTIFICATE` and the client-certificate variables since v0.21.0, already in v0.1.0, so the docs no longer list it, and `TestOTel_TLSPath_AllSignals` now pins it for all three signals. What a scheme-less `OTEL_EXPORTER_OTLP_ENDPOINT` does now varies by signal (the logs exporter can be left with no destination), so the docs say to always include the scheme. The architecture doc no longer describes an `Endpoint` field that `ProviderConfig` does not have. +- **OpenTelemetry 1.47: the logger provider moves off `otel/log/global`** (`go.mod`, `go.sum`, `internal/observability/{logger,provider}.go`, `internal/observability/provider_test.go`, `tests/integration/otel_test.go`, `internal/app/wire.go`, `AGENTS.md`, `docs/src/content/docs/{configuration.mdx,deployment.md,architecture.md,development.md}`): closes [#732](https://github.com/Wave-RF/WaveHouse/issues/732). `otel/log` 1.47.0 deprecates the `log/global` package in favour of `otel.GetLoggerProvider` and `otel.SetLoggerProvider`, which only exist from `otel` 1.47, so the `otel` modules (`otel`, `log`, `metric`, `trace`, `sdk`, `sdk/log`, `sdk/metric`, the OTLP and Prometheus exporters) are bumped together and every use of `log/global` (the import and its calls, in `internal/observability` and the integration test) moves with them. gRPC stays at 1.83.2, and so do `proto/otlp` (1.11.0) and `grpc-gateway` (2.30.0), whose newer releases require gRPC 1.84. The docs' "logs ignore the TLS env vars" gap was stale: `otlploggrpc` has applied `OTEL_EXPORTER_OTLP_CERTIFICATE` and the client-certificate variables since v0.21.0, already in v0.1.0, so the docs no longer list it, and `TestOTel_TLSPath_AllSignals` now pins it for all three signals. Each signal misreads a scheme-less `OTEL_EXPORTER_OTLP_ENDPOINT` differently (the logs exporter can be left with no destination), so the docs say to always include the scheme. The architecture doc no longer describes an `Endpoint` field that `ProviderConfig` does not have. - **Dependabot holds the Go modules that need the vulnerable gRPC 1.84, and npm updates honour the release-age window** (`.github/dependabot.yml`, `docs/src/content/docs/development.md`): refs [#643](https://github.com/Wave-RF/WaveHouse/issues/643) and [#441](https://github.com/Wave-RF/WaveHouse/issues/441). `google.golang.org/grpc` 1.84.x is affected by GO-2026-6443 and has no patched 1.84 release, so the `gomod` entry ignores `>= 1.84.0, < 1.85.0`, plus `go.opentelemetry.io/proto/otlp >= 1.11.1` and `grpc-gateway/v2 >= 2.31.0`, which require it. All three holds are dropped together when gRPC 1.85.0 is tagged; #643 tracks that. The `npm` entry gets `cooldown` with `default-days: 7` to match `minimumReleaseAge`, excluding the same `@wave-rf/*` and `@wavehouse/*` packages as `minimumReleaseAgeExclude`. Cooldown covers the bumped package, not the transitive ones Dependabot re-resolves, so a lockfile can still fail pnpm's policy check; that remainder stays open in #441. - **Message-queue subjects lead with the tenant, and the async paths read it off each message** (`internal/mq/{mq,subject,embedded}.go`, `internal/api/{ingest,stream}.go`, `internal/stream/hub.go`, `internal/ingest/{worker,sweeper}.go`, `internal/app/wire.go`, `docs/src/content/docs/{architecture,ingest-pipeline,deployment,api}.md`, `docs/src/content/docs/{settings-directory,access-control}.mdx`, `AGENTS.md`): story 5a of the multi-tenant epic ([#583](https://github.com/Wave-RF/WaveHouse/issues/583)). `mq.Topic` gains a `Tenant`, the leading token of every subject — `ingest..
[.]`, `dlq..
[.]` — placed verbatim, since the tenant-id grammar makes it one token, so one wildcard selects a tenant's traffic (`ingest.acme.>`); a settings directory that holds the four files produces the same subjects with `0` as the token, and nothing else about them changes. The ingest and stream handlers address the request's tenant, which the resolved store carries (`settings.Store.Tenant`, from story 8). The stream hub indexes subscribers by the full topic and evaluates each event under its own tenant's policy, so a subscriber on one tenant's table never receives another tenant's rows for a table of the same name; a gap-fill and the opening schema frame read the connection's tenant. The ingest worker reads each message's tenant off its topic, batches per tenant table, and resolves the dead-letter switch under the row's own tenant — an envelope it cannot read is parked or dropped under the topic's tenant too — and bumps that tenant's cache namespaces (story 8's `invalidate` now receives the message's tenant rather than tenant `0`; the wiring's `sharedTables` still repeats each bump under every tenant the directory holds, since every tenant reads the same ClickHouse table until story 6). `Publish` and gap-fill refuse a topic whose tenant is empty or outside the grammar, so nothing lands on tenant `0` by omission. diff --git a/docs/src/content/docs/configuration.mdx b/docs/src/content/docs/configuration.mdx index 5c582d5c..d1478e03 100644 --- a/docs/src/content/docs/configuration.mdx +++ b/docs/src/content/docs/configuration.mdx @@ -289,7 +289,7 @@ The master switch is `otel.enabled`. When `true`, each signal (traces/metrics/lo **Sampling rates apply only to the OTLP push path.** Stdout always emits 100% of records — operators using a scraping-style pipeline (Promtail/Grafana Alloy → Loki, Vector, Fluent Bit, etc.) set the collection rate at the scraper, not the application. WaveHouse pushes telemetry to an OTel collector; the scraper world owns its own ingest policy. If you want to throttle OTLP volume for cost, lower the rates below. If you want to throttle Loki/Datadog Logs/etc., do it at that pipeline. -**Direct-to-cloud OTLP.** The OTLP destination is configured through the standard [`OTEL_EXPORTER_OTLP_*` environment variables](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) read by the OpenTelemetry SDK, not WaveHouse config: `OTEL_EXPORTER_OTLP_ENDPOINT` (always include a scheme — `https://` selects TLS via system root CAs, `http://` selects plaintext), `OTEL_EXPORTER_OTLP_HEADERS` for per-RPC auth, `OTEL_EXPORTER_OTLP_CERTIFICATE` to trust a custom/private CA, and `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_KEY` for mutual TLS. Without a scheme the SDK misreads the endpoint and each signal handles it differently (the logs exporter can be left with no destination while traces and metrics still export), so always write the scheme. An unset endpoint means **TLS** to `localhost:4317`, so a plaintext local collector needs `http://localhost:4317`, or `OTEL_EXPORTER_OTLP_INSECURE=true` with the endpoint left unset. The custom/private CA and mutual TLS variables apply to all three signals (traces, metrics and logs). With these set, WaveHouse ships telemetry straight to a TLS-protected cloud gateway (Grafana Cloud's OTLP gateway, Honeycomb, etc.) — no sidecar required (a sidecar is still useful for egress queuing, batching, and tail-based sampling; it's just no longer mandatory). A malformed entry in `OTEL_EXPORTER_OTLP_HEADERS` never fails startup, but the signals treat it differently: traces and metrics log and skip just that entry, while the logs exporter logs it and discards the whole variable, so logs are sent with no headers at all and an authenticating gateway rejects them. Datadog has no public direct-to-cloud OTLP endpoint — use the local DDOT Collector path in the [deployment guide](/deployment#observability). See [Deployment → Observability](/deployment#observability) for worked Honeycomb / Grafana Cloud examples. +**Direct-to-cloud OTLP.** The OTLP destination is configured through the standard [`OTEL_EXPORTER_OTLP_*` environment variables](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) read by the OpenTelemetry SDK, not WaveHouse config: `OTEL_EXPORTER_OTLP_ENDPOINT` (always include a scheme — `https://` selects TLS via system root CAs, `http://` selects plaintext), `OTEL_EXPORTER_OTLP_HEADERS` for per-RPC auth, `OTEL_EXPORTER_OTLP_CERTIFICATE` to trust a custom/private CA, and `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` / `OTEL_EXPORTER_OTLP_CLIENT_KEY` for mutual TLS. Without a scheme the SDK misreads the endpoint and each signal handles it differently (the logs exporter, for one, can be left with no destination at all), so always write the scheme. An unset endpoint means **TLS** to `localhost:4317`, so a plaintext local collector needs `http://localhost:4317`, or `OTEL_EXPORTER_OTLP_INSECURE=true` with the endpoint left unset. The custom/private CA and mutual TLS variables apply to all three signals (traces, metrics and logs). With these set, WaveHouse ships telemetry straight to a TLS-protected cloud gateway (Grafana Cloud's OTLP gateway, Honeycomb, etc.) — no sidecar required (a sidecar is still useful for egress queuing, batching, and tail-based sampling; it's just no longer mandatory). A malformed entry in `OTEL_EXPORTER_OTLP_HEADERS` never fails startup, but the signals treat it differently: traces and metrics log and skip just that entry, while the logs exporter logs it and discards the whole variable, so logs are sent with no headers at all and an authenticating gateway rejects them. Datadog has no public direct-to-cloud OTLP endpoint — use the local DDOT Collector path in the [deployment guide](/deployment#observability). See [Deployment → Observability](/deployment#observability) for worked Honeycomb / Grafana Cloud examples. | YAML Key | Env Var | Default | Description | | -------- | ------- | ------- | ----------- | diff --git a/docs/src/content/docs/development.md b/docs/src/content/docs/development.md index bfd08605..58df3138 100644 --- a/docs/src/content/docs/development.md +++ b/docs/src/content/docs/development.md @@ -570,6 +570,8 @@ go get -u ./... # Update all direct deps to latest minor/patch go mod tidy # Remove unused, add missing ``` +`go get -u` ignores Dependabot's holds: until [#643](https://github.com/Wave-RF/WaveHouse/issues/643) closes it pulls `google.golang.org/grpc` 1.84.x, and `make vulncheck` fails on GO-2026-6443. Keep gRPC, `go.opentelemetry.io/proto/otlp` and `grpc-gateway/v2` at their `go.mod` versions (see [Dependabot](#dependabot)). + ### Vulnerability Scanning `govulncheck` analyzes your actual call graph — not just the module graph — so it only reports vulnerabilities in code paths you use. From 542f34127c1581c49c11fa735c4b7f54c299fdad Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 7 Oct 2026 07:25:40 -0400 Subject: [PATCH 07/15] docs(otel): enable OTel in the local-dashboard steps; say how to undo go get -u make dev and the standalone compose file leave OTel off, so setting only the endpoint sends nothing; say to set WH_OTEL_ENABLED too. After go get -u, pinning gRPC back to 1.83.2 steps proto/otlp and grpc-gateway down with it. Put the logs Shutdown behaviour in the test comment in the past, matching provider.go. Co-Authored-By: Claude Opus 5.5 --- docs/src/content/docs/deployment.md | 4 ++-- docs/src/content/docs/development.md | 2 +- internal/observability/provider_test.go | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/src/content/docs/deployment.md b/docs/src/content/docs/deployment.md index a692d197..9b570a89 100644 --- a/docs/src/content/docs/deployment.md +++ b/docs/src/content/docs/deployment.md @@ -813,9 +813,9 @@ make obs-grafana # Full Grafana LGTM stack, auto-login enabled make obs-front ``` -All options automatically listen on standard OTLP ports (`4317` gRPC / `4318` HTTP) as **plaintext** receivers. If you are running WaveHouse directly on your host (e.g. `make dev`), set `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317` to reach them — the SDK's unset default dials `localhost:4317` over **TLS**, which a plaintext receiver rejects. +All options automatically listen on standard OTLP ports (`4317` gRPC / `4318` HTTP) as **plaintext** receivers. If you are running WaveHouse directly on your host, enable OTel and point it at them: `WH_OTEL_ENABLED=true OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 make dev` (`make dev` leaves OTel off, and the SDK's unset default dials `localhost:4317` over **TLS**, which a plaintext receiver rejects). -If you are running a containerized WaveHouse (e.g., via `deployments/compose/standalone.yaml`), you must override its environment to reach the host-bound collector: `OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4317`. +If you are running a containerized WaveHouse (e.g., via `deployments/compose/standalone.yaml`), you must override its environment to enable OTel and reach the host-bound collector: `WH_OTEL_ENABLED=true` and `OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4317`. ### Dashboards diff --git a/docs/src/content/docs/development.md b/docs/src/content/docs/development.md index 58df3138..afe6c362 100644 --- a/docs/src/content/docs/development.md +++ b/docs/src/content/docs/development.md @@ -570,7 +570,7 @@ go get -u ./... # Update all direct deps to latest minor/patch go mod tidy # Remove unused, add missing ``` -`go get -u` ignores Dependabot's holds: until [#643](https://github.com/Wave-RF/WaveHouse/issues/643) closes it pulls `google.golang.org/grpc` 1.84.x, and `make vulncheck` fails on GO-2026-6443. Keep gRPC, `go.opentelemetry.io/proto/otlp` and `grpc-gateway/v2` at their `go.mod` versions (see [Dependabot](#dependabot)). +`go get -u` ignores Dependabot's holds: until [#643](https://github.com/Wave-RF/WaveHouse/issues/643) closes it pulls `google.golang.org/grpc` 1.84.x, and `make vulncheck` fails on GO-2026-6443. After `go get -u ./...`, run `go get google.golang.org/grpc@v1.83.2` (the version `go.mod` pins); Go steps `proto/otlp` and `grpc-gateway/v2` back down with it (see [Dependabot](#dependabot)). ### Vulnerability Scanning diff --git a/internal/observability/provider_test.go b/internal/observability/provider_test.go index 9941fb46..11204d45 100644 --- a/internal/observability/provider_test.go +++ b/internal/observability/provider_test.go @@ -62,10 +62,10 @@ func TestInitProvider_Shutdown(t *testing.T) { // must stay bounded by its context deadline. That needs two things together — // fanning the traces/metrics/logs providers out concurrently (so their flushes // overlap instead of summing) AND returning when the deadline passes even -// though the experimental logs SDK's BatchProcessor.Shutdown ignores ctx and -// blocks for the exporter's full ~10s timeout during gRPC backoff. Drop either -// and the shutdown overruns the budget: serial stacks the flushes, and a plain -// wg.Wait blocks on the logs straggler. +// when the logs SDK's BatchProcessor.Shutdown ignores ctx and blocks for the +// exporter's full ~10s timeout during gRPC backoff, as it did at sdk/log +// v0.20.0. Drop either and the shutdown overruns the budget: serial stacks the +// flushes, and a plain wg.Wait blocks on the logs straggler. func TestInitProvider_ShutdownParallelBounded(t *testing.T) { // Pin a definitely-unreachable collector so every exporter spends the // shutdown budget in gRPC retry/backoff. t.Setenv also enforces From ed21d9195691579860b604cf35404109428ce37f Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 7 Oct 2026 07:34:46 -0400 Subject: [PATCH 08/15] docs(otel): make the local-dashboard steps match the scripts and compose file Only make obs-front publishes 4318; the compose file passes no environment through, so the variables go in its environment block; and host.docker.internal needs extra_hosts on Linux Docker Engine. Co-Authored-By: Claude Opus 5.5 --- docs/src/content/docs/deployment.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/content/docs/deployment.md b/docs/src/content/docs/deployment.md index 9b570a89..9bbb804e 100644 --- a/docs/src/content/docs/deployment.md +++ b/docs/src/content/docs/deployment.md @@ -813,9 +813,9 @@ make obs-grafana # Full Grafana LGTM stack, auto-login enabled make obs-front ``` -All options automatically listen on standard OTLP ports (`4317` gRPC / `4318` HTTP) as **plaintext** receivers. If you are running WaveHouse directly on your host, enable OTel and point it at them: `WH_OTEL_ENABLED=true OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 make dev` (`make dev` leaves OTel off, and the SDK's unset default dials `localhost:4317` over **TLS**, which a plaintext receiver rejects). +All three publish a **plaintext** OTLP gRPC receiver on `4317` (`make obs-front` also publishes OTLP/HTTP on `4318`). If you are running WaveHouse directly on your host, enable OTel and point it at them: `WH_OTEL_ENABLED=true OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 make dev` (`make dev` leaves OTel off, and the SDK's unset default dials `localhost:4317` over **TLS**, which a plaintext receiver rejects). -If you are running a containerized WaveHouse (e.g., via `deployments/compose/standalone.yaml`), you must override its environment to enable OTel and reach the host-bound collector: `WH_OTEL_ENABLED=true` and `OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4317`. +If you are running a containerized WaveHouse (e.g., via `deployments/compose/standalone.yaml`), add `WH_OTEL_ENABLED: "true"` and `OTEL_EXPORTER_OTLP_ENDPOINT: http://host.docker.internal:4317` to the `wavehouse` service's `environment:` block (exporting them in your shell is not enough: the compose file does not pass them through). On Linux Docker Engine also add `extra_hosts: ["host.docker.internal:host-gateway"]` to the service, since only Docker Desktop defines that name. ### Dashboards From 5a49e234040e9a0d5e9e5b35a640d078f836d4e7 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 7 Oct 2026 07:44:25 -0400 Subject: [PATCH 09/15] docs(deps): re-pin gRPC inside the update block, before go mod tidy Re-pinning after go mod tidy leaves stale go.sum lines that make tidy rejects. go get -u also upgrades indirect dependencies. Co-Authored-By: Claude Opus 5.5 --- docs/src/content/docs/development.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/src/content/docs/development.md b/docs/src/content/docs/development.md index afe6c362..529ff1da 100644 --- a/docs/src/content/docs/development.md +++ b/docs/src/content/docs/development.md @@ -566,11 +566,12 @@ All test targets accept `ARGS="..."` for pass-through `go test` flags. Build tar ### Updating Dependencies ```bash -go get -u ./... # Update all direct deps to latest minor/patch -go mod tidy # Remove unused, add missing +go get -u ./... # Update all dependencies (direct and indirect) to latest minor/patch +go get google.golang.org/grpc@v1.83.2 # Re-apply the gRPC hold until #643 closes (see below) +go mod tidy # Remove unused, add missing ``` -`go get -u` ignores Dependabot's holds: until [#643](https://github.com/Wave-RF/WaveHouse/issues/643) closes it pulls `google.golang.org/grpc` 1.84.x, and `make vulncheck` fails on GO-2026-6443. After `go get -u ./...`, run `go get google.golang.org/grpc@v1.83.2` (the version `go.mod` pins); Go steps `proto/otlp` and `grpc-gateway/v2` back down with it (see [Dependabot](#dependabot)). +`go get -u` ignores Dependabot's holds: until [#643](https://github.com/Wave-RF/WaveHouse/issues/643) closes it pulls `google.golang.org/grpc` 1.84.x, and `make vulncheck` fails on GO-2026-6443. The second line re-applies the hold (1.83.2 is the version `go.mod` pins), and Go steps `proto/otlp` and `grpc-gateway/v2` back down with it; run it before `go mod tidy`, or `make tidy` finds stale `go.sum` lines (see [Dependabot](#dependabot)). ### Vulnerability Scanning From 517aa9f077b75dc8727fec4c612e79a19a34d3e0 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 7 Oct 2026 07:56:48 -0400 Subject: [PATCH 10/15] docs(otel): stop the Alloy pattern pushing metrics and logs twice; point at the gRPC re-pin With otel.enabled on for traces, metrics and logs default to on and are pushed over OTLP as well as scraped; give the pattern a worked environment that turns them off. Name the go get re-pin line wherever the gRPC holds' removal is described, so it goes with them. Co-Authored-By: Claude Opus 5.5 --- .github/dependabot.yml | 3 ++- docs/src/content/docs/deployment.md | 12 ++++++++++-- docs/src/content/docs/development.md | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 34515ea4..aa6dccc0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,7 +17,8 @@ updates: # patch exists. Only that range is held, so the fixed 1.85.0 release is # proposed as soon as it ships. Drop this ignore once grpc 1.85.0 is # tagged, or earlier if a 1.84.x release lists GO-2026-6443 as fixed. - # Drop all three holds in this block together then. Tracked in #643, + # Drop all three holds in this block together then, and the gRPC re-pin + # line in development.md's Updating Dependencies. Tracked in #643, # which stays open until they are gone. - dependency-name: "google.golang.org/grpc" versions: [">= 1.84.0, < 1.85.0"] diff --git a/docs/src/content/docs/deployment.md b/docs/src/content/docs/deployment.md index 9bbb804e..3ac7ca00 100644 --- a/docs/src/content/docs/deployment.md +++ b/docs/src/content/docs/deployment.md @@ -788,9 +788,17 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317 # plaintext gRPC; DD_ The Grafana stack typically wants Prometheus-style scraping for metrics, stdout scraping for logs, and OTLP push for traces. Wire it like this: -- **Logs**: Alloy scrapes stdout via the Docker socket / file tail / k8s logs API. No WaveHouse config needed — stdout always emits 100%. +- **Logs**: Alloy scrapes stdout via the Docker socket / file tail / k8s logs API — stdout always emits 100%. If traces also go via OTLP (`otel.enabled: true`), set `otel.logs.enabled: false`, or WaveHouse pushes its logs over OTLP as well. - **Traces**: Set `OTEL_EXPORTER_OTLP_ENDPOINT` to Alloy's `otelcol.receiver.otlp` listener (`http://alloy:4317`). Alloy forwards to Tempo. -- **Metrics**: Set `prometheus.enabled: true`. Alloy's `prometheus.scrape` reads `http://wavehouse:8080/metrics` (or whatever port you configured). The `prometheus` block is independent of `otel.*` — you can leave `otel.enabled: false` if Alloy is only scraping (no OTLP push at all), or combine the two if traces still go via OTLP. +- **Metrics**: Set `prometheus.enabled: true`. Alloy's `prometheus.scrape` reads `http://wavehouse:8080/metrics` (or whatever port you configured). The `prometheus` block is independent of `otel.*` — you can leave `otel.enabled: false` if Alloy is only scraping (no OTLP push at all), or combine the two if traces still go via OTLP, with `otel.metrics.enabled: false` so metrics aren't also pushed. + +```bash +export WH_OTEL_ENABLED=true # traces go via OTLP +export WH_OTEL_METRICS_ENABLED=false # Alloy scrapes /metrics instead +export WH_OTEL_LOGS_ENABLED=false # Alloy scrapes stdout instead +export WH_PROMETHEUS_ENABLED=true +export OTEL_EXPORTER_OTLP_ENDPOINT=http://alloy:4317 +``` For the metrics path specifically: WaveHouse uses the OTel SDK's Prometheus exporter under the hood, which translates OTel metric names to Prometheus conventions automatically (dots and dashes become underscores; counters get a `_total` suffix). Existing OTel instruments don't need renaming. diff --git a/docs/src/content/docs/development.md b/docs/src/content/docs/development.md index 529ff1da..7e025988 100644 --- a/docs/src/content/docs/development.md +++ b/docs/src/content/docs/development.md @@ -587,7 +587,7 @@ For a combined security scan, run `make verify` — it runs `vulncheck` alongsid Dependabot is configured in `.github/dependabot.yml` to open weekly grouped PRs for three update configs: -- **Go modules** (root) — outdated or vulnerable Go dependencies, commit prefix `deps:`. `google.golang.org/grpc` 1.84.x is held back (GO-2026-6443, no 1.84.x fix), along with the releases that require it, `go.opentelemetry.io/proto/otlp` 1.11.1 and later and `grpc-gateway/v2` 2.31.0 and later; drop the three holds together when gRPC 1.85.0 ships ([#643](https://github.com/Wave-RF/WaveHouse/issues/643)) +- **Go modules** (root) — outdated or vulnerable Go dependencies, commit prefix `deps:`. `google.golang.org/grpc` 1.84.x is held back (GO-2026-6443, no 1.84.x fix), along with the releases that require it, `go.opentelemetry.io/proto/otlp` 1.11.1 and later and `grpc-gateway/v2` 2.31.0 and later; drop the three holds, and the `go get google.golang.org/grpc@v1.83.2` line under [Updating Dependencies](#updating-dependencies), together when gRPC 1.85.0 ships ([#643](https://github.com/Wave-RF/WaveHouse/issues/643)) - **GitHub Actions** (root **and** `/.github/actions/setup-env`) — outdated action versions tracked against the SHA pins across `.github/workflows/*` and the `setup-env` composite action, commit prefix `ci:` - **npm — pnpm workspace** (root) — covers all three TypeScript packages (the docs site, the SDK, and the E2E tests) in one grouped PR, commit prefix `deps:` From 436bddec208b025110ec1b78e6de19fa7095c6a5 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 7 Oct 2026 08:06:08 -0400 Subject: [PATCH 11/15] docs(deps): say exactly what go get -u ./... updates It covers the main module's non-test dependencies; test-only deps need -t and the go.mod tool block needs go get -u tool. Co-Authored-By: Claude Opus 5.5 --- docs/src/content/docs/development.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/development.md b/docs/src/content/docs/development.md index 7e025988..bab90b93 100644 --- a/docs/src/content/docs/development.md +++ b/docs/src/content/docs/development.md @@ -566,7 +566,7 @@ All test targets accept `ARGS="..."` for pass-through `go test` flags. Build tar ### Updating Dependencies ```bash -go get -u ./... # Update all dependencies (direct and indirect) to latest minor/patch +go get -u ./... # Update the main module's non-test deps (direct and indirect); add -t for test-only deps, and run go get -u tool for the tool block go get google.golang.org/grpc@v1.83.2 # Re-apply the gRPC hold until #643 closes (see below) go mod tidy # Remove unused, add missing ``` From a4f5d35acd28d30d7217c3a968f021ca05155072 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 7 Oct 2026 08:14:21 -0400 Subject: [PATCH 12/15] docs(deps): re-pin gRPC with a 1.83 prefix query, not an exact patch An exact v1.83.2 would downgrade gRPC once Dependabot takes a later 1.83.x patch; @v1.83 always resolves to the newest 1.83.x. Co-Authored-By: Claude Opus 5.5 --- docs/src/content/docs/development.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/content/docs/development.md b/docs/src/content/docs/development.md index bab90b93..0b9703c1 100644 --- a/docs/src/content/docs/development.md +++ b/docs/src/content/docs/development.md @@ -567,11 +567,11 @@ All test targets accept `ARGS="..."` for pass-through `go test` flags. Build tar ```bash go get -u ./... # Update the main module's non-test deps (direct and indirect); add -t for test-only deps, and run go get -u tool for the tool block -go get google.golang.org/grpc@v1.83.2 # Re-apply the gRPC hold until #643 closes (see below) +go get google.golang.org/grpc@v1.83 # Re-apply the gRPC hold (newest 1.83.x) until #643 closes (see below) go mod tidy # Remove unused, add missing ``` -`go get -u` ignores Dependabot's holds: until [#643](https://github.com/Wave-RF/WaveHouse/issues/643) closes it pulls `google.golang.org/grpc` 1.84.x, and `make vulncheck` fails on GO-2026-6443. The second line re-applies the hold (1.83.2 is the version `go.mod` pins), and Go steps `proto/otlp` and `grpc-gateway/v2` back down with it; run it before `go mod tidy`, or `make tidy` finds stale `go.sum` lines (see [Dependabot](#dependabot)). +`go get -u` ignores Dependabot's holds: until [#643](https://github.com/Wave-RF/WaveHouse/issues/643) closes it pulls `google.golang.org/grpc` 1.84.x, and `make vulncheck` fails on GO-2026-6443. The second line re-applies the hold (`@v1.83` selects the newest 1.83.x patch, the last line without GO-2026-6443), and Go steps `proto/otlp` and `grpc-gateway/v2` back down with it; run it before `go mod tidy`, or `make tidy` finds stale `go.sum` lines (see [Dependabot](#dependabot)). ### Vulnerability Scanning @@ -587,7 +587,7 @@ For a combined security scan, run `make verify` — it runs `vulncheck` alongsid Dependabot is configured in `.github/dependabot.yml` to open weekly grouped PRs for three update configs: -- **Go modules** (root) — outdated or vulnerable Go dependencies, commit prefix `deps:`. `google.golang.org/grpc` 1.84.x is held back (GO-2026-6443, no 1.84.x fix), along with the releases that require it, `go.opentelemetry.io/proto/otlp` 1.11.1 and later and `grpc-gateway/v2` 2.31.0 and later; drop the three holds, and the `go get google.golang.org/grpc@v1.83.2` line under [Updating Dependencies](#updating-dependencies), together when gRPC 1.85.0 ships ([#643](https://github.com/Wave-RF/WaveHouse/issues/643)) +- **Go modules** (root) — outdated or vulnerable Go dependencies, commit prefix `deps:`. `google.golang.org/grpc` 1.84.x is held back (GO-2026-6443, no 1.84.x fix), along with the releases that require it, `go.opentelemetry.io/proto/otlp` 1.11.1 and later and `grpc-gateway/v2` 2.31.0 and later; drop the three holds, and the `go get google.golang.org/grpc@v1.83` line under [Updating Dependencies](#updating-dependencies), together when gRPC 1.85.0 ships ([#643](https://github.com/Wave-RF/WaveHouse/issues/643)) - **GitHub Actions** (root **and** `/.github/actions/setup-env`) — outdated action versions tracked against the SHA pins across `.github/workflows/*` and the `setup-env` composite action, commit prefix `ci:` - **npm — pnpm workspace** (root) — covers all three TypeScript packages (the docs site, the SDK, and the E2E tests) in one grouped PR, commit prefix `deps:` From 78d5d8fb1a6d721ee3681c272b6ccd21351cc5a4 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 7 Oct 2026 08:24:29 -0400 Subject: [PATCH 13/15] docs(deps): 1.83.2 is the fixed 1.83 patch, not the whole line GO-2026-6443 affects 1.83.0 and 1.83.1; only 1.83.2 carries the fix. Co-Authored-By: Claude Opus 5.5 --- docs/src/content/docs/development.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/development.md b/docs/src/content/docs/development.md index 0b9703c1..5d566e0d 100644 --- a/docs/src/content/docs/development.md +++ b/docs/src/content/docs/development.md @@ -571,7 +571,7 @@ go get google.golang.org/grpc@v1.83 # Re-apply the gRPC hold (newest 1.83.x) go mod tidy # Remove unused, add missing ``` -`go get -u` ignores Dependabot's holds: until [#643](https://github.com/Wave-RF/WaveHouse/issues/643) closes it pulls `google.golang.org/grpc` 1.84.x, and `make vulncheck` fails on GO-2026-6443. The second line re-applies the hold (`@v1.83` selects the newest 1.83.x patch, the last line without GO-2026-6443), and Go steps `proto/otlp` and `grpc-gateway/v2` back down with it; run it before `go mod tidy`, or `make tidy` finds stale `go.sum` lines (see [Dependabot](#dependabot)). +`go get -u` ignores Dependabot's holds: until [#643](https://github.com/Wave-RF/WaveHouse/issues/643) closes it pulls `google.golang.org/grpc` 1.84.x, and `make vulncheck` fails on GO-2026-6443. The second line re-applies the hold (`@v1.83` selects the newest 1.83.x patch, 1.83.2, which carries the GO-2026-6443 fix; 1.83.0 and 1.83.1 are affected), and Go steps `proto/otlp` and `grpc-gateway/v2` back down with it ([Dependabot](#dependabot) lists the same holds). Run it before `go mod tidy`, or `make tidy` finds stale `go.sum` lines. ### Vulnerability Scanning From 2c567c3da5b9ea48cf2fdb234a62dc85da649fa6 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 7 Oct 2026 08:33:11 -0400 Subject: [PATCH 14/15] docs(deps): make vulncheck scans at package level, not the call graph make vulncheck runs govulncheck -scan package, which reports any advisory in an imported package without reachability; only V=1 runs the call-graph scan. The gRPC hold text relies on that. Co-Authored-By: Claude Opus 5.5 --- docs/src/content/docs/development.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/src/content/docs/development.md b/docs/src/content/docs/development.md index 5d566e0d..bf4d37bb 100644 --- a/docs/src/content/docs/development.md +++ b/docs/src/content/docs/development.md @@ -575,10 +575,11 @@ go mod tidy # Remove unused, add missing ### Vulnerability Scanning -`govulncheck` analyzes your actual call graph — not just the module graph — so it only reports vulnerabilities in code paths you use. +`make vulncheck` runs `govulncheck -scan package`: it reports every known vulnerability in a package your code imports, without checking whether the vulnerable function is reachable. That is stricter than govulncheck's default call-graph scan, so a hit does not on its own mean the code path is used. ```bash -make vulncheck +make vulncheck # package-level scan (what make verify and CI run) +V=1 make vulncheck # default symbol-level (call-graph) scan, with call stacks ``` For a combined security scan, run `make verify` — it runs `vulncheck` alongside `lint`, and `gosec` is one of the linters enabled in `.golangci.yml`. This is also what CI runs on every push and pull request. From f369cc1afba733c95f01730bdc2ee47e66f2a3f3 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Wed, 7 Oct 2026 08:42:33 -0400 Subject: [PATCH 15/15] docs(otel): keep OTLP logs on in the Alloy pattern; say what turning them off costs stdout Only the OTel-aware logger adds trace_id/span_id, component, source and the build fields to stdout, so turning otel.logs.enabled off breaks Loki-to-Tempo links; the configuration table said it left stdout untouched. Describe make vulncheck's V=1 output accurately. Co-Authored-By: Claude Opus 5.5 --- Makefile | 2 +- docs/src/content/docs/configuration.mdx | 2 +- docs/src/content/docs/deployment.md | 3 +-- docs/src/content/docs/development.md | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index f206924e..db455dec 100644 --- a/Makefile +++ b/Makefile @@ -447,7 +447,7 @@ test-release-channel: $(call run,release-channel test,scripts/ci/release-channel.test.sh,) .PHONY: vulncheck -vulncheck: go-mod-download ## Run govulncheck (V=1 for full call stacks) +vulncheck: go-mod-download ## Run govulncheck -scan package (V=1: symbol-level scan with example traces) ifdef V @echo "$(CYAN)==> Running govulncheck (verbose)...$(RESET)" @$(GOVULNCHECK) ./... diff --git a/docs/src/content/docs/configuration.mdx b/docs/src/content/docs/configuration.mdx index d1478e03..b24073d6 100644 --- a/docs/src/content/docs/configuration.mdx +++ b/docs/src/content/docs/configuration.mdx @@ -297,7 +297,7 @@ The master switch is `otel.enabled`. When `true`, each signal (traces/metrics/lo | `otel.traces.enabled` | `WH_OTEL_TRACES_ENABLED` | `true` | Export traces via OTLP gRPC. | | `otel.traces.sample_rate` | `WH_OTEL_TRACES_SAMPLE_RATE` | `1.0` | Head-based trace sampling rate in `[0.0, 1.0]`. `1.0` exports every trace; `0.0` exports none. Defaults to 100% (matches the OpenTelemetry SDK default); lower it for high-QPS production services where collector or backend cost is a concern. Best practice is "100% at the source, downsample at the collector" via tail-based sampling. Validated at config load. | | `otel.metrics.enabled` | `WH_OTEL_METRICS_ENABLED` | `true` | Export metrics + Go runtime metrics via OTLP gRPC. Periodic reader interval is fixed at 15s. Metrics are pre-aggregated so there is no sampling knob. | -| `otel.logs.enabled` | `WH_OTEL_LOGS_ENABLED` | `true` | Export logs via OTLP gRPC. Disabling this leaves stdout logging untouched — the OTel logger provider is simply not registered. | +| `otel.logs.enabled` | `WH_OTEL_LOGS_ENABLED` | `true` | Export logs via OTLP gRPC. Disabling it leaves stdout with 100% of records, but from the plain boot logger: lines lose `trace_id`/`span_id`, `component`, `source` and the build fields. | | `otel.logs.sample_rate` | `WH_OTEL_LOGS_SAMPLE_RATE` | `1.0` | OTLP export rate for `DEBUG`/`INFO` records, in `[0.0, 1.0]`. Validated at config load. `WARN` and `ERROR` records always export at 100% — dropping them silently during incidents is too dangerous to expose as a knob. **Stdout always receives 100% of records regardless of this rate** (see the scraper note above). | ### Prometheus diff --git a/docs/src/content/docs/deployment.md b/docs/src/content/docs/deployment.md index 3ac7ca00..71837b6d 100644 --- a/docs/src/content/docs/deployment.md +++ b/docs/src/content/docs/deployment.md @@ -788,14 +788,13 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317 # plaintext gRPC; DD_ The Grafana stack typically wants Prometheus-style scraping for metrics, stdout scraping for logs, and OTLP push for traces. Wire it like this: -- **Logs**: Alloy scrapes stdout via the Docker socket / file tail / k8s logs API — stdout always emits 100%. If traces also go via OTLP (`otel.enabled: true`), set `otel.logs.enabled: false`, or WaveHouse pushes its logs over OTLP as well. +- **Logs**: Alloy scrapes stdout via the Docker socket / file tail / k8s logs API — stdout always emits 100%. If traces also go via OTLP (`otel.enabled: true`), keep `otel.logs.enabled` on: it is what puts `trace_id`/`span_id` on the stdout lines, so Loki-to-Tempo links resolve. It also pushes logs over OTLP, so drop that stream in Alloy, or ship logs over OTLP instead of scraping stdout. - **Traces**: Set `OTEL_EXPORTER_OTLP_ENDPOINT` to Alloy's `otelcol.receiver.otlp` listener (`http://alloy:4317`). Alloy forwards to Tempo. - **Metrics**: Set `prometheus.enabled: true`. Alloy's `prometheus.scrape` reads `http://wavehouse:8080/metrics` (or whatever port you configured). The `prometheus` block is independent of `otel.*` — you can leave `otel.enabled: false` if Alloy is only scraping (no OTLP push at all), or combine the two if traces still go via OTLP, with `otel.metrics.enabled: false` so metrics aren't also pushed. ```bash export WH_OTEL_ENABLED=true # traces go via OTLP export WH_OTEL_METRICS_ENABLED=false # Alloy scrapes /metrics instead -export WH_OTEL_LOGS_ENABLED=false # Alloy scrapes stdout instead export WH_PROMETHEUS_ENABLED=true export OTEL_EXPORTER_OTLP_ENDPOINT=http://alloy:4317 ``` diff --git a/docs/src/content/docs/development.md b/docs/src/content/docs/development.md index bf4d37bb..21db97a9 100644 --- a/docs/src/content/docs/development.md +++ b/docs/src/content/docs/development.md @@ -526,7 +526,7 @@ Run `make help` to see all targets. Key ones: | `make fmt` | Check formatting across Go (`gofumpt`) + TS (Biome). Run `make fix` to apply. | | `make tidy` | Verify `go.mod`/`go.sum` are tidy (run `make fix` to apply) | | `make lint` | Run linters across Go (`golangci-lint`) + TS (Biome) + Markdown/MDX (markdownlint) + prose (misspell) | -| `make vulncheck` | Run `govulncheck` (V=1 for full call stacks) | +| `make vulncheck` | Run `govulncheck -scan package` (`V=1`: the default symbol-level scan, with example traces) | | `make verify` | Repo-wide static checks: Go (tidy + fmt + vulncheck + lint) + TS (Biome + `tsc` typecheck) + Markdown/MDX (markdownlint + rule fixtures) + prose (misspell) + shell (shellcheck) + workflows (actionlint) + path-classifier fixtures + release-channel fixtures + docs type-check (`astro check` — not a full build, so link validation stays CI's job) (parallel-safe: `make -j verify`) | | `make fix` | Auto-fixes across Go (`tidy` + `gofumpt` + `goimports` + `lint --fix`), TS (Biome `--write`), Markdown (markdownlint `--fix`), MDX (`fix-mdx-fences` only — the generic fixers never run over `.mdx`), and docs-prose spelling (misspell, both) | | **Build** | | @@ -579,7 +579,7 @@ go mod tidy # Remove unused, add missing ```bash make vulncheck # package-level scan (what make verify and CI run) -V=1 make vulncheck # default symbol-level (call-graph) scan, with call stacks +V=1 make vulncheck # default symbol-level (call-graph) scan, with example call traces ``` For a combined security scan, run `make verify` — it runs `vulncheck` alongside `lint`, and `gosec` is one of the linters enabled in `.golangci.yml`. This is also what CI runs on every push and pull request.