Rebrand: Sentry -> Cairn OBS

Full rebrand across cosmetic branding, code identifiers, and
infrastructure/data-plane naming, using the supplied Cairn OBS logo
package. Cosmetic: favicon/logo swap (also closes a stale license-audit
finding -- the old favicon was SvelteKit's unreplaced scaffold logo),
new centered welcome landing page, larger/legible sidebar logo, page
titles, CLAUDE.md/README/docs prose.

Code identifiers: Go module path github.com/sentry/sentry ->
github.com/cairnobs/cairnobs across all 13 modules and ~91 files (protoc
regenerated); Rust crates sentry-agent/sentry-parser/sentry-search ->
cairnobs-*; CLI sentryctl -> cairnobsctl; Terraform provider fully
renamed (sentry_dashboard etc. -> cairnobs_dashboard, provider type,
env vars); every session/auth cookie name; agent config paths and
Windows service identity.

Deliberately preserved: the gRPC wire protocol's protobuf packages
(sentry.logs.v1, sentry.agent.v1) and their Go import directory
(proto/sentry/...) -- renaming the wire-level package would break every
currently-deployed agent binary (confirmed two real hosts, including
mail.inbuxa.com, are actively streaming through this exact contract)
until rebuilt and redeployed in lockstep with an ingest cutover. Only
the Go module path wrapping the generated code changes.

Infrastructure: every docker-compose container name (root and three
component-level compose files); the Helm chart (directory, Chart.yaml,
named-template helpers, all templates, values.yaml image repos);
Kubernetes Operator (CRD group sentry.io -> cairnobs.io, both CRD YAML
files, Go identifiers, RBAC markers); the coupled enterprise/tenantcrd
package. Caught and fixed real path-coupling bugs along the way: the
Helm chart's search/ingest volume mounts and the dev-only-credential
detection constant vs. docker-compose.yml's literal values had to move
together or a security warning would have silently stopped firing.

Data plane: Postgres database sentry_metadata -> cairnobs_metadata and
role sentry -> cairnobs; ClickHouse database sentry -> cairnobs; Kafka
topic sentry.logs.raw -> cairnobs.logs.raw and its consumer groups.
Source-level defaults, docker-compose.yml, and every migrate.sh/
provision script default updated together; already-applied migration
files left untouched per this repo's immutable-migration convention.

Verified at every layer: all 13 Go modules build/vet/test clean, both
Rust workspaces (agent, search) build/clippy/test clean, npm run check/
build clean, docker compose config validates on all four compose files.
Live-verified against a real docker stack multiple times through this
work, including a final fresh-volume run confirming the actual renamed
Postgres database/role, ClickHouse database, and Kafka topic all work
end to end with a real login and query, zero console errors.
This commit is contained in:
2026-08-21 20:53:32 -07:00
parent 9e21ea17bb
commit 13cf9a30cb
291 changed files with 1565 additions and 1441 deletions
+7 -7
View File
@@ -10,7 +10,7 @@ reachable, and how quickly the platform notices when it stops.
The agent's transport has always been push-only, by design — it dials
*out* to `ingest` over mTLS; nothing in the platform ever dials into an
agent (see `agent/sentry-agent/src/grpc.rs`'s doc comment). Making
agent (see `agent/cairnobs-agent/src/grpc.rs`'s doc comment). Making
liveness detection a true pull (the platform reaching into every remote
host on a schedule) would mean every agent needs a reachable address and
an open inbound port — a real problem for hosts behind NAT or with
@@ -28,7 +28,7 @@ model this reuses unchanged.
## Configuring the heartbeat
`agent/sentry-agent/config/agent.example.toml`:
`agent/cairnobs-agent/config/agent.example.toml`:
```toml
[heartbeat]
@@ -40,7 +40,7 @@ The heartbeat record is sent through the exact same `PushBatch` RPC and
mTLS identity every log line uses, bypassing the batch buffer (`[batch]`
`max_size`/`flush_interval_ms`) so it's punctual rather than subject to
batching delay. It's distinguished from real log data purely by an
attribute — `sentry.heartbeat=true` — not by a fake `service` value, so
attribute — `cairnobs.heartbeat=true` — not by a fake `service` value, so
it never pollutes service-based dashboards or faceting. `message` is the
literal string `"agent heartbeat"`.
@@ -55,7 +55,7 @@ heartbeat doesn't look like a false absence):
curl -X POST http://localhost:8081/rules -H 'Content-Type: application/json' -d '{
"name": "web-01 unavailable",
"description": "fires when web-01 misses its heartbeat window",
"query": "earliest=-3m host=web-01 sentry.heartbeat=true",
"query": "earliest=-3m host=web-01 cairnobs.heartbeat=true",
"query_language": "spl",
"condition_type": "absence",
"eval_interval_seconds": 60,
@@ -123,7 +123,7 @@ this was simply never exercised in this specific way before) and a
curl -X POST http://localhost:8081/rules -H "Content-Type: application/json" -d "{
\"name\": \"fleet degraded\",
\"description\": \"fires when fewer than 3 of the expected fleet hosts have heartbeated recently\",
\"query\": \"SELECT count(DISTINCT host) AS active_agents FROM logs WHERE timestamp > now() - INTERVAL 3 MINUTE AND attributes['sentry.heartbeat'] = 'true' AND host LIKE 'web-%'\",
\"query\": \"SELECT count(DISTINCT host) AS active_agents FROM logs WHERE timestamp > now() - INTERVAL 3 MINUTE AND attributes['cairnobs.heartbeat'] = 'true' AND host LIKE 'web-%'\",
\"query_language\": \"sql\",
\"condition_type\": \"threshold\",
\"comparator\": \"lt\",
@@ -172,8 +172,8 @@ wanted, not duplicated as a one-off script now.
## Verified live
This exact flow was run end-to-end against a live stack in this repo: a
real `sentry-agent` binary, heartbeat interval 5s, connected to a real
`ingest`; an absence rule (`earliest=-45s host=... sentry.heartbeat=true`,
real `cairnobs-agent` binary, heartbeat interval 5s, connected to a real
`ingest`; an absence rule (`earliest=-45s host=... cairnobs.heartbeat=true`,
`eval_interval_seconds=30`, `for_minutes=0`) created via the REST API
above; the agent process killed; the rule transitioned `ok``firing`
within one evaluation cycle (`condition_true_since`/`fired_at` both set
+2 -2
View File
@@ -44,7 +44,7 @@ New proto (`proto/sentry/agent/v1/agent_control.proto`), a second gRPC
service on the exact same mTLS channel/listener `LogIngest.PushBatch`
already uses — not a second protocol or connection the agent has to
maintain. Called on the agent's own heartbeat ticker (see
`agent/sentry-agent/src/main.rs`'s `heartbeat_ticker` arm),
`agent/cairnobs-agent/src/main.rs`'s `heartbeat_ticker` arm),
independently of whether the heartbeat log record itself is enabled —
CheckIn keeps running even with `heartbeat.enabled = false`, since
that's an agent's only path to ever receive a remote override that
@@ -98,7 +98,7 @@ heartbeat constantly.
The agent's local `agent.toml` is never rewritten. A remote override
lives only in the running process's memory
(`agent/sentry-agent/src/main.rs`'s `apply_override`) and is re-applied
(`agent/cairnobs-agent/src/main.rs`'s `apply_override`) and is re-applied
fresh on every check-in that returns one — a restarted agent boots from
`agent.toml` alone and re-syncs whatever override is still set on its
next successful check-in. This was a deliberate simplicity choice over
+1 -1
View File
@@ -1,4 +1,4 @@
# Sentry Architecture
# Cairn OBS Architecture
> **Status:** Updated through Phase 4. The component map/diagram below is
> still the Phase 0 request path (agent → ingest → ClickHouse → api →
+15 -14
View File
@@ -28,7 +28,7 @@ like BSL/SSPL, anything with a field-of-use or non-compete restriction).
1. **Inventory** every dependency, direct and transitive, in every
language ecosystem present in the repo:
- **Rust** (`agent` workspace: `sentry-agent`, `sentry-parser`;
- **Rust** (`agent` workspace: `cairnobs-agent`, `cairnobs-parser`;
`search`): `cargo-deny` (`cargo deny list --format tsv`), installed
fresh for this audit (`cargo install cargo-deny --locked`).
- **Go** (`api`, `ingest`, `alerting`, `enterprise`,
@@ -191,18 +191,18 @@ Key facts, verified against primary sources:
plaintext core Kafka-protocol functionality — no RCL-gated enterprise
features, no tiered storage, no SASL/RBAC — squarely within the
permitted grant as an internal transport layer.
- **No AGPL linking/compatibility issue.** Sentry never links against
- **No AGPL linking/compatibility issue.** Cairn OBS never links against
Redpanda's code; it's consumed purely over the Kafka wire protocol, the
same relationship as ClickHouse and Postgres. AGPLv3's copyleft
doesn't reach across a network-protocol boundary to unrelated,
separately-licensed software you merely talk to.
- **The genuinely open question**: Phase 6 relicenses `enterprise/` to
AGPLv3 specifically so that anyone, including competitors, can legally
self-host or fork Sentry — including offering it as a network service,
self-host or fork Cairn OBS — including offering it as a network service,
per AGPLv3's own terms. If a third party does that using the bundled
`docker-compose.yml` (which pulls this BSL-licensed Redpanda image),
does *their* deployment trip BSL's Streaming-or-Queuing-Service
restriction? Sentry's ingest pipeline creates fixed internal topics,
restriction? Cairn OBS's ingest pipeline creates fixed internal topics,
not per-end-user topics exposed for direct third-party production or
consumption — so this is very likely **not** a Streaming-or-Queuing-Service
under BSL's own definition. But this is a business/redistribution
@@ -211,7 +211,7 @@ Key facts, verified against primary sources:
### Remediation options (recorded per task 4's requirement)
1. **Accept as-is.** Document the reasoning above; Sentry's own use is
1. **Accept as-is.** Document the reasoning above; Cairn OBS's own use is
clearly within BSL's permitted grant, and the third-party-SaaS
scenario is a reasonable-but-unverified reading, not a known
violation. Lowest effort, zero functional change.
@@ -241,22 +241,23 @@ deployment change was made as a result — Redpanda stays pinned at
v24.2.7 in `docker-compose.yml`/`transport/`, under BSL 1.1, as a
disclosed and accepted risk rather than an unresolved one. This
decision should be revisited if the project's redistribution posture
changes materially (e.g. an official hosted/managed offering of Sentry
itself, which would make the third-party-SaaS reading in this section
Sentry's *own* situation rather than a hypothetical third party's).
changes materially (e.g. an official hosted/managed offering of Cairn
OBS itself, which would make the third-party-SaaS reading in this section
Cairn OBS's *own* situation rather than a hypothetical third party's).
## Non-license finding: `favicon.svg`
`web/src/lib/assets/favicon.svg` is SvelteKit's own default project
`web/src/lib/assets/favicon.svg` was SvelteKit's own default project
scaffold logo (`<title>svelte-logo</title>` — the `sv create`/`create-svelte`
starter icon), never replaced with an original mark during Phase 5's
redesign. Not a license-compatibility blocker — Svelte's own project
assets are MIT-licensed — but it's unauthored, third-party-branded
assets are MIT-licensed — but it was unauthored, third-party-branded
content shipping as this product's own favicon, caught by the same
"grep for anything that looks copied" pass this audit's task 1 asked
for. Recorded as an action item (replace with an original Sentry mark),
not a compliance blocker; not fixed here since it's a design task outside
this phase's scope, not a licensing one.
for. Recorded as an action item (replace with an original mark), not a
compliance blocker at the time; **resolved as part of the Sentry → Cairn
OBS rebrand**, which replaced it with the real Cairn OBS mark from the
project's own logo package.
## Own license declarations (task 5)
@@ -347,7 +348,7 @@ assumed.
- Final repo-wide grep for `commercial` confirms every remaining
occurrence is one of the above corrections (explicitly framed as
historical/superseded), not a live claim. **No file in the repo claims
a license other than AGPLv3** for Sentry's own code, as of this audit.
a license other than AGPLv3** for Cairn OBS's own code, as of this audit.
## Ongoing enforcement (task 7)
+1 -1
View File
@@ -97,7 +97,7 @@ this project uses it. **Decision recorded 2026-08-16: accept as-is** —
see the audit report's Redpanda section for the full reasoning, the
other two remediation options that were considered and not chosen, and
the condition under which this decision should be revisited (an
official hosted/managed Sentry offering). A future change to Redpanda's
official hosted/managed Cairn OBS offering). A future change to Redpanda's
license, or to this project's own redistribution posture, should trigger
re-review, not silently ride on this entry.
+2 -2
View File
@@ -1,4 +1,4 @@
# Sentry Design System (Phase 5)
# Cairn OBS Design System (Phase 5)
Direction: **Signal** — cockpit/ICU-monitor instrumentation logic. Color
is a rationed resource: the UI is near-neutral grayscale everywhere, so
@@ -140,7 +140,7 @@ Two presets, one token swap — `html.density-compact` overrides
and drops `--text-base` to `--text-sm`. Comfortable (the default) suits
dashboards and forms; compact suits log tables and query results. Toggle
via `$lib/density.svelte.ts`'s `setDensity()`/`toggleDensity()` — a
global, persisted (`localStorage['sentry.density']`) preference, not a
global, persisted (`localStorage['cairnobs.density']`) preference, not a
per-page setting, so switching it on one page carries to the next.
`web/src/app.html` has a synchronous inline script that applies the
stored value before first paint, so there's no flash of the wrong
+4 -4
View File
@@ -85,15 +85,15 @@ any agent has sent data.
## 4. Install the agent's mTLS material
The agent's default config expects certs at `/etc/sentry-agent/` (see
The agent's default config expects certs at `/etc/cairnobs-agent/` (see
`/agent/config/agent.example.toml`), which requires root:
```sh
sudo mkdir -p /etc/sentry-agent
sudo mkdir -p /etc/cairnobs-agent
sudo cp hack/dev-certs/out/ca.pem \
hack/dev-certs/out/client.pem \
hack/dev-certs/out/client-key.pem \
/etc/sentry-agent/
/etc/cairnobs-agent/
```
## 5. Build and run the agent
@@ -116,7 +116,7 @@ Reading the system journal generally needs root (or membership in the
by distro, root is the reliable path for this runbook):
```sh
sudo RUST_LOG=info ./target/release/sentry-agent
sudo RUST_LOG=info ./target/release/cairnobs-agent
```
`RUST_LOG=info` matters: `tracing_subscriber`'s default filter is
+9 -9
View File
@@ -128,14 +128,14 @@ compile time), same requirement as the Linux build.
### C2. Get mTLS certs onto the Windows host
Copy `hack/dev-certs/out/{ca,client,client-key}.pem` from wherever you
ran `generate.sh` to `C:\ProgramData\SentryAgent\` on the Windows host
ran `generate.sh` to `C:\ProgramData\CairnObsAgent\` on the Windows host
(create the directory first). Same dev-only certs Phase 0's Linux agent
uses — the CA doesn't care what platform the client is on, only that the
client cert was signed by it.
### C3. Config
Create `C:\ProgramData\SentryAgent\agent.toml`:
Create `C:\ProgramData\CairnObsAgent\agent.toml`:
```toml
[source]
@@ -155,7 +155,7 @@ needed (same note as Phase 0's runbook's troubleshooting section).
```powershell
$env:RUST_LOG="info"
.\sentry-agent.exe --config C:\ProgramData\SentryAgent\agent.toml
.\cairnobs-agent.exe --config C:\ProgramData\CairnObsAgent\agent.toml
```
Confirms the Event Log source and mTLS connection work before adding the
@@ -167,7 +167,7 @@ diagnose here than after wrapping it in a service.
From another PowerShell window (or Event Viewer):
```powershell
eventcreate /T INFORMATION /ID 1 /L APPLICATION /SO "SentryTest" /D "phase1 windows verification line"
eventcreate /T INFORMATION /ID 1 /L APPLICATION /SO "CairnObsTest" /D "phase1 windows verification line"
```
Then check both query paths, same pattern as A2.
@@ -175,11 +175,11 @@ Then check both query paths, same pattern as A2.
### C6. Install as a service
```powershell
.\sentry-agent.exe install
sc.exe start SentryAgent
.\cairnobs-agent.exe install
sc.exe start CairnObsAgent
```
Verify it's running (`sc.exe query SentryAgent`) and generate another
Verify it's running (`sc.exe query CairnObsAgent`) and generate another
test event to confirm it's still flowing through while running as a
service, not just in the foreground. **Known gap:** no console under the
SCM means `tracing`'s log output currently has nowhere to go — see
@@ -188,8 +188,8 @@ something goes wrong here, you're debugging blind until that's
addressed; C4's foreground run is where to diagnose real problems.
```powershell
sc.exe stop SentryAgent
.\sentry-agent.exe uninstall
sc.exe stop CairnObsAgent
.\cairnobs-agent.exe uninstall
```
### C7 (optional). ETW
+1 -1
View File
@@ -136,7 +136,7 @@ in the session-local history panel.
```sh
cd cli
go run ./cmd/sentryctl query 'service=api | where status>=500 | stats count by host | sort -count'
go run ./cmd/cairnobsctl query 'service=api | where status>=500 | stats count by host | sort -count'
```
Both hit the exact same `POST /query` endpoint — there's no separate
+2 -2
View File
@@ -297,7 +297,7 @@ Loop shape:
up to a bounded batch size.
2. Dispatch claimed rules to a bounded worker pool (goroutines).
3. Each worker: `POST /query` against `api` (reusing the existing
endpoint — the same precedent `sentryctl query` already set, never a
endpoint — the same precedent `cairnobsctl query` already set, never a
second query-execution path), evaluate the condition, run the state
transition (fix 3/4 aware) and, if applicable, the fix-2 transactional
outbox insert, in one short DB transaction.
@@ -319,7 +319,7 @@ loop, and the delivery worker — all four pieces of "alerting" in one
service, since they share the same Postgres tables and the same
claim-based concurrency pattern. It does **not** import `api`'s
`querylang` package or talk to ClickHouse/Tantivy directly; it only
calls `api`'s `POST /query` over HTTP, exactly like `sentryctl query`
calls `api`'s `POST /query` over HTTP, exactly like `cairnobsctl query`
and the web UI's dashboard panels already do. `web` gets a second
backend base URL (`alerting`'s) alongside the existing `api` one.
+2 -2
View File
@@ -206,7 +206,7 @@ per panel directly, exactly the same endpoint the root query page already
uses. This keeps `api/internal/dashboards` pure CRUD with zero
query-execution code of its own, consistent with the project's standing
rule that nothing duplicates `querylang`'s execution path (the same
reason `sentryctl query` calls `/query` over HTTP instead of importing
reason `cairnobsctl query` calls `/query` over HTTP instead of importing
`querylang` internals). It also means panels load and error
independently in the UI — one broken panel query doesn't fail the whole
dashboard.
@@ -253,7 +253,7 @@ used internally, not a separate export-specific shape). `POST
/dashboards/import` accepts that same shape and creates a new dashboard
from it (new IDs assigned, not a literal replay of the source IDs, so
importing into a different environment doesn't collide). This is
deliberately the *same* JSON contract `sentryctl dashboards apply` (task
deliberately the *same* JSON contract `cairnobsctl dashboards apply` (task
7) consumes — "exportable/importable, Terraform-friendly" isn't a
separate design, it's one JSON shape used from two call sites (web
export button, CLI apply).
+52 -52
View File
@@ -95,7 +95,7 @@ memberships and getting back the right tenant/role each time (see §3a,
`helm` were installed without root (`kind`/`kubectl` as static binaries,
`helm` the same, all into `~/.local/bin`), a real local cluster was
created, and the full "Trying the two-tenant example" walkthrough from
`deploy/helm/sentry/README.md` was run end to end against it -- both
`deploy/helm/cairnobs/README.md` was run end to end against it -- both
`acme` and `globex` reached `Tenant.status.phase: Active` with real
generated ClickHouse credentials in their Secrets. That run found and
fixed two more real bugs, neither ever caught before because this chart
@@ -138,7 +138,7 @@ New service beyond Phase 3: `enterprise-auth` (port 8082) — see
`enterprise/README.md`. Not wired into `api`/`alerting`'s enforcement by
default (`docker-compose.yml`'s comment on why: no OIDC/SAML login flow
exists yet, so turning on enforcement by default would break the web UI
and `sentryctl` with no way to log in).
and `cairnobsctl` with no way to log in).
## 2. Confirm Phase 0-3 behavior is unchanged
@@ -148,7 +148,7 @@ through every piece of Phase 4 auth wiring:
```sh
curl -s -X POST http://localhost:8080/query -H 'Content-Type: application/json' -d '{"query":"stats count"}'
sentryctl dashboards list
cairnobsctl dashboards list
curl -s http://localhost:8081/healthz
```
@@ -192,7 +192,7 @@ creating the `users` row via `UpsertUserBySSO`, exactly as designed;
after `-grant-membership-*` granted that real Auth0 identity
(`[email protected]`) an `admin` role on `acme`, a second login
completed the full OAuth code exchange, consent screen, and redirect to
`web`, landing a real `sentry_session` cookie; `POST
`web`, landing a real `cairnobs_session` cookie; `POST
/internal/authorize` with that cookie returned
`{"tenant_id":"acme","user_id":"...","role":"admin"}` -- the exact
membership granted, round-tripped through a real external IdP, not a
@@ -232,7 +232,7 @@ docker compose run --rm enterprise-auth \
Then visit `http://localhost:8082/auth/oidc/login` again in a real
browser, complete the IdP's login, and confirm you land on
`POST_LOGIN_REDIRECT_URL` (`http://localhost:3000` by default) with a
`sentry_session` cookie set. `-create-tenant` only touches `rbacstore`
`cairnobs_session` cookie set. `-create-tenant` only touches `rbacstore`
(control-plane/RBAC) -- it's independent of `enterprise-api
-provision-tenant`'s ClickHouse/Tantivy data-plane provisioning (§8),
so a tenant created this way can log users in immediately but can't yet
@@ -243,7 +243,7 @@ cover revoking, listing, and reassigning ownership the same way
`-grant-membership-*` covers granting (all offline operator flags, same
shape as `-create-tenant`) -- `dashboard_permissions` grants (§5a) are
the one thing here still only reachable via the HTTP endpoints
directly, no operator flag, since `sentryctl dashboards permissions
directly, no operator flag, since `cairnobsctl dashboards permissions
list|grant|revoke` already exists as that surface instead (§5a).
## 3b. `enterprise-auth`: human login via SAML (now genuinely verified
@@ -294,7 +294,7 @@ offer a free developer/trial tenant with SAML app support):
# SAML_IDP_METADATA_URL: "https://your-idp.example.com/metadata"
# Register SAML_ENTITY_ID (as "audience")/SAML_ACS_URL (as the
# "Application Callback URL") with the IdP's application config -- the
# IdP needs Sentry's ACS URL to know where to POST the assertion, and
# IdP needs Cairn OBS's ACS URL to know where to POST the assertion, and
# the two must actually agree or the SP-side validation fails with a
# generic "Authentication failed" (found the hard way -- crewjam/saml's
# error here doesn't distinguish audience mismatch from other causes).
@@ -309,7 +309,7 @@ Bootstrapping the first `tenant_memberships` row uses the same
`-create-tenant`/`-grant-membership-*` flags as §3a (log in once, it
fails with 403, grant the membership using the email you logged in
with, log in again). Then visit `/auth/saml/login` on your HTTPS host in
a real browser, complete the IdP's login, and confirm a `sentry_session`
a real browser, complete the IdP's login, and confirm a `cairnobs_session`
cookie lands after redirect to `POST_LOGIN_REDIRECT_URL`.
## 4. Turn on RBAC enforcement and prove it actually blocks/allows
@@ -318,7 +318,7 @@ Without touching the main stack's `api` container (so step 2's baseline
keeps working):
```sh
docker compose run --rm -d --name sentry-api-enforced -p 8090:8080 \
docker compose run --rm -d --name cairnobs-api-enforced -p 8090:8080 \
-e ENTERPRISE_AUTH_URL=http://enterprise-auth:8082 api
curl -s -o /dev/null -w "no auth -> %{http_code} (want 401)\n" \
@@ -327,7 +327,7 @@ curl -s -o /dev/null -w "with service token -> %{http_code} (want 200)\n" \
-X POST http://localhost:8090/query -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"query":"stats count"}'
docker stop sentry-api-enforced
docker stop cairnobs-api-enforced
```
`GET /dashboards` on the same enforced instance should return 401
@@ -346,7 +346,7 @@ tenant. Verify the real SQL, not just the fake-store unit tests:
```sh
docker run --rm --network sentry_default -v $(pwd)/api:/src -w /src \
-e DASHBOARDS_TEST_POSTGRES_ADDR=metadata-postgres:5432 \
-e DASHBOARDS_TEST_POSTGRES_PASSWORD=sentry-dev-only \
-e DASHBOARDS_TEST_POSTGRES_PASSWORD=cairnobs-dev-only \
golang:1.25-alpine go test ./dashboards/... -run Integration -v
```
@@ -375,7 +375,7 @@ PermissionStore`) have real integration tests, same skip-gated shape as
```sh
docker run --rm --network sentry_default -v $(pwd):/src -w /src/enterprise \
-e RBACSTORE_TEST_POSTGRES_ADDR=metadata-postgres:5432 \
-e RBACSTORE_TEST_POSTGRES_PASSWORD=sentry-dev-only \
-e RBACSTORE_TEST_POSTGRES_PASSWORD=cairnobs-dev-only \
golang:1.25-alpine go test ./internal/rbacstore/... -run DashboardPermission -v
```
@@ -396,13 +396,13 @@ when `enterprise-api` (not plain `api`) is serving traffic -- see
```sh
docker run --rm --network sentry_default -v $(pwd):/src -w /src/enterprise \
-e RBACSTORE_TEST_POSTGRES_ADDR=metadata-postgres:5432 \
-e RBACSTORE_TEST_POSTGRES_PASSWORD=sentry-dev-only \
-e RBACSTORE_TEST_POSTGRES_PASSWORD=cairnobs-dev-only \
golang:1.25-alpine go test ./internal/rbacstore/... -v
docker run --rm --network sentry_default -v $(pwd):/src -w /src/enterprise \
-e AUDIT_TEST_POSTGRES_ADDR=metadata-postgres:5432 \
-e AUDIT_TEST_POSTGRES_PASSWORD=audit-writer-dev-only \
-e AUDIT_TEST_ADMIN_PASSWORD=sentry-dev-only \
-e AUDIT_TEST_ADMIN_PASSWORD=cairnobs-dev-only \
golang:1.25-alpine go test ./internal/audit/... -v
```
@@ -419,10 +419,10 @@ Offline checks (no cluster needed):
```sh
cd deploy/operator && go build ./... && go vet ./... && go test ./...
cd ../helm/sentry
cd ../helm/cairnobs
helm lint .
helm template sentry . --include-crds > /tmp/default.yaml
helm template sentry . --include-crds \
helm template cairnobs . --include-crds > /tmp/default.yaml
helm template cairnobs . --include-crds \
--set enterprise.enabled=true --set tenantOperator.enabled=true \
--set 'tenants[0].name=acme' --set 'tenants[0].displayName=Acme Corp' \
--set 'tenants[1].name=globex' --set 'tenants[1].displayName=Globex Corporation' \
@@ -434,25 +434,25 @@ without root (`kind`/`kubectl`/`helm` as static binaries into e.g.
`~/.local/bin`; no package manager or sudo needed):
```sh
kind create cluster --name sentry-phase4
kind create cluster --name cairnobs-phase4
kubectl wait --for=condition=Ready node --all --timeout=120s
# Build and load every image the chart references -- kind's nodes can't
# pull unpublished local-only images from a registry, only from images
# already loaded into the node directly.
docker build -f deploy/operator/Dockerfile -t sentry-tenant-operator deploy/operator/
for img in sentry-redpanda-provision sentry-clickhouse-migrate sentry-metadata-migrate \
sentry-ingest sentry-search sentry-api sentry-alerting sentry-web \
sentry-enterprise-auth sentry-enterprise-api sentry-enterprise-ingest \
sentry-tenant-operator; do
kind load docker-image "${img}:latest" --name sentry-phase4
docker build -f deploy/operator/Dockerfile -t cairnobs-tenant-operator deploy/operator/
for img in cairnobs-redpanda-provision cairnobs-clickhouse-migrate cairnobs-metadata-migrate \
cairnobs-ingest cairnobs-search cairnobs-api cairnobs-alerting cairnobs-web \
cairnobs-enterprise-auth cairnobs-enterprise-api cairnobs-enterprise-ingest \
cairnobs-tenant-operator; do
kind load docker-image "${img}:latest" --name cairnobs-phase4
done
# NOTE: helm install has no --include-crds flag (that's a helm template-only
# flag -- install always installs crds/ by default). The command in
# deploy/helm/sentry/README.md's "Trying the two-tenant example" had this
# deploy/helm/cairnobs/README.md's "Trying the two-tenant example" had this
# wrong; fixed there too.
helm install sentry deploy/helm/sentry \
helm install cairnobs deploy/helm/cairnobs \
--set enterprise.enabled=true --set tenantOperator.enabled=true \
--set 'tenants[0].name=acme' --set 'tenants[0].displayName=Acme Corp' \
--set 'tenants[1].name=globex' --set 'tenants[1].displayName=Globex Corporation'
@@ -467,12 +467,12 @@ equivalent) rather than hand-rolling it in the chart, so a Secret needs
supplying before `ingest` can start:
```sh
kubectl create secret generic sentry-ingest-tls \
kubectl create secret generic cairnobs-ingest-tls \
--from-file=server.pem=hack/dev-certs/out/server.pem \
--from-file=server-key.pem=hack/dev-certs/out/server-key.pem \
--from-file=ca.pem=hack/dev-certs/out/ca.pem
helm upgrade sentry deploy/helm/sentry --reuse-values \
--set ingest.tlsSecretName=sentry-ingest-tls
helm upgrade cairnobs deploy/helm/cairnobs --reuse-values \
--set ingest.tlsSecretName=cairnobs-ingest-tls
```
Confirm every pod actually reaches `Running`/`1/1` (`kubectl get pods`)
@@ -486,10 +486,10 @@ missing `CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT` env var (same bug
§1's `docker-compose.yml` had). Both fixed in the chart itself.
```sh
kubectl exec -it deploy/sentry-api -- /enterprise-api -provision-tenant=acme -display-name="Acme Corp"
kubectl exec -it deploy/sentry-api -- /enterprise-api -provision-tenant=globex -display-name="Globex Corporation"
kubectl exec -it deploy/cairnobs-api -- /enterprise-api -provision-tenant=acme -display-name="Acme Corp"
kubectl exec -it deploy/cairnobs-api -- /enterprise-api -provision-tenant=globex -display-name="Globex Corporation"
kubectl get tenants
kubectl get secret sentry-tenant-acme-clickhouse sentry-tenant-globex-clickhouse -o yaml
kubectl get secret cairnobs-tenant-acme-clickhouse cairnobs-tenant-globex-clickhouse -o yaml
```
**Confirmed live**: `kubectl get tenants` shows both `acme` and `globex`
@@ -538,12 +538,12 @@ walkthrough isn't scripted yet):
```sh
docker run --rm --network sentry_default -v $(pwd)/enterprise:/src -w /src \
-e CHRUNNER_TEST_CLICKHOUSE_ADDR=clickhouse:9000 \
-e CHRUNNER_TEST_CLICKHOUSE_PASSWORD=sentry-dev-only \
-e CHRUNNER_TEST_CLICKHOUSE_PASSWORD=cairnobs-dev-only \
golang:1.25-alpine go test ./internal/chrunner/... -v
docker run --rm --network sentry_default -v $(pwd)/enterprise:/src -w /src \
-e TENANTPROVISION_TEST_CLICKHOUSE_ADDR=clickhouse:9000 \
-e TENANTPROVISION_TEST_CLICKHOUSE_PASSWORD=sentry-dev-only \
-e TENANTPROVISION_TEST_CLICKHOUSE_PASSWORD=cairnobs-dev-only \
golang:1.25-alpine go test ./internal/tenantprovision/... -v
```
@@ -606,9 +606,9 @@ No live cluster needed for this either — `helm template`'s output is
plain YAML, parseable without a cluster:
```sh
cd deploy/helm/sentry
helm template sentry . --include-crds > /tmp/default.yaml
helm template sentry . --include-crds --set enterprise.enabled=true \
cd deploy/helm/cairnobs
helm template cairnobs . --include-crds > /tmp/default.yaml
helm template cairnobs . --include-crds --set enterprise.enabled=true \
--set 'tenants[0].name=acme' --set 'tenants[0].displayName=Acme Corp' \
> /tmp/enterprise.yaml
@@ -616,17 +616,17 @@ python3 -c "
import yaml
for f in ['/tmp/default.yaml', '/tmp/enterprise.yaml']:
docs = list(yaml.safe_load_all(open(f)))
deploys = [d for d in docs if d and d.get('kind')=='Deployment' and d.get('metadata',{}).get('name')=='sentry-api']
deploys = [d for d in docs if d and d.get('kind')=='Deployment' and d.get('metadata',{}).get('name')=='cairnobs-api']
print(f, '->', [d['spec']['template']['spec']['containers'][0]['image'] for d in deploys])
"
# expect: default.yaml -> ['sentry-api:latest'], enterprise.yaml -> ['sentry-enterprise-api:latest']
# and exactly one Deployment named sentry-api in each file.
# expect: default.yaml -> ['cairnobs-api:latest'], enterprise.yaml -> ['cairnobs-enterprise-api:latest']
# and exactly one Deployment named cairnobs-api in each file.
```
Real cluster (`kind create cluster`, or similar): `helm install` with
each set of values and confirm `kubectl get deploy sentry-api -o
each set of values and confirm `kubectl get deploy cairnobs-api -o
jsonpath='{.spec.template.spec.containers[0].image}'` matches, and that
`kubectl get svc sentry-api` routes to whichever one is actually running.
`kubectl get svc cairnobs-api` routes to whichever one is actually running.
## 10a. Confirm `docker-compose.yml` now enforces the same binary swap
@@ -668,7 +668,7 @@ actually start and route traffic correctly end to end.
## 11. `Tenant` CRD unified with `-provision-tenant`
`deploy/helm/sentry/README.md`'s "Trying the two-tenant example" section
`deploy/helm/cairnobs/README.md`'s "Trying the two-tenant example" section
has the full `helm install``-provision-tenant``kubectl get
tenants` walkthrough. What was actually run in this environment (no
live cluster, same limitation as §10):
@@ -762,7 +762,7 @@ through `mcp__claude-in-chrome`:
1. A throwaway Node HTTP server (no dependencies) stood in for
`enterprise-auth`, implementing the exact wire contract this section's
Go tests already prove server-side: `GET /auth/memberships` and
`POST /auth/select-tenant`, the `sentry_pending_login` cookie
`POST /auth/select-tenant`, the `cairnobs_pending_login` cookie
(`Path=/auth`), the credentialed CORS headers, and critically the
*plain-text* `http.Error` response bodies the real handler sends on
failure (not JSON -- `enterpriseAuthRequest` in `$lib/api.ts` reads
@@ -857,7 +857,7 @@ Also not built as part of the identity mechanism itself: any Helm/
`docker-compose.yml` wiring that issues an agent a real ingest credential
automatically (`enterprise-auth -create-ingest-credential-tenant=<id>`
is, like every other credential-minting flag in this codebase, a manual
operator action) -- `deploy/helm/sentry/values.yaml`'s
operator action) -- `deploy/helm/cairnobs/values.yaml`'s
`ingest.requireTenantCredential` (default `false`) only turns on
*validation*, deliberately not folded into `enterprise.enabled`
directly, since flipping that flag with no agents holding a credential
@@ -1022,7 +1022,7 @@ that's never executed against a real database. See
**Also not built**: Helm/`docker-compose.yml` do gate *whether*
`enterprise-ingest` runs at all (`ingest.requireTenantCredential`, same
flag §13 uses for validation -- see `deploy/helm/sentry/templates/
flag §13 uses for validation -- see `deploy/helm/cairnobs/templates/
enterprise-ingest.yaml`), but `docker-compose.yml`'s version is a
disclosed, weaker approximation of Helm's: Helm achieves genuine
`-mode=server`/`-mode=consumer` mutual exclusivity between `ingest` and
@@ -1040,7 +1040,7 @@ Full accounting: `/docs/security/threat-model.md`. Headline items:
- **Both storage engines' isolation exists, and both Helm and
docker-compose now enforce which binary runs.**
`deploy/helm/sentry/templates/api.yaml`/`enterprise-api.yaml` are
`deploy/helm/cairnobs/templates/api.yaml`/`enterprise-api.yaml` are
mutually exclusive on `enterprise.enabled` (§10) -- a Helm-deployed
cluster can't accidentally run the non-isolated binary once that flag
is set. `docker-compose.yml`'s `api`/`enterprise-api` services are now
@@ -1120,14 +1120,14 @@ Full accounting: `/docs/security/threat-model.md`. Headline items:
handler reads `dashboard_permissions` via
`enterprise/internal/rbacstore.DashboardPermissions`, only when
`enterprise-api` -- not plain `api` -- serves traffic), **and now has a
CLI surface**: `sentryctl dashboards permissions list|grant|revoke`
(`cli/cmd/sentryctl/cmd_dashboards.go`) against
CLI surface**: `cairnobsctl dashboards permissions list|grant|revoke`
(`cli/cmd/cairnobsctl/cmd_dashboards.go`) against
`GET`/`PUT`/`DELETE /dashboards/{id}/permissions/{userId}` -- still no
`web` UI for it, just the CLI. Verified against a real `httptest.
Server` (not a fake store this time -- the CLI has no store of its
own, just an HTTP client, so this is exercising real request
construction/method/path/body/error-parsing, the same pattern every
other `sentryctl` subcommand's tests use):
other `cairnobsctl` subcommand's tests use):
```sh
cd cli
@@ -1162,7 +1162,7 @@ Full accounting: `/docs/security/threat-model.md`. Headline items:
```sh
docker compose down -v
helm uninstall sentry # if installed against a real cluster
helm uninstall cairnobs # if installed against a real cluster
```
## Troubleshooting
@@ -1191,6 +1191,6 @@ parameter.
**`helm template` fails with `error calling include: ... can't evaluate
field Release in type string`.**
A call site is passing a bare string to `sentry.selectorLabels` instead
A call site is passing a bare string to `cairnobs.selectorLabels` instead
of `(list $ "name")` — see `templates/_helpers.tpl`'s doc comment for
why the plain-string form doesn't work with `include`.
+1 -1
View File
@@ -63,7 +63,7 @@ Design choices worth calling out explicitly:
| Throughput under concurrency | Adequate for one-user-at-a-time interactive use; not built for high concurrent QPS | Purpose-built for high-throughput serving (continuous batching, PagedAttention) |
| Fit for this project | Matches `docker-compose for local/homelab` (CLAUDE.md's stated deployment target) — most self-hosters won't have a dedicated inference GPU | Fits a provisioned-GPU SaaS inference tier — not this phase's target (cloud is the opt-in secondary path, not primary) |
Sentry's actual AI workload shape is one interactive query bar per user
Cairn OBS's actual AI workload shape is one interactive query bar per user
at a time, not a high-QPS inference-serving problem — vLLM's real
advantage (batched throughput at scale) isn't the bottleneck this phase
has. Ollama's lower hardware floor and much simpler operational story
+1 -1
View File
@@ -191,7 +191,7 @@ cd enterprise && go build ./... && go vet ./... && go test ./...
docker run --rm --network sentry_default -v "$(pwd):/src" -w /src/enterprise \
-e AUDIT_TEST_POSTGRES_ADDR=metadata-postgres:5432 \
-e AUDIT_TEST_POSTGRES_PASSWORD=audit-writer-dev-only \
-e AUDIT_TEST_ADMIN_PASSWORD=sentry-dev-only \
-e AUDIT_TEST_ADMIN_PASSWORD=cairnobs-dev-only \
golang:1.25-alpine go test ./internal/audit/... -v
# cli module
+11 -11
View File
@@ -1,16 +1,16 @@
# Query language reference
Sentry has one query language for everything: filtering, free-text
Cairn OBS has one query language for everything: filtering, free-text
search, and aggregation, in a single query, against a single endpoint
(`POST /query`), from a single query bar in the web UI or `sentryctl
(`POST /query`), from a single query bar in the web UI or `cairnobsctl
query` on the command line. You don't pick a "search mode" or a
"reporting mode" first — you write one query, and Sentry figures out
"reporting mode" first — you write one query, and Cairn OBS figures out
which parts need ClickHouse, which parts need the full-text index, and
combines them.
If you already know Splunk's SPL, most of this will feel immediately
familiar: a base search, piped through a sequence of processing stages.
Sentry's language is a deliberately smaller subset — the operators
Cairn OBS's language is a deliberately smaller subset — the operators
people actually use day to day, not SPL's full surface area — plus raw
SQL as an escape hatch for anything the pipe syntax doesn't (yet) cover.
@@ -67,7 +67,7 @@ timeout a single bare word
message:"connection refused" the same thing, explicit
```
Free-text search is powered by Sentry's full-text index (Tantivy), which
Free-text search is powered by Cairn OBS's full-text index (Tantivy), which
supports phrase matching and wildcards:
```
@@ -92,7 +92,7 @@ what most people expect from a search bar:
error timeout same as: error and timeout
```
`or` works between free-text terms, and Sentry's full-text index handles
`or` works between free-text terms, and Cairn OBS's full-text index handles
it natively:
```
@@ -176,14 +176,14 @@ tail 50 last 50, chronologically
## Field mapping: what's a "real" column vs. an attribute
Sentry's structured columns are `timestamp`, `host`, `service`,
Cairn OBS's structured columns are `timestamp`, `host`, `service`,
`severity`, `message`, and `record_id`. Anything else you reference by
name — `status`, `latency_ms`, `winevt.event_id`, whatever your logs
happen to carry — is looked up in the per-record attributes, which are
always stored as text.
This matters for comparisons: `status>=500` only makes sense as a number,
so Sentry casts the attribute's text value to a number for you
so Cairn OBS casts the attribute's text value to a number for you
automatically when the value you're comparing against looks numeric.
`status="unknown"` compares as text instead, since `"unknown"` isn't a
number. You don't need to do anything differently — this happens based
@@ -212,7 +212,7 @@ directly, no pipe-syntax parsing involved:
SELECT host, count(*) FROM logs WHERE service = 'api' GROUP BY host
```
SELECT-only, single statement — Sentry allowlists this at the API level.
SELECT-only, single statement — Cairn OBS allowlists this at the API level.
Use this for anything the pipe syntax doesn't cover yet: window
functions, `WITH` clauses, ClickHouse-specific functions, joins across
other tables you've added, and so on. There's no performance penalty for
@@ -221,7 +221,7 @@ execution plan internally.
## Which syntax am I using?
Sentry detects automatically: a query starting with `SELECT` runs as
Cairn OBS detects automatically: a query starting with `SELECT` runs as
SQL, anything else runs as the pipe syntax. This covers the overwhelming
majority of real queries with no extra step. If you're writing a pipe
query that happens to start with the literal word "select" as a search
@@ -237,7 +237,7 @@ detected next to the query box, with a dropdown to override it.
## Combining free-text search with aggregation
This is the case that makes Sentry's query language more than "SQL with
This is the case that makes Cairn OBS's query language more than "SQL with
extra steps" — free text and aggregation, together, in one query:
```
+1 -1
View File
@@ -1,4 +1,4 @@
# Sentry Threat Model (Phase 4)
# Cairn OBS Threat Model (Phase 4)
Written for a prospective enterprise customer's security team, describing
the system **as actually built** through Phase 4 task 7 — not the target