diff --git a/alerting/Dockerfile b/alerting/Dockerfile index cc42b53..8e24c50 100644 --- a/alerting/Dockerfile +++ b/alerting/Dockerfile @@ -1,7 +1,7 @@ # alerting never needs /proto (it talks to /api over plain HTTP, no gRPC), # so unlike api/ingest/search this build context is just alerting/ itself, # same shape as cli/Dockerfile: -# docker build -f alerting/Dockerfile -t sentry-alerting alerting/ +# docker build -f alerting/Dockerfile -t cairnobs-alerting alerting/ FROM golang:1.25-alpine AS builder WORKDIR /src COPY . . diff --git a/alerting/README.md b/alerting/README.md index 480a47f..5a33dad 100644 --- a/alerting/README.md +++ b/alerting/README.md @@ -11,7 +11,7 @@ correctness properties this implementation follows exactly). POSTGRES_PASSWORD=cairnobs-dev-only API_QUERY_URL=http://localhost:8080 go run ./cmd/alerting ``` -Talks to the same `sentry_metadata` Postgres database as `/api` +Talks to the same `cairnobs_metadata` Postgres database as `/api` (different tables — see `/metadata/README.md`), and to `/api`'s `POST /query` over plain HTTP for rule evaluation. Never connects to ClickHouse or Tantivy directly. @@ -48,8 +48,8 @@ only, not separate delivery paths. |---|---| | `HTTP_LISTEN_ADDR` | `:8081` | | `POSTGRES_ADDR` | `localhost:5432` | -| `POSTGRES_DATABASE` | `sentry_metadata` | -| `POSTGRES_USERNAME` | `sentry` | +| `POSTGRES_DATABASE` | `cairnobs_metadata` | +| `POSTGRES_USERNAME` | `cairnobs` | | `POSTGRES_PASSWORD` | (empty — must be set) | | `API_QUERY_URL` | `http://localhost:8080` | | `CORS_ALLOWED_ORIGIN` | `*` | @@ -94,5 +94,5 @@ go test ./... ``` ```sh -docker build -f Dockerfile -t sentry-alerting . # context is alerting/, not the repo root -- no /proto needed +docker build -f Dockerfile -t cairnobs-alerting . # context is alerting/, not the repo root -- no /proto needed ``` diff --git a/alerting/internal/config/config.go b/alerting/internal/config/config.go index 42ad5e0..b5974db 100644 --- a/alerting/internal/config/config.go +++ b/alerting/internal/config/config.go @@ -72,8 +72,8 @@ func Load() (Config, error) { HTTPListenAddr: getenv("HTTP_LISTEN_ADDR", ":8081"), Postgres: PostgresConfig{ Addr: getenv("POSTGRES_ADDR", "localhost:5432"), - Database: getenv("POSTGRES_DATABASE", "sentry_metadata"), - Username: getenv("POSTGRES_USERNAME", "sentry"), + Database: getenv("POSTGRES_DATABASE", "cairnobs_metadata"), + Username: getenv("POSTGRES_USERNAME", "cairnobs"), Password: getenv("POSTGRES_PASSWORD", ""), }, APIQueryURL: getenv("API_QUERY_URL", "http://localhost:8080"), diff --git a/alerting/internal/sessioncheck/sessioncheck_integration_test.go b/alerting/internal/sessioncheck/sessioncheck_integration_test.go index 9f04523..4f46bb0 100644 --- a/alerting/internal/sessioncheck/sessioncheck_integration_test.go +++ b/alerting/internal/sessioncheck/sessioncheck_integration_test.go @@ -37,7 +37,7 @@ func integrationPool(t *testing.T) *pgxpool.Pool { t.Skip("SESSIONCHECK_TEST_POSTGRES_ADDR not set -- skipping live-Postgres integration test") } password := os.Getenv("SESSIONCHECK_TEST_POSTGRES_PASSWORD") - dsn := fmt.Sprintf("postgres://sentry:%s@%s/sentry_metadata", password, addr) + dsn := fmt.Sprintf("postgres://cairnobs:%s@%s/cairnobs_metadata", password, addr) pool, err := pgxpool.New(context.Background(), dsn) if err != nil { t.Fatalf("opening pool: %v", err) diff --git a/api/Dockerfile b/api/Dockerfile index bf36a90..8117756 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,7 +1,7 @@ # Build context must be the repo root (sentry/), since this needs both # api/ and proto/ (api now speaks gRPC to /search, using proto's checked-in # Go bindings via the `replace` directive in api/go.mod): -# docker build -f api/Dockerfile -t sentry-api . +# docker build -f api/Dockerfile -t cairnobs-api . FROM golang:1.25-alpine AS builder WORKDIR /src diff --git a/api/README.md b/api/README.md index 5c5b2b4..e19057b 100644 --- a/api/README.md +++ b/api/README.md @@ -46,7 +46,7 @@ Environment variables (see `internal/config/config.go`): |---|---|---| | `HTTP_LISTEN_ADDR` | `:8080` | | | `CLICKHOUSE_ADDR` | `localhost:9000` | Native protocol port | -| `CLICKHOUSE_DATABASE` / `_USERNAME` / `_PASSWORD` | `sentry` / `default` / `` | | +| `CLICKHOUSE_DATABASE` / `_USERNAME` / `_PASSWORD` | `cairnobs` / `default` / `` | | | `SEARCH_GRPC_ADDR` | `localhost:50052` | Must match `/search`'s `GRPC_LISTEN_ADDR` | | `QUERY_TIMEOUT_SECONDS` | `30` | Per-request timeout | | `CORS_ALLOWED_ORIGIN` | `*` | Wide open by default since there's no auth yet; tighten together | @@ -66,7 +66,7 @@ go test ./... ```sh # from the repo root, not api/ -docker build -f api/Dockerfile -t sentry-api . +docker build -f api/Dockerfile -t cairnobs-api . ``` ## Testing notes diff --git a/api/dashboards/store_integration_test.go b/api/dashboards/store_integration_test.go index 89a2534..f384e99 100644 --- a/api/dashboards/store_integration_test.go +++ b/api/dashboards/store_integration_test.go @@ -32,7 +32,7 @@ func integrationStore(t *testing.T) (*Store, *pgxpool.Pool) { t.Skip("DASHBOARDS_TEST_POSTGRES_ADDR not set -- skipping live-Postgres integration test") } password := os.Getenv("DASHBOARDS_TEST_POSTGRES_PASSWORD") - dsn := fmt.Sprintf("postgres://sentry:%s@%s/sentry_metadata", password, addr) + dsn := fmt.Sprintf("postgres://cairnobs:%s@%s/cairnobs_metadata", password, addr) pool, err := pgxpool.New(context.Background(), dsn) if err != nil { t.Fatalf("opening pool: %v", err) diff --git a/api/internal/config/config.go b/api/internal/config/config.go index fa80084..cd5cacd 100644 --- a/api/internal/config/config.go +++ b/api/internal/config/config.go @@ -37,8 +37,8 @@ type LocalAuthConfig struct { SessionTTL time.Duration // CookieDomain empty means a host-only cookie (fine for local dev, // where web/api are both localhost:). Set to e.g. - // ".sentry.example.com" in production so the cookie is also sent to - // api.sentry.example.com/alerting.sentry.example.com. + // ".cairnobs.example.com" in production so the cookie is also sent to + // api.cairnobs.example.com/alerting.cairnobs.example.com. CookieDomain string // CookieSecure defaults true (never sent over plain HTTP) -- // deliberately opt-out via LOCAL_AUTH_COOKIE_SECURE=false, only @@ -108,14 +108,14 @@ func Load() (Config, error) { HTTPListenAddr: getenv("HTTP_LISTEN_ADDR", ":8080"), ClickHouse: ClickHouseConfig{ Addr: getenv("CLICKHOUSE_ADDR", "localhost:9000"), - Database: getenv("CLICKHOUSE_DATABASE", "sentry"), + Database: getenv("CLICKHOUSE_DATABASE", "cairnobs"), Username: getenv("CLICKHOUSE_USERNAME", "default"), Password: getenv("CLICKHOUSE_PASSWORD", ""), }, Postgres: PostgresConfig{ Addr: getenv("POSTGRES_ADDR", "localhost:5432"), - Database: getenv("POSTGRES_DATABASE", "sentry_metadata"), - Username: getenv("POSTGRES_USERNAME", "sentry"), + Database: getenv("POSTGRES_DATABASE", "cairnobs_metadata"), + Username: getenv("POSTGRES_USERNAME", "cairnobs"), Password: getenv("POSTGRES_PASSWORD", ""), }, // Search service's gRPC address (see /search) -- default matches diff --git a/api/localauth/store_integration_test.go b/api/localauth/store_integration_test.go index 5b11ec0..5d23feb 100644 --- a/api/localauth/store_integration_test.go +++ b/api/localauth/store_integration_test.go @@ -35,7 +35,7 @@ func integrationStore(t *testing.T) *Store { t.Skip("LOCALAUTH_TEST_POSTGRES_ADDR not set -- skipping live-Postgres integration test") } password := os.Getenv("LOCALAUTH_TEST_POSTGRES_PASSWORD") - dsn := fmt.Sprintf("postgres://sentry:%s@%s/sentry_metadata", password, addr) + dsn := fmt.Sprintf("postgres://cairnobs:%s@%s/cairnobs_metadata", password, addr) pool, err := pgxpool.New(context.Background(), dsn) if err != nil { t.Fatalf("opening pool: %v", err) @@ -285,3 +285,4 @@ func TestIntegrationGetPasswordHashByID(t *testing.T) { t.Errorf("GetPasswordHashByID for an unknown ID: err = %v, want ErrNotFound", err) } } + diff --git a/docker-compose.yml b/docker-compose.yml index 0066aff..0eb7dac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,7 +38,7 @@ services: timeout: 5s retries: 30 - # One-shot: creates the sentry.logs.raw topic, then exits 0. ingest + # One-shot: creates the cairnobs.logs.raw topic, then exits 0. ingest # waits on this completing successfully before it starts. redpanda-provision: build: @@ -120,13 +120,13 @@ services: image: postgres:16-alpine container_name: cairnobs-metadata-postgres environment: - POSTGRES_DB: sentry_metadata - POSTGRES_USER: sentry + POSTGRES_DB: cairnobs_metadata + POSTGRES_USER: cairnobs POSTGRES_PASSWORD: "cairnobs-dev-only" # not a real secret, same framing as CLICKHOUSE_PASSWORD above volumes: - metadata-postgres-data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U sentry -d sentry_metadata"] + test: ["CMD-SHELL", "pg_isready -U cairnobs -d cairnobs_metadata"] interval: 5s timeout: 5s retries: 30 @@ -143,12 +143,12 @@ services: environment: POSTGRES_HOST: "metadata-postgres" POSTGRES_PORT: "5432" - POSTGRES_USER: "sentry" + POSTGRES_USER: "cairnobs" POSTGRES_PASSWORD: "cairnobs-dev-only" - POSTGRES_DATABASE: "sentry_metadata" + POSTGRES_DATABASE: "cairnobs_metadata" # Password for the restricted audit_writer Postgres role (Phase 4 # task 4) -- INSERT+SELECT only on audit_log, never UPDATE/DELETE, - # via its own connection pool distinct from the shared "sentry" + # via its own connection pool distinct from the shared "cairnobs" # role every other store uses. See /docs/phase-4-isolation-design.md. AUDIT_WRITER_PASSWORD: "audit-writer-dev-only" @@ -185,19 +185,19 @@ services: # compose file provisions one. # # AGENT_REGISTRY_POSTGRES_ADDR enables agent inventory/remote - # config (see /docs/agent-management-design.md) -- same "sentry" + # config (see /docs/agent-management-design.md) -- same "cairnobs" # shared Postgres role api/dashboards already uses (agent # inventory carries no tamper-evidence requirement, unlike # audit_log's dedicated restricted role). Set here (unlike # ENTERPRISE_AUTH_URL above) since this feature has no multi- # tenancy prerequisite -- it works the same in single-tenant core. AGENT_REGISTRY_POSTGRES_ADDR: "metadata-postgres:5432" - AGENT_REGISTRY_POSTGRES_USERNAME: "sentry" + AGENT_REGISTRY_POSTGRES_USERNAME: "cairnobs" AGENT_REGISTRY_POSTGRES_PASSWORD: "cairnobs-dev-only" volumes: - ./hack/dev-certs/out:/etc/cairnobs-ingest:ro - # Reads the same sentry.logs.raw topic ingest's consumer does (own + # Reads the same cairnobs.logs.raw topic ingest's consumer does (own # offset tracking, own failure domain — see /search/README.md) and # builds a Tantivy full-text index over the message field. search: @@ -255,8 +255,8 @@ services: CLICKHOUSE_PASSWORD: "cairnobs-dev-only" SEARCH_GRPC_ADDR: "search:50052" POSTGRES_ADDR: "metadata-postgres:5432" - POSTGRES_DATABASE: "sentry_metadata" - POSTGRES_USERNAME: "sentry" + POSTGRES_DATABASE: "cairnobs_metadata" + POSTGRES_USERNAME: "cairnobs" POSTGRES_PASSWORD: "cairnobs-dev-only" healthcheck: # alerting (Phase 3 task 5) depends_on api -- without this, that @@ -293,8 +293,8 @@ services: - "8081:8081" environment: POSTGRES_ADDR: "metadata-postgres:5432" - POSTGRES_DATABASE: "sentry_metadata" - POSTGRES_USERNAME: "sentry" + POSTGRES_DATABASE: "cairnobs_metadata" + POSTGRES_USERNAME: "cairnobs" POSTGRES_PASSWORD: "cairnobs-dev-only" # Resolves to whichever of api/enterprise-api is actually active -- # enterprise-api declares a `default.aliases: [api]` network alias @@ -337,8 +337,8 @@ services: # secret. Must be at least 32 bytes (see internal/config.Load). ENTERPRISE_SESSION_SIGNING_KEY: "cairnobs-dev-only-session-signing-key-32bytes+" POSTGRES_ADDR: "metadata-postgres:5432" - POSTGRES_DATABASE: "sentry_metadata" - POSTGRES_USERNAME: "sentry" + POSTGRES_DATABASE: "cairnobs_metadata" + POSTGRES_USERNAME: "cairnobs" POSTGRES_PASSWORD: "cairnobs-dev-only" # Where the browser lands after internal/loginhandler sets a # session cookie -- web's mapped host port (see web's build args @@ -391,8 +391,8 @@ services: CLICKHOUSE_ADMIN_PASSWORD: "cairnobs-dev-only" SEARCH_GRPC_ADDR: "search:50052" POSTGRES_ADDR: "metadata-postgres:5432" - POSTGRES_DATABASE: "sentry_metadata" - POSTGRES_USERNAME: "sentry" + POSTGRES_DATABASE: "cairnobs_metadata" + POSTGRES_USERNAME: "cairnobs" POSTGRES_PASSWORD: "cairnobs-dev-only" AUDIT_WRITER_USERNAME: "audit_writer" AUDIT_WRITER_PASSWORD: "audit-writer-dev-only" @@ -434,8 +434,8 @@ services: REDPANDA_BROKERS: "redpanda:9092" CLICKHOUSE_ADDR: "clickhouse:9000" POSTGRES_ADDR: "metadata-postgres:5432" - POSTGRES_DATABASE: "sentry_metadata" - POSTGRES_USERNAME: "sentry" + POSTGRES_DATABASE: "cairnobs_metadata" + POSTGRES_USERNAME: "cairnobs" POSTGRES_PASSWORD: "cairnobs-dev-only" healthcheck: test: ["CMD", "/enterprise-ingest", "-healthcheck"] diff --git a/docs/agent-management-design.md b/docs/agent-management-design.md index f8ea8f1..49f7797 100644 --- a/docs/agent-management-design.md +++ b/docs/agent-management-design.md @@ -135,7 +135,7 @@ already configured for. ## Data model `metadata/migrations/0037_create_agents.sql`: one `agents` table, one -row per `(tenant_id, host)`, in the same `sentry_metadata` Postgres +row per `(tenant_id, host)`, in the same `cairnobs_metadata` Postgres dashboards/alert_rules already live in — not a new database, matching this project's established "shared schema, different services own different tables" shape. `tenant_id` defaults to `'default'` for @@ -257,18 +257,18 @@ the process exits cleanly -- `pending_command` confirmed cleared and ## CLI surface (punch-list item 3) -`sentryctl agents` (`cli/cmd/sentryctl/cmd_agents.go`), same list/get +`cairnobsctl agents` (`cli/cmd/cairnobsctl/cmd_agents.go`), same list/get shape as `dashboards`/`alerts`, plus a `config` sub-subcommand (mirroring `dashboards permissions`) since an override has its own get/set/clear lifecycle distinct from the agent resource itself: ``` -sentryctl agents list|get -sentryctl agents config get |clear -sentryctl agents config set [--batch-max-size N] [--batch-flush-interval-ms N] +cairnobsctl agents list|get +cairnobsctl agents config get |clear +cairnobsctl agents config set [--batch-max-size N] [--batch-flush-interval-ms N] [--heartbeat-enabled true|false] [--heartbeat-interval-ms N] [--journald-unit UNIT] -sentryctl agents restart [--yes] +cairnobsctl agents restart [--yes] ``` `config set` is the one command with real logic beyond a thin HTTP diff --git a/docs/architecture.md b/docs/architecture.md index 7d18052..a0b52c0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -81,7 +81,7 @@ This split is not to be changed without discussion — see CLAUDE.md. | `alerting` (Go, Phase 3) | Evaluates alert rules on an interval, calls `api`'s `POST /query` (via a `RoleService` credential once Phase 4 auth is configured — see `/docs/phase-4-isolation-design.md`'s alerting↔api gap), delivers firing/resolved notifications (webhook/Slack/PagerDuty). | | `enterprise` (Go, AGPLv3 — see "Licensing boundary" below, Phase 4) | OIDC login (`internal/loginhandler`'s `/auth/oidc/login`+`/auth/oidc/callback`) and SAML login (`/auth/saml/login`+`/auth/saml/acs`, via `internal/saml`'s `crewjam/saml` wiring) — both a real IdP round trip, each verified with a real fake IdP (`coreos/go-oidc`'s `oidctest`, `crewjam/saml`'s `samlidp`) but not a real external one, RBAC storage (`internal/rbacstore`), session/service-token issuance (`internal/session`), the append-only audit log (`internal/audit`), `enterprise-auth`'s HTTP surface (`/internal/authorize`, `/auth/features`), per-tenant ClickHouse provisioning (`internal/tenantprovision`) and query routing (`internal/chrunner`), and `cmd/enterprise-api` — a second binary combining core's `api/queryapi`/`api/dashboards` handlers with these tenant-aware implementations. Never imported by core — see "Licensing boundary" below. Also `internal/searchclient` (per-tenant Tantivy routing, wired the same way into `search`). | | `web` (SvelteKit, static build) | Query bar, dashboards, alerts, and (Phase 4) a settings page that renders SSO status via a runtime capability check (`GET /auth/features`) rather than bundling `enterprise/`'s components directly — an architectural choice (core builds and runs standalone) that predates and doesn't depend on Phase 6's relicensing. | -| `cli` (`sentryctl`) | `ping`, `query`, `dashboards` (list/get/apply), `alerts` (list/get/apply). `$SENTRYCTL_TOKEN`, if set, is forwarded as a Bearer credential (Phase 4). | +| `cli` (`cairnobsctl`) | `ping`, `query`, `dashboards` (list/get/apply), `alerts` (list/get/apply). `$CAIRNOBSCTL_TOKEN`, if set, is forwarded as a Bearer credential (Phase 4). | | `deploy` | A Helm chart covering every `docker-compose.yml` service, plus (Phase 4) a small Go Operator managing one CRD (`Tenant`) that provisions a per-tenant ClickHouse credential Secret. Never applied to a live cluster in the environment this was built in — see `/deploy/README.md`'s verification section before trusting it. | ## Tenant isolation model (Phase 4) @@ -199,7 +199,7 @@ escape hatch is opaque to any compiler-injected filter. new credentials and still never touches ClickHouse/Postgres. **The deployment-topology gap is closed for both Helm and -docker-compose**: `deploy/helm/sentry/templates/api.yaml`/ +docker-compose**: `deploy/helm/cairnobs/templates/api.yaml`/ `enterprise-api.yaml` are mutually exclusive on `enterprise.enabled`, rendering to the same Service name and port either way, so a Helm-deployed cluster can't accidentally run the wrong binary — the same diff --git a/docs/compliance/license-inventory.json b/docs/compliance/license-inventory.json index f0de707..55d7e70 100644 --- a/docs/compliance/license-inventory.json +++ b/docs/compliance/license-inventory.json @@ -8510,7 +8510,7 @@ "direct_or_transitive": "direct", "ecosystem": "docker-image", "flagged": "True", - "flag_reason": "Category (c): BSL is explicitly source-available, not OSI-approved open source, per the audit's own classification framework. Verified against the actual licenses/bsl.md at the v24.2.7 tag, not assumed. No AGPL linking-compatibility issue (Redpanda is consumed only over the Kafka wire protocol, never linked into Sentry's own code -- same relationship as ClickHouse/Postgres). Real open question: whether a third party self-hosting Sentry (now fully AGPLv3 per task 6) 'as a service' would trip BSL's Streaming-or-Queuing-Service restriction on the bundled Redpanda image -- Sentry's ingest pipeline creates fixed internal topics, not per-end-user topics, so this is very likely NOT a Streaming-or-Queuing-Service under BSL's own definition, but this is a business/redistribution judgment call, not a pure technical one -- flagged for your decision, not resolved unilaterally. See license-audit-report.md's Redpanda section for the three remediation options (accept as-is / swap to Apache Kafka / flag only). RESOLVED 2026-08-16 (business decision): accept as-is. Sentry's own use (internal Kafka-protocol transport, no resale of broker access) is within BSL's Additional Use Grant; the third-party self-hosting-as-a-service question was judged unlikely to trip BSL's Streaming-or-Queuing-Service restriction, and was accepted as a known, disclosed risk rather than swapping to a heavier broker or dropping the bundled image. See license-audit-report.md's Redpanda section.", + "flag_reason": "Category (c): BSL is explicitly source-available, not OSI-approved open source, per the audit's own classification framework. Verified against the actual licenses/bsl.md at the v24.2.7 tag, not assumed. No AGPL linking-compatibility issue (Redpanda is consumed only over the Kafka wire protocol, never linked into Cairn OBS's own code -- same relationship as ClickHouse/Postgres). Real open question: whether a third party self-hosting Cairn OBS (now fully AGPLv3 per task 6) 'as a service' would trip BSL's Streaming-or-Queuing-Service restriction on the bundled Redpanda image -- Cairn OBS's ingest pipeline creates fixed internal topics, not per-end-user topics, so this is very likely NOT a Streaming-or-Queuing-Service under BSL's own definition, but this is a business/redistribution judgment call, not a pure technical one -- flagged for your decision, not resolved unilaterally. See license-audit-report.md's Redpanda section for the three remediation options (accept as-is / swap to Apache Kafka / flag only). RESOLVED 2026-08-16 (business decision): accept as-is. Cairn OBS's own use (internal Kafka-protocol transport, no resale of broker access) is within BSL's Additional Use Grant; the third-party self-hosting-as-a-service question was judged unlikely to trip BSL's Streaming-or-Queuing-Service restriction, and was accepted as a known, disclosed risk rather than swapping to a heavier broker or dropping the bundled image. See license-audit-report.md's Redpanda section.", "classification": "c" }, { diff --git a/docs/phase-0-runbook.md b/docs/phase-0-runbook.md index 62428b7..ae68f24 100644 --- a/docs/phase-0-runbook.md +++ b/docs/phase-0-runbook.md @@ -49,7 +49,7 @@ docker compose up -d --build ``` This builds and starts, in dependency order: `redpanda` → `redpanda-provision` -(creates the `sentry.logs.raw` topic, then exits) → `clickhouse` → +(creates the `cairnobs.logs.raw` topic, then exits) → `clickhouse` → `clickhouse-migrate` (applies `/storage/migrations`, then exits) → `ingest` and `api` → `web`. @@ -133,7 +133,7 @@ In another terminal, **after** the agent is running and connected started won't be picked up): ```sh -logger "hello from sentry phase 0" +logger "hello from cairnobs phase 0" ``` `logger` (part of util-linux, present on virtually every Linux distro) @@ -150,7 +150,7 @@ interval by default, so the line won't hit ingest instantly. starts every service in the file). Open `http://localhost:3000`, run the default query (`SELECT * FROM logs ORDER BY timestamp DESC LIMIT 100`), and look for a row with -`message = "hello from sentry phase 0"`. +`message = "hello from cairnobs phase 0"`. **Or via curl, if you want to skip the browser:** @@ -160,11 +160,11 @@ curl -X POST http://localhost:8080/query \ -d '{"sql": "SELECT * FROM logs ORDER BY timestamp DESC LIMIT 10"}' ``` -**Or via sentryctl, just to confirm api is up (doesn't check the data +**Or via cairnobsctl, just to confirm api is up (doesn't check the data itself):** ```sh -cd cli && go run ./cmd/sentryctl ping +cd cli && go run ./cmd/cairnobsctl ping ``` If you see the row: that's Phase 0 done, end to end. If you don't, see @@ -199,7 +199,7 @@ Check each hop in order rather than guessing: at-least-once design (see `/ingest/README.md`), so this may just need more time rather than intervention. 3. `docker compose exec redpanda rpk topic list` — confirm - `sentry.logs.raw` exists (if `redpanda-provision` failed, it won't). + `cairnobs.logs.raw` exists (if `redpanda-provision` failed, it won't). **`docker compose up` fails on `service_completed_successfully`.** You're likely on Compose v1 (`docker-compose`, hyphenated) rather than v2 diff --git a/docs/phase-1-runbook.md b/docs/phase-1-runbook.md index 1ee14ae..bc4c633 100644 --- a/docs/phase-1-runbook.md +++ b/docs/phase-1-runbook.md @@ -54,7 +54,7 @@ docker compose logs search ``` You should see "search gRPC server listening" and rskafka connecting to -all of `sentry.logs.raw`'s partitions. If you see nothing at all, check +all of `cairnobs.logs.raw`'s partitions. If you see nothing at all, check `RUST_LOG=info` is set on the `search` service in `docker-compose.yml`. ### A2. Generate a log line and confirm both query paths agree diff --git a/docs/phase-3-runbook.md b/docs/phase-3-runbook.md index daf198f..d145ac7 100644 --- a/docs/phase-3-runbook.md +++ b/docs/phase-3-runbook.md @@ -43,10 +43,10 @@ go run . --count 500000 Create a dashboard and a couple of panels, either through the web UI (`http://localhost:3000/dashboards` → "+ Create" → "+ Add panel") or via -`sentryctl`: +`cairnobsctl`: ```sh -sentryctl dashboards apply my-dashboard.json # shape = GET /dashboards/{id}/export +cairnobsctl dashboards apply my-dashboard.json # shape = GET /dashboards/{id}/export ``` **Verified live**: a table panel (`severity=INFO | head 10`) and a bar @@ -95,7 +95,7 @@ Bring up a local webhook receiver for testing (no real Slack/PagerDuty needed): ```sh -docker run -d --name sentry-webhook-sink --network sentry_default \ +docker run -d --name cairnobs-webhook-sink --network sentry_default \ -p 9099:9099 -v $(pwd)/hack/webhook-sink:/src -w /src golang:1.25-alpine go run . ``` @@ -104,7 +104,7 @@ Create a notification target and a rule, either via the web UI ```sh curl -X POST http://localhost:8081/targets -H 'Content-Type: application/json' -d '{ - "name": "local sink", "kind": "webhook", "webhook_url": "http://sentry-webhook-sink:9099/" + "name": "local sink", "kind": "webhook", "webhook_url": "http://cairnobs-webhook-sink:9099/" }' curl -X POST http://localhost:8081/rules -H 'Content-Type: application/json' -d '{ @@ -119,7 +119,7 @@ curl -X POST http://localhost:8081/rules -H 'Content-Type: application/json' -d rule via the `/alerts/new` form, watched it transition in the browser): the rule transitions `ok` → `firing` on its first evaluation (`for_minutes: 0`), the delivery log shows `firing / sent / 200`, and -`docker logs sentry-webhook-sink` shows the real received payload. +`docker logs cairnobs-webhook-sink` shows the real received payload. Also verified live: a threshold rule whose query returns **zero rows** records `last_eval_status: "error"` with the exact expected message @@ -205,13 +205,13 @@ already claimed. Moving off a single-process ticker to a distributed scheduler, and materially larger rule counts (10,000+), are both explicitly out of scope for this phase. -## 5. Confirm `sentryctl` +## 5. Confirm `cairnobsctl` ```sh -sentryctl dashboards list -sentryctl dashboards apply exported-dashboard.json -sentryctl alerts list -sentryctl alerts apply rule.json +cairnobsctl dashboards list +cairnobsctl dashboards apply exported-dashboard.json +cairnobsctl alerts list +cairnobsctl alerts apply rule.json ``` Both `dashboards` and `alerts` hit the exact same REST endpoints the web diff --git a/docs/phase-4-rbac-design.md b/docs/phase-4-rbac-design.md index a50c26a..5d184bd 100644 --- a/docs/phase-4-rbac-design.md +++ b/docs/phase-4-rbac-design.md @@ -108,7 +108,7 @@ yet. Stated plainly so this doesn't read as more built than it is. ## Schema -Lives in `/metadata` (`sentry_metadata`), alongside everything else from +Lives in `/metadata` (`cairnobs_metadata`), alongside everything else from Phase 3, per `/docs/phase-4-isolation-design.md`'s existing schema additions (`tenants`, the `tenant_id` backfill on `alert_state`/ `delivery_log`). New tables, continuing that migration sequence: diff --git a/docs/phase-7-ai-design.md b/docs/phase-7-ai-design.md index c3a086f..7b869ef 100644 --- a/docs/phase-7-ai-design.md +++ b/docs/phase-7-ai-design.md @@ -265,7 +265,7 @@ with three levels (`ok`/`warn`/`reject`) and human-readable reasons: query"), decided here rather than left ambiguous: hand-written queries get the identical assessment an AI-generated one would, so there's real parity, but retroactively hard-blocking existing dashboard/ - `sentryctl` query patterns that happen to have no time bound is a + `cairnobsctl` query patterns that happen to have no time bound is a behavioral change this phase didn't set out to make and could break real existing usage. `warnings` is `omitempty` -- a client that doesn't look for it sees no shape change at all. All existing @@ -472,7 +472,7 @@ renders them is structurally the same conditional-message pattern already live-verified repeatedly for Explain/Fix/Optimize's own "unavailable" states, not new untested UI shape. -CLI (task 11): `sentryctl query --nl "..."` in `cli/cmd/sentryctl/cmd_query.go`. +CLI (task 11): `cairnobsctl query --nl "..."` in `cli/cmd/cairnobsctl/cmd_query.go`. Same posture as the UI, enforced identically regardless of how the result was produced: a low-confidence, non-compiling, or cost-guard-blocked translation is never run, even with `--execute` -- confirmed by @@ -559,7 +559,7 @@ query at the moment of acceptance, not tracked keystroke-by-keystroke. **Genuinely verified against a live Postgres**, not just unit-tested against a fake `InteractionLogger`: `metadata/migrations/0036` was -applied to the running dev stack's `sentry-metadata-postgres` +applied to the running dev stack's `cairnobs-metadata-postgres` (`docker compose up -d --build metadata-migrate`, confirmed via `\d+ audit_log` before/after showing `ai_interaction` added to the `event_type` CHECK constraint), and two new tests in @@ -622,7 +622,7 @@ ends up with: handler/routing logic via `fakeProvider` -- fast, deterministic, no network, all run in CI today. 2. **Integration tests** (this task, new): the mock-Ollama-server suite - above, plus `cli/cmd/sentryctl/cmd_query_test.go`'s existing + above, plus `cli/cmd/cairnobsctl/cmd_query_test.go`'s existing `httptest.Server`-backed coverage of `--nl`/`--execute` (already written during Track B, task 11) -- proves the plumbing (HTTP routing, JSON contracts, `planner.Compile`/`costguard.Assess` integration, diff --git a/docs/phase-7-runbook.md b/docs/phase-7-runbook.md index e0b9fc4..f5f707e 100644 --- a/docs/phase-7-runbook.md +++ b/docs/phase-7-runbook.md @@ -12,7 +12,7 @@ document is verification only. Every AI operation (`complete`, `explain`, `fix`, `optimize`, `translate`, and the audit-logging endpoint behind it) has been run end-to-end against a real `docker compose` stack — real HTTP requests -into the real `sentry-api` container, through the real +into the real `cairnobs-api` container, through the real `api/ai/provider/ollama.Client`, over a real network call, into a real process answering Ollama's actual `/api/chat` wire contract. **No real model weights are used anywhere in this verification** — see @@ -74,7 +74,7 @@ Run it as a container on the compose network with a network alias of `OLLAMA_BASE_URL` at it via a throwaway compose override: ```sh -docker run -d --rm --name sentry-mock-ollama --network sentry_default --network-alias ollama \ +docker run -d --rm --name cairnobs-mock-ollama --network sentry_default --network-alias ollama \ -v "$(pwd)/hack/mock-ollama:/src" -w /src golang:1.25-alpine \ sh -c "go build -o /tmp/mock-ollama . && /tmp/mock-ollama" @@ -106,7 +106,7 @@ wired into a stack anyone else might reach: ```sh docker compose up -d api # drops back to the plain env, no -f override -docker rm -f sentry-mock-ollama +docker rm -f cairnobs-mock-ollama rm /tmp/docker-compose.ai-verify.yml curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/ai/translate -d '{}' # 404 -- confirms AI routes are unregistered again @@ -118,7 +118,7 @@ With AI routes enabled (step 2) and the web dev server running against `localhost:8080`, open the Search page's query bar: - Type a partial query and pause — ghost text should appear inline - after ~300ms; Tab accepts it. Stop `sentry-mock-ollama` and confirm + after ~300ms; Tab accepts it. Stop `cairnobs-mock-ollama` and confirm ghost text just silently stops appearing (no error toast, no console noise) — this is the "graceful degradation" requirement, not incidental behavior. @@ -149,7 +149,7 @@ separately click "Run query". CLI: ```sh -cd cli && go run ./cmd/sentryctl query --nl "errors in the last hour" --api http://localhost:8080 +cd cli && go run ./cmd/cairnobsctl query --nl "errors in the last hour" --api http://localhost:8080 # prints the translated query and, in an interactive terminal, prompts y/N before running ``` @@ -164,7 +164,7 @@ pattern applied to that service instead: 1. Accept or dismiss a Fix/Optimize/Translate suggestion in the web UI. 2. Confirm a row landed in `audit_log`: ```sh - docker exec sentry-metadata-postgres psql -U sentry -d sentry_metadata \ + docker exec cairnobs-metadata-postgres psql -U cairnobs -d cairnobs_metadata \ -c "SELECT event_type, query_text, detail FROM audit_log WHERE event_type='ai_interaction' ORDER BY id DESC LIMIT 5;" ``` `detail` should show `operation`/`accepted`/`edited` matching what you diff --git a/docs/query-language-design.md b/docs/query-language-design.md index 57be6ee..ebf9fd3 100644 --- a/docs/query-language-design.md +++ b/docs/query-language-design.md @@ -23,8 +23,15 @@ new cross-backend join strategy from scratch. Pipe syntax, SPL-inspired, EBNF-ish: ``` -query := base_search ("|" pipe_stage)* +query := (base_search | pipe_stage) ("|" pipe_stage)* base_search := bool_expr // implicit filter/search, SPL convention + // omitted entirely when the query starts + // directly with a pipe-stage keyword (e.g. + // `stats count by host`, no leading filter, + // no leading "|") -- means match-everything. + // A field genuinely named "where"/"stats"/etc + // still parses as a filter (`where=foo`), + // disambiguated by comparator lookahead. pipe_stage := "where" bool_expr | "stats" agg_call ("," agg_call)* ["by" field ("," field)*] | "sort" sort_field ("," sort_field)* diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index 408db70..b2b63d9 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -97,13 +97,13 @@ wire-level `SearchRequest` carries the right `tenant_id`. All pass, for real, no disclaimer needed for this specific claim. **Both Helm and docker-compose now close this.** -`deploy/helm/sentry/templates/api.yaml` and `enterprise-api.yaml` are +`deploy/helm/cairnobs/templates/api.yaml` and `enterprise-api.yaml` are mutually exclusive, gated on opposite sides of the same `enterprise.enabled` flag, rendering to the same Service name/port — so a Helm-deployed cluster runs exactly one of the two binaries, chosen by the same flag that turns on RBAC/audit/SSO, not a second independently-forgettable decision. Verified by parsing (not -eyeballing) the rendered YAML under both values: exactly one `sentry-api` +eyeballing) the rendered YAML under both values: exactly one `cairnobs-api` Deployment either way, with the right image. `docker-compose.yml`'s `api`/`enterprise-api` services are now the analogous mutually-exclusive choice, gated behind `COMPOSE_PROFILES` (`.env` checks in @@ -233,7 +233,7 @@ Browser ──▶ enterprise-auth (GET /auth/oidc/login, /auth/oidc/callback) └─▶ external IdP (OIDC authorization code flow) └─▶ Postgres (rbacstore: users, tenant_memberships) -sentryctl ──▶ api, alerting (Bearer token when SENTRYCTL_TOKEN is set) +cairnobsctl ──▶ api, alerting (Bearer token when CAIRNOBSCTL_TOKEN is set) ``` Ingest path (agent → Redpanda → ingest → ClickHouse, and Redpanda → @@ -442,12 +442,12 @@ layer": 1. A dedicated `audit_writer` Postgres role with only `INSERT`+`SELECT` grants (`metadata/migrations/0012-0014`), via its **own** - `pgxpool.Pool` — never the shared `sentry` role/pool every other + `pgxpool.Pool` — never the shared `cairnobs` role/pool every other store uses. 2. A `BEFORE UPDATE OR DELETE ... RAISE EXCEPTION` trigger (`metadata/migrations/0015-0016`) that rejects the operation for *any* role, including the table owner — confirmed live: even the - `sentry` role cannot `UPDATE` a row without first disabling the + `cairnobs` role cannot `UPDATE` a row without first disabling the trigger, a privileged operation distinct from ordinary application access. @@ -547,10 +547,10 @@ terms: cluster/trusted network (`api`/`alerting`/`web`), never exposed publicly. Nothing in this codebase enforces that at the network layer; it's a deployment responsibility (NetworkPolicy, or equivalent) not - yet codified in `/deploy/helm/sentry`. + yet codified in `/deploy/helm/cairnobs`. - `ENTERPRISE_SESSION_SIGNING_KEY`, ClickHouse/Postgres passwords, and (once minted) the `alerting` service token are all K8s `Secret` - objects in the Helm chart (`/deploy/helm/sentry/templates/ + objects in the Helm chart (`/deploy/helm/cairnobs/templates/ secrets.yaml`) — standard K8s `Secret` semantics apply (base64, not encrypted at rest without a cluster-level `EncryptionConfiguration`). No secrets-manager integration (Vault, cloud KMS) exists; the chart @@ -571,7 +571,7 @@ terms: | Ingest tenant *identity* (credential validation, tagging) | **Built and tested** — fail-closed `TenantResolver`, `tenant_id` Kafka header attached per record | | Ingest tenant *write-routing*, ClickHouse | **Enforced, verified live** — `enterprise-ingest`/`chwriter.Registry` route each tagged batch to its tenant's own database, fail-closed on an untagged/unprovisioned tenant; both Docker-free and live-ClickHouse tests pass. Active-tenant snapshot refreshes every minute (`Registry.StartRefreshing`) — a deprovisioned tenant loses write access within a minute, not "until the next restart" | | Ingest tenant *write-routing*, Tantivy | **Built and genuinely verified** — `search/src/consumer.rs` routes each record into its own tenant's index via `IndexRegistry`, same registry the (already-verified) read side uses; no Docker needed, real tests pass. Active-tenant-gated too: `tenants::ActiveTenantTracker` polls `enterprise-auth` every 60s (off unless configured), refusing any tenant not in the polled allowlist — same one-minute staleness bound as ClickHouse's now-refreshing snapshot, no more asymmetry between the two | -| Deployment actually routing traffic to `enterprise-api` (Helm) | **Enforced, verified live** — `api`/`enterprise-api` are mutually exclusive, same flag as RBAC/audit/SSO; a real `helm install` against a real `kind` cluster confirmed the `sentry-api` Deployment runs `sentry-enterprise-api:latest` with `enterprise.enabled=true`, real endpoints behind the `sentry-api` Service, not just `helm template`'s rendered YAML | +| Deployment actually routing traffic to `enterprise-api` (Helm) | **Enforced, verified live** — `api`/`enterprise-api` are mutually exclusive, same flag as RBAC/audit/SSO; a real `helm install` against a real `kind` cluster confirmed the `cairnobs-api` Deployment runs `cairnobs-enterprise-api:latest` with `enterprise.enabled=true`, real endpoints behind the `cairnobs-api` Service, not just `helm template`'s rendered YAML | | Deployment actually routing traffic to `enterprise-api` (docker-compose) | **Enforced, verified live** — `api`/`enterprise-api` are mutually exclusive via `COMPOSE_PROFILES`, same flag choice as Helm's `enterprise.enabled`; a real `docker compose up` of `enterprise-api` was run in this environment (and caught/fixed a startup-crashing duplicate `GET /healthz` route registration bug in the process), not just `docker compose config` | | Human SSO login — OIDC | **Enforced, verified live** — real login against a real Auth0 developer tenant, full browser round trip; correctly failed closed on an identity with no `tenant_memberships` row, then succeeded and issued a real session after `-grant-membership-*`, with `POST /internal/authorize` returning exactly the granted tenant/role | | Human SSO login — SAML | **Enforced, verified live** — real login against Auth0's SAML2 Web App addon acting as a real SAML IdP, over real (self-signed, dev-only) TLS; a real signed assertion validated (audience, destination, signature), landed on `/select-tenant` with real memberships, and `POST /internal/authorize` confirmed the selected tenant/role. Found and fixed a real bug in the process: `loginhandler.go`'s cookies decided `Secure` from `r.TLS != nil` alone, which is wrong behind any TLS-terminating reverse proxy (the deployment shape this handler actually runs in) — `enterprise-auth` never terminates TLS itself, so `r.TLS` was nil even over a genuinely HTTPS connection, silently dropping `Secure` and breaking SAML's `SameSite=None` cookie | diff --git a/enterprise/Dockerfile b/enterprise/Dockerfile index e6460bc..fbe6f8f 100644 --- a/enterprise/Dockerfile +++ b/enterprise/Dockerfile @@ -13,7 +13,7 @@ # imports existed; the repo-root context below is the same shape # enterprise-api's and enterprise-ingest's Dockerfiles already use for # the identical reason. -# docker build -f enterprise/Dockerfile -t sentry-enterprise-auth . +# docker build -f enterprise/Dockerfile -t cairnobs-enterprise-auth . FROM golang:1.25-alpine AS builder WORKDIR /src COPY . . diff --git a/enterprise/README.md b/enterprise/README.md index 5a72c1a..dda0972 100644 --- a/enterprise/README.md +++ b/enterprise/README.md @@ -40,7 +40,7 @@ section for exactly what "not yet run" means here and why. Don't read - `/alerting`'s `queryclient` presents a `RoleService` Bearer token (`API_SERVICE_TOKEN`) when configured -- see `/docs/phase-4-isolation-design.md`'s `alerting`↔`api` gap. -- `sentryctl` presents `$SENTRYCTL_TOKEN` as a Bearer credential on every +- `cairnobsctl` presents `$CAIRNOBSCTL_TOKEN` as a Bearer credential on every request when set. - `internal/rbacstore`: full CRUD over `users`/`tenants`/ `tenant_memberships`/`data_sources` (`metadata/migrations/0017-0032`). @@ -155,7 +155,7 @@ issues a `session.Manager.IssuePendingLogin` token (a distinct Go/JWT type from a real session -- see that type's doc comment for a real bug this design caught in its own tests: a shared JSON key would have let a full session token double as a pending login) as a -`sentry_pending_login` cookie (`Path=/auth`) and redirects to +`cairnobs_pending_login` cookie (`Path=/auth`) and redirects to `SELECT_TENANT_REDIRECT_URL` (defaults to `{POST_LOGIN_REDIRECT_URL}/select-tenant`) instead of completing the login. From there: @@ -204,7 +204,7 @@ below) and Tantivy (`/search/README.md`'s "Per-tenant indices" section -- needed no code in this module at all, since `search`'s `IndexRegistry` already lived in AGPL core); and deployment-topology routing (does traffic actually reach `enterprise-api` instead of -`api`), now a single-flag choice in both `deploy/helm/sentry` and +`api`), now a single-flag choice in both `deploy/helm/cairnobs` and `docker-compose.yml` (`enterprise.enabled` / `COMPOSE_PROFILES`), see CLAUDE.md. @@ -285,7 +285,7 @@ for both directions is the simpler, still-correctly-scoped choice). A real multi-tenant deployment runs `ingest -mode=server` (agent-facing, tags records, unchanged) alongside `enterprise-ingest` (consumer, per-tenant writes) *instead of* `ingest -mode=consumer` -- see `deploy/ -helm/sentry`'s `ingest.requireTenantCredential` value (gates both the +helm/cairnobs`'s `ingest.requireTenantCredential` value (gates both the credential-validation requirement and this mode split together, since write-routing is only meaningful once records actually carry a tenant_id to route on) and `docker-compose.yml`'s `enterprise-ingest` @@ -318,7 +318,7 @@ internal/saml/ crewjam/saml wiring: SP setup, login redirect, respons internal/session/ issues/validates signed session + RoleService tokens internal/authhandler/ POST /internal/authorize, GET /auth/features internal/loginhandler/ GET /auth/oidc/{login,callback} + GET /auth/saml/login + POST /auth/saml/acs -- the human login flow -internal/rbacstore/ users/tenants/tenant_memberships/data_sources/dashboard_permissions CRUD (pgx against sentry_metadata) +internal/rbacstore/ users/tenants/tenant_memberships/data_sources/dashboard_permissions CRUD (pgx against cairnobs_metadata) internal/tenantprovision/ real ClickHouse CREATE DATABASE/USER/GRANT internal/tenantcrd/ syncs -provision-tenant's real result into deploy/operator's Tenant CRD (K8s dynamic client, no cluster needed to test) internal/chrunner/ tenant-scoped api/querylang/executor.SQLRunner @@ -431,7 +431,7 @@ SEARCH_TOKEN=$(docker compose run --rm enterprise-auth -mint-service-token=searc ``` ```sh -docker build -f Dockerfile -t sentry-enterprise-auth . # context is enterprise/, not the repo root +docker build -f Dockerfile -t cairnobs-enterprise-auth . # context is enterprise/, not the repo root ``` ## Bootstrapping a tenant and its first human user @@ -484,7 +484,7 @@ comment already worries about). Changing a non-Owner role is just re-running `-grant-membership-*` with a different `-grant-membership-role` (`SetMembership`'s upsert already supports it). `dashboard_permissions` grants have no `enterprise-auth` flag and -don't need one -- `sentryctl dashboards permissions list|grant|revoke` +don't need one -- `cairnobsctl dashboards permissions list|grant|revoke` covers them over the HTTP endpoints `api/dashboards`' handler already exposes (`PUT`/`DELETE /dashboards/{id}/permissions/{userId}`, `GET .../permissions`), see `/cli/README.md`. @@ -514,8 +514,8 @@ curl -s http://localhost:8080/healthz `internal/tenantcrd` sync step is a documented no-op in this deployment shape, same as everywhere else this codebase has an "off unless configured" optional dependency. It only does anything in a real -cluster with `deploy/helm/sentry`'s `tenantOperator.enabled=true` -- see -`/deploy/helm/sentry/README.md`'s "Trying the two-tenant example." +cluster with `deploy/helm/cairnobs`'s `tenantOperator.enabled=true` -- see +`/deploy/helm/cairnobs/README.md`'s "Trying the two-tenant example." `-provision-tenant` creates the tenant/data_source rows in rbacstore if they don't exist, provisions ClickHouse, persists the credentials, and @@ -529,8 +529,8 @@ to, see `tenantprovision.ProvisionClickHouse`'s doc comment). |---|---| | `HTTP_LISTEN_ADDR` | `:8082` | | `POSTGRES_ADDR` | `localhost:5432` | -| `POSTGRES_DATABASE` | `sentry_metadata` | -| `POSTGRES_USERNAME` | `sentry` | +| `POSTGRES_DATABASE` | `cairnobs_metadata` | +| `POSTGRES_USERNAME` | `cairnobs` | | `POSTGRES_PASSWORD` | (empty) | | `OIDC_ISSUER_URL` | (empty — OIDC discovery skipped if unset) | | `OIDC_CLIENT_ID` | (empty) | @@ -554,8 +554,8 @@ to, see `tenantprovision.ProvisionClickHouse`'s doc comment). | `CLICKHOUSE_ADMIN_PASSWORD` | (empty) | | `SEARCH_GRPC_ADDR` | `localhost:50052` | | `POSTGRES_ADDR` | `localhost:5432` | -| `POSTGRES_DATABASE` | `sentry_metadata` | -| `POSTGRES_USERNAME` | `sentry` | +| `POSTGRES_DATABASE` | `cairnobs_metadata` | +| `POSTGRES_USERNAME` | `cairnobs` | | `POSTGRES_PASSWORD` | (empty) | | `AUDIT_WRITER_USERNAME` | `audit_writer` | | `AUDIT_WRITER_PASSWORD` | (empty) | diff --git a/enterprise/cmd/enterprise-api/Dockerfile b/enterprise/cmd/enterprise-api/Dockerfile index c4f58dc..de3ae0a 100644 --- a/enterprise/cmd/enterprise-api/Dockerfile +++ b/enterprise/cmd/enterprise-api/Dockerfile @@ -1,7 +1,7 @@ # Same shape as every other Go service's Dockerfile in this repo -- # context must be the repo root (needs both enterprise/ and proto/, like # api/Dockerfile does for api/ + proto/), not enterprise/ alone. -# docker build -f enterprise/cmd/enterprise-api/Dockerfile -t sentry-enterprise-api . +# docker build -f enterprise/cmd/enterprise-api/Dockerfile -t cairnobs-enterprise-api . FROM golang:1.25-alpine AS builder WORKDIR /src COPY . . diff --git a/enterprise/cmd/enterprise-api/main.go b/enterprise/cmd/enterprise-api/main.go index c1e3a75..4aae840 100644 --- a/enterprise/cmd/enterprise-api/main.go +++ b/enterprise/cmd/enterprise-api/main.go @@ -17,7 +17,7 @@ // keeps running plain api/cmd/api, unchanged; a real multi-tenant // deployment runs this one instead. // -// Both Helm (deploy/helm/sentry/templates/api.yaml vs +// Both Helm (deploy/helm/cairnobs/templates/api.yaml vs // enterprise-api.yaml) and docker-compose.yml (COMPOSE_PROFILES) now // make this the deployment-topology choice, not just a binary sitting // unused alongside api's -- see this repo's CLAUDE.md. `search`'s write diff --git a/enterprise/cmd/enterprise-ingest/Dockerfile b/enterprise/cmd/enterprise-ingest/Dockerfile index 352dba3..423d21b 100644 --- a/enterprise/cmd/enterprise-ingest/Dockerfile +++ b/enterprise/cmd/enterprise-ingest/Dockerfile @@ -2,7 +2,7 @@ # context must be the repo root (needs ingest/, proto/, and enterprise/, # like enterprise-api/Dockerfile does for api/ + proto/ + enterprise/), # not enterprise/ alone. -# docker build -f enterprise/cmd/enterprise-ingest/Dockerfile -t sentry-enterprise-ingest . +# docker build -f enterprise/cmd/enterprise-ingest/Dockerfile -t cairnobs-enterprise-ingest . FROM golang:1.25-alpine AS builder WORKDIR /src COPY . . diff --git a/enterprise/cmd/enterprise-ingest/main.go b/enterprise/cmd/enterprise-ingest/main.go index 57c42b1..0bb5688 100644 --- a/enterprise/cmd/enterprise-ingest/main.go +++ b/enterprise/cmd/enterprise-ingest/main.go @@ -1,6 +1,6 @@ // Command enterprise-ingest is the multi-tenant-aware alternative to // running `ingest -mode=consumer` -- reads the same shared -// sentry.logs.raw Redpanda topic ingest/cmd/ingest's agent-facing +// cairnobs.logs.raw Redpanda topic ingest/cmd/ingest's agent-facing // server half (PushBatch) produces onto (see that binary's doc // comment), but writes each record into its own tenant's dedicated // ClickHouse database (enterprise/internal/chwriter) instead of the one @@ -21,7 +21,7 @@ // alongside) `ingest -mode=consumer` -- `ingest -mode=server` (the // agent-facing half, which tags records with a tenant_id via // TenantResolver) keeps running unchanged and unconditionally either -// way; only which process consumes sentry.logs.raw and where it writes +// way; only which process consumes cairnobs.logs.raw and where it writes // changes. package main diff --git a/enterprise/internal/apiconfig/apiconfig.go b/enterprise/internal/apiconfig/apiconfig.go index 38c498f..78abbc1 100644 --- a/enterprise/internal/apiconfig/apiconfig.go +++ b/enterprise/internal/apiconfig/apiconfig.go @@ -125,8 +125,8 @@ func Load() (Config, error) { }, Postgres: PostgresConfig{ Addr: getenv("POSTGRES_ADDR", "localhost:5432"), - Database: getenv("POSTGRES_DATABASE", "sentry_metadata"), - Username: getenv("POSTGRES_USERNAME", "sentry"), + Database: getenv("POSTGRES_DATABASE", "cairnobs_metadata"), + Username: getenv("POSTGRES_USERNAME", "cairnobs"), Password: getenv("POSTGRES_PASSWORD", ""), }, AI: AIConfig{ diff --git a/enterprise/internal/audit/audit.go b/enterprise/internal/audit/audit.go index 7e74629..fba3e46 100644 --- a/enterprise/internal/audit/audit.go +++ b/enterprise/internal/audit/audit.go @@ -3,11 +3,11 @@ // section. Two independent defenses back the "no update/delete path // from the application layer" requirement -- both verified against a // live Postgres, not just written: audit_writer (this package's own -// Postgres role, via its own connection pool, never the shared `sentry` +// Postgres role, via its own connection pool, never the shared `cairnobs` // role every other store uses) has only INSERT+SELECT grants, and a // BEFORE UPDATE OR DELETE trigger (metadata/migrations/0015-0016) // rejects the operation for *any* role, including the table owner -- -// confirmed live: even `sentry` cannot UPDATE a row without first +// confirmed live: even `cairnobs` cannot UPDATE a row without first // disabling the trigger, a privileged operation distinct from ordinary // application access. // diff --git a/enterprise/internal/audit/integration_test.go b/enterprise/internal/audit/integration_test.go index 7982d59..7d30315 100644 --- a/enterprise/internal/audit/integration_test.go +++ b/enterprise/internal/audit/integration_test.go @@ -34,7 +34,7 @@ func testPool(t *testing.T, user, password string) *pgxpool.Pool { if addr == "" { t.Skip("AUDIT_TEST_POSTGRES_ADDR not set -- skipping live-Postgres integration test") } - dsn := fmt.Sprintf("postgres://%s:%s@%s/sentry_metadata", user, password, addr) + dsn := fmt.Sprintf("postgres://%s:%s@%s/cairnobs_metadata", user, password, addr) pool, err := pgxpool.New(context.Background(), dsn) if err != nil { t.Fatalf("opening pool: %v", err) @@ -65,7 +65,7 @@ func cleanupAuditLog(t *testing.T, adminPool *pgxpool.Pool) { func TestAppendAndVerifyChainRealPostgres(t *testing.T) { writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD")) - adminPool := testPool(t, "sentry", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) + adminPool := testPool(t, "cairnobs", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) cleanupAuditLog(t, adminPool) defer cleanupAuditLog(t, adminPool) @@ -104,7 +104,7 @@ func TestAppendAndVerifyChainRealPostgres(t *testing.T) { // field on QueryAuditEntry -- matching that type's own doc comment. func TestQueryAPILoggerWritesAttributedToContextIdentity(t *testing.T) { writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD")) - adminPool := testPool(t, "sentry", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) + adminPool := testPool(t, "cairnobs", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) cleanupAuditLog(t, adminPool) defer cleanupAuditLog(t, adminPool) @@ -131,7 +131,7 @@ func TestQueryAPILoggerWritesAttributedToContextIdentity(t *testing.T) { func TestQueryAPILoggerRefusesWithoutIdentity(t *testing.T) { writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD")) - adminPool := testPool(t, "sentry", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) + adminPool := testPool(t, "cairnobs", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) cleanupAuditLog(t, adminPool) defer cleanupAuditLog(t, adminPool) @@ -149,7 +149,7 @@ func TestQueryAPILoggerRefusesWithoutIdentity(t *testing.T) { // confidence/accepted/edited fields that don't have dedicated columns. func TestAIInteractionLoggerWritesAttributedToContextIdentity(t *testing.T) { writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD")) - adminPool := testPool(t, "sentry", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) + adminPool := testPool(t, "cairnobs", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) cleanupAuditLog(t, adminPool) defer cleanupAuditLog(t, adminPool) @@ -199,7 +199,7 @@ func TestAIInteractionLoggerWritesAttributedToContextIdentity(t *testing.T) { func TestAIInteractionLoggerRefusesWithoutIdentity(t *testing.T) { writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD")) - adminPool := testPool(t, "sentry", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) + adminPool := testPool(t, "cairnobs", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) cleanupAuditLog(t, adminPool) defer cleanupAuditLog(t, adminPool) @@ -216,7 +216,7 @@ func TestAIInteractionLoggerRefusesWithoutIdentity(t *testing.T) { // would also pass. func TestVerifyChainDetectsTampering(t *testing.T) { writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD")) - adminPool := testPool(t, "sentry", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) + adminPool := testPool(t, "cairnobs", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) cleanupAuditLog(t, adminPool) defer cleanupAuditLog(t, adminPool) @@ -270,7 +270,7 @@ func TestVerifyChainDetectsTampering(t *testing.T) { // unit test of the Go code alone. func TestAppendConcurrentWritesProduceAValidChain(t *testing.T) { writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD")) - adminPool := testPool(t, "sentry", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) + adminPool := testPool(t, "cairnobs", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) cleanupAuditLog(t, adminPool) defer cleanupAuditLog(t, adminPool) @@ -316,7 +316,7 @@ func TestAppendConcurrentWritesProduceAValidChain(t *testing.T) { // the previous checkpoint's hash. func TestCheckpointerRun(t *testing.T) { writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD")) - adminPool := testPool(t, "sentry", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) + adminPool := testPool(t, "cairnobs", os.Getenv("AUDIT_TEST_ADMIN_PASSWORD")) cleanupAuditLog(t, adminPool) defer cleanupAuditLog(t, adminPool) diff --git a/enterprise/internal/chwriter/chwriter.go b/enterprise/internal/chwriter/chwriter.go index 6678506..64cf706 100644 --- a/enterprise/internal/chwriter/chwriter.go +++ b/enterprise/internal/chwriter/chwriter.go @@ -16,7 +16,7 @@ // chrunner.RunSQL resolves exactly one tenant per call from ctx (a // single request always belongs to one identity); WriteBatch resolves // per *record*, since one Kafka batch pulled off the shared -// sentry.logs.raw topic can freely mix records from many different +// cairnobs.logs.raw topic can freely mix records from many different // tenants -- see ingest/internal/grpcserver's doc comment for why // there's one shared topic, not topic-per-tenant. package chwriter diff --git a/enterprise/internal/config/config.go b/enterprise/internal/config/config.go index afe4ae1..b0a3cff 100644 --- a/enterprise/internal/config/config.go +++ b/enterprise/internal/config/config.go @@ -101,8 +101,8 @@ func Load() (Config, error) { HTTPListenAddr: getenv("HTTP_LISTEN_ADDR", ":8082"), Postgres: PostgresConfig{ Addr: getenv("POSTGRES_ADDR", "localhost:5432"), - Database: getenv("POSTGRES_DATABASE", "sentry_metadata"), - Username: getenv("POSTGRES_USERNAME", "sentry"), + Database: getenv("POSTGRES_DATABASE", "cairnobs_metadata"), + Username: getenv("POSTGRES_USERNAME", "cairnobs"), Password: getenv("POSTGRES_PASSWORD", ""), }, OIDC: OIDCConfig{ diff --git a/enterprise/internal/ingestconfig/ingestconfig.go b/enterprise/internal/ingestconfig/ingestconfig.go index 8bea8da..2c9a985 100644 --- a/enterprise/internal/ingestconfig/ingestconfig.go +++ b/enterprise/internal/ingestconfig/ingestconfig.go @@ -74,26 +74,26 @@ func Load() (Config, error) { ClickHouseAddr: getenv("CLICKHOUSE_ADDR", "localhost:9000"), Postgres: PostgresConfig{ Addr: getenv("POSTGRES_ADDR", "localhost:5432"), - Database: getenv("POSTGRES_DATABASE", "sentry_metadata"), - Username: getenv("POSTGRES_USERNAME", "sentry"), + Database: getenv("POSTGRES_DATABASE", "cairnobs_metadata"), + Username: getenv("POSTGRES_USERNAME", "cairnobs"), Password: getenv("POSTGRES_PASSWORD", ""), }, Redpanda: RedpandaConfig{ Brokers: strings.Split(getenv("REDPANDA_BROKERS", "localhost:9092"), ","), // Same default topic ingest/internal/config uses -- this - // binary reads the identical shared sentry.logs.raw topic + // binary reads the identical shared cairnobs.logs.raw topic // ingest/cmd/ingest's server half (agent-facing PushBatch) // produces onto; there's no per-tenant topic, see // ingest/internal/grpcserver's doc comment. - Topic: getenv("REDPANDA_TOPIC", "sentry.logs.raw"), + Topic: getenv("REDPANDA_TOPIC", "cairnobs.logs.raw"), // A distinct consumer group from ingest/cmd/ingest's own - // default ("sentry-ingest") -- this binary and a + // default ("cairnobs-ingest") -- this binary and a // single-tenant `ingest -mode=consumer` must never share a // group (each message would only ever reach one of them, // silently splitting traffic) even though in practice a // real multi-tenant deployment runs this binary *instead // of*, not alongside, `ingest -mode=consumer`. - ConsumerGroup: getenv("REDPANDA_CONSUMER_GROUP", "sentry-enterprise-ingest"), + ConsumerGroup: getenv("REDPANDA_CONSUMER_GROUP", "cairnobs-enterprise-ingest"), }, } diff --git a/enterprise/internal/rbacstore/rbacstore.go b/enterprise/internal/rbacstore/rbacstore.go index cac0747..01a5c0e 100644 --- a/enterprise/internal/rbacstore/rbacstore.go +++ b/enterprise/internal/rbacstore/rbacstore.go @@ -2,7 +2,7 @@ // role schema (metadata/migrations/0017-0021) described in // /docs/phase-4-rbac-design.md: users (global SSO identity), tenants, // and tenant_memberships (per-tenant role). It uses the same shared -// "sentry" Postgres role/pool every other metadata store does (unlike +// "cairnobs" Postgres role/pool every other metadata store does (unlike // enterprise/internal/audit's deliberately separate, narrower-granted // pool) -- ordinary read/write CRUD on control-plane config, not an // append-only ledger, so it has no analogous reason to restrict its own diff --git a/enterprise/internal/rbacstore/rbacstore_test.go b/enterprise/internal/rbacstore/rbacstore_test.go index 84a49d9..00061a5 100644 --- a/enterprise/internal/rbacstore/rbacstore_test.go +++ b/enterprise/internal/rbacstore/rbacstore_test.go @@ -29,7 +29,7 @@ func testStore(t *testing.T) *Store { t.Skip("RBACSTORE_TEST_POSTGRES_ADDR not set -- skipping live-Postgres integration test") } password := os.Getenv("RBACSTORE_TEST_POSTGRES_PASSWORD") - dsn := fmt.Sprintf("postgres://sentry:%s@%s/sentry_metadata", password, addr) + dsn := fmt.Sprintf("postgres://cairnobs:%s@%s/cairnobs_metadata", password, addr) pool, err := pgxpool.New(context.Background(), dsn) if err != nil { t.Fatalf("opening pool: %v", err) diff --git a/enterprise/internal/saml/saml_test.go b/enterprise/internal/saml/saml_test.go index cd22304..caf7738 100644 --- a/enterprise/internal/saml/saml_test.go +++ b/enterprise/internal/saml/saml_test.go @@ -27,7 +27,7 @@ func TestNewRejectsMissingConfig(t *testing.T) { } func TestNewRejectsMissingIDPMetadata(t *testing.T) { - _, err := New(Config{EntityID: "https://sentry.example.com/saml/metadata", ACSURL: "https://sentry.example.com/saml/acs"}) + _, err := New(Config{EntityID: "https://cairnobs.example.com/saml/metadata", ACSURL: "https://cairnobs.example.com/saml/acs"}) if err == nil { t.Fatalf("expected an error when IDPMetadata is missing") } @@ -40,8 +40,8 @@ func TestNewRejectsMissingIDPMetadata(t *testing.T) { // redirect rather than just "the code compiles." func TestLoginURLBuildsAgainstRealIDPMetadata(t *testing.T) { sp, err := New(Config{ - EntityID: "https://sentry.example.com/saml/metadata", - ACSURL: "https://sentry.example.com/saml/acs", + EntityID: "https://cairnobs.example.com/saml/metadata", + ACSURL: "https://cairnobs.example.com/saml/acs", IDPMetadata: fakeIDPMetadata(), }) if err != nil { diff --git a/hack/alert-load-test/README.md b/hack/alert-load-test/README.md index a709914..f6c798d 100644 --- a/hack/alert-load-test/README.md +++ b/hack/alert-load-test/README.md @@ -14,7 +14,7 @@ go run . --count 500000 # 2. Run a webhook-sink so the (never-firing, by design) rules have a # valid notification target to point at: -docker run -d --name sentry-webhook-sink --network sentry_default \ +docker run -d --name cairnobs-webhook-sink --network sentry_default \ -p 9099:9099 -v $(pwd)/../webhook-sink:/src -w /src golang:1.25-alpine go run . # 3. Run the load test: diff --git a/hack/alert-load-test/main.go b/hack/alert-load-test/main.go index 5b847c0..863fcdd 100644 --- a/hack/alert-load-test/main.go +++ b/hack/alert-load-test/main.go @@ -43,7 +43,7 @@ var ( pollInterval = flag.Duration("poll-interval", 5*time.Second, "how often to poll GET /rules while observing") concurrency = flag.Int("concurrency", 20, "concurrent rule-creation requests") skipCleanup = flag.Bool("no-cleanup", false, "leave the seeded rules/target in place after the run") - webhookURL = flag.String("webhook-url", "http://sentry-webhook-sink:9099/", "notification target URL -- default assumes a webhook-sink container reachable on the compose network") + webhookURL = flag.String("webhook-url", "http://cairnobs-webhook-sink:9099/", "notification target URL -- default assumes a webhook-sink container reachable on the compose network") ) func main() { diff --git a/ingest/Dockerfile b/ingest/Dockerfile index 9612b39..2d76bef 100644 --- a/ingest/Dockerfile +++ b/ingest/Dockerfile @@ -1,6 +1,6 @@ # Build context must be the repo root (sentry/), not ingest/, since this # needs both ingest/ and proto/: -# docker build -f ingest/Dockerfile -t sentry-ingest . +# docker build -f ingest/Dockerfile -t cairnobs-ingest . FROM golang:1.25-alpine AS builder WORKDIR /src diff --git a/ingest/README.md b/ingest/README.md index 914c09e..edaf1e4 100644 --- a/ingest/README.md +++ b/ingest/README.md @@ -86,13 +86,13 @@ full list and defaults) — no config file format for Phase 0: | Var | Default | Purpose | |---|---|---| | `GRPC_LISTEN_ADDR` | `:4317` | Agent-facing gRPC listen address | -| `TLS_CERT_FILE` / `TLS_KEY_FILE` | `/etc/sentry-ingest/server{,-key}.pem` | ingest's own mTLS identity | -| `TLS_CLIENT_CA_FILE` | `/etc/sentry-ingest/ca.pem` | CA used to verify agent client certs | +| `TLS_CERT_FILE` / `TLS_KEY_FILE` | `/etc/cairnobs-ingest/server{,-key}.pem` | ingest's own mTLS identity | +| `TLS_CLIENT_CA_FILE` | `/etc/cairnobs-ingest/ca.pem` | CA used to verify agent client certs | | `REDPANDA_BROKERS` | `localhost:9092` | Comma-separated broker list | -| `REDPANDA_TOPIC` | `sentry.logs.raw` | Must match the topic provisioned in `/transport` | -| `REDPANDA_CONSUMER_GROUP` | `sentry-ingest` | Consumer group id | +| `REDPANDA_TOPIC` | `cairnobs.logs.raw` | Must match the topic provisioned in `/transport` | +| `REDPANDA_CONSUMER_GROUP` | `cairnobs-ingest` | Consumer group id | | `CLICKHOUSE_ADDR` | `localhost:9000` | Native protocol port, not HTTP | -| `CLICKHOUSE_DATABASE` / `_USERNAME` / `_PASSWORD` | `sentry` / `default` / `` | | +| `CLICKHOUSE_DATABASE` / `_USERNAME` / `_PASSWORD` | `cairnobs` / `default` / `` | | | `CONSUMER_BATCH_MAX_SIZE` | `500` | Records per ClickHouse batch insert | | `CONSUMER_BATCH_FLUSH_INTERVAL_MS` | `2000` | Max time a partial batch waits before flushing | | `ENTERPRISE_AUTH_URL` | (empty) | Enables `internal/grpcserver.TenantResolver` -- empty means PushBatch never requires a bearer credential and no `tenant_id` header is ever attached, same as every Phase 0-3 deployment | @@ -108,12 +108,12 @@ go test ./... Requires `google.golang.org/protobuf/cmd/protoc-gen-go` and `google.golang.org/grpc/cmd/protoc-gen-go-grpc` only if you're regenerating `/proto`'s Go bindings — ingest itself just imports the -already-generated `github.com/sentry/sentry/proto` module (see the +already-generated `github.com/cairnobs/cairnobs/proto` module (see the `replace` directive in `go.mod`, pointing at `../proto`). ```sh # from the repo root, not ingest/ -docker build -f ingest/Dockerfile -t sentry-ingest . +docker build -f ingest/Dockerfile -t cairnobs-ingest . ``` ## Testing notes diff --git a/ingest/internal/agentregistry/agentregistry.go b/ingest/internal/agentregistry/agentregistry.go index cba25da..392f4cc 100644 --- a/ingest/internal/agentregistry/agentregistry.go +++ b/ingest/internal/agentregistry/agentregistry.go @@ -1,7 +1,7 @@ // Package agentregistry is the Postgres-backed implementation of // grpcserver.AgentRegistry -- ingest's half of agent inventory/remote // config (see /docs/agent-management-design.md). Writes into the same -// sentry_metadata Postgres api reads/writes from for the web UI's +// cairnobs_metadata Postgres api reads/writes from for the web UI's // inventory and edit-config views (api/agents), the same shared-schema- // different-services shape alerting and api already use for dashboards/ // alert_rules. diff --git a/ingest/internal/config/config.go b/ingest/internal/config/config.go index b52ee27..bd205d7 100644 --- a/ingest/internal/config/config.go +++ b/ingest/internal/config/config.go @@ -26,8 +26,8 @@ type Config struct { // AgentRegistry enables agent inventory/remote config // (internal/agentregistry, internal/grpcserver.AgentRegistry) when // Postgres.Addr is set -- same "off unless configured" shape as - // EnterpriseAuthURL above. Writes into the same sentry_metadata - // database api/web already use, via the same shared "sentry" role + // EnterpriseAuthURL above. Writes into the same cairnobs_metadata + // database api/web already use, via the same shared "cairnobs" role // every other non-audit table in this schema uses (unlike // audit_log's dedicated restricted role -- agent inventory carries // no tamper-evidence requirement). @@ -110,12 +110,12 @@ func Load() (Config, error) { }, Redpanda: RedpandaConfig{ Brokers: strings.Split(getenv("REDPANDA_BROKERS", "localhost:9092"), ","), - Topic: getenv("REDPANDA_TOPIC", "sentry.logs.raw"), - ConsumerGroup: getenv("REDPANDA_CONSUMER_GROUP", "sentry-ingest"), + Topic: getenv("REDPANDA_TOPIC", "cairnobs.logs.raw"), + ConsumerGroup: getenv("REDPANDA_CONSUMER_GROUP", "cairnobs-ingest"), }, ClickHouse: ClickHouseConfig{ Addr: getenv("CLICKHOUSE_ADDR", "localhost:9000"), - Database: getenv("CLICKHOUSE_DATABASE", "sentry"), + Database: getenv("CLICKHOUSE_DATABASE", "cairnobs"), Username: getenv("CLICKHOUSE_USERNAME", "default"), Password: getenv("CLICKHOUSE_PASSWORD", ""), }, @@ -123,8 +123,8 @@ func Load() (Config, error) { AgentRegistry: AgentRegistryConfig{ Postgres: PostgresConfig{ Addr: getenv("AGENT_REGISTRY_POSTGRES_ADDR", ""), - Database: getenv("AGENT_REGISTRY_POSTGRES_DATABASE", "sentry_metadata"), - Username: getenv("AGENT_REGISTRY_POSTGRES_USERNAME", "sentry"), + Database: getenv("AGENT_REGISTRY_POSTGRES_DATABASE", "cairnobs_metadata"), + Username: getenv("AGENT_REGISTRY_POSTGRES_USERNAME", "cairnobs"), Password: getenv("AGENT_REGISTRY_POSTGRES_PASSWORD", ""), }, }, diff --git a/ingest/internal/config/config_test.go b/ingest/internal/config/config_test.go index 63a6f2c..d54f8f6 100644 --- a/ingest/internal/config/config_test.go +++ b/ingest/internal/config/config_test.go @@ -10,8 +10,8 @@ func TestLoadDefaults(t *testing.T) { if cfg.GRPC.ListenAddr != ":4317" { t.Errorf("GRPC.ListenAddr = %q, want :4317", cfg.GRPC.ListenAddr) } - if cfg.Redpanda.Topic != "sentry.logs.raw" { - t.Errorf("Redpanda.Topic = %q, want sentry.logs.raw", cfg.Redpanda.Topic) + if cfg.Redpanda.Topic != "cairnobs.logs.raw" { + t.Errorf("Redpanda.Topic = %q, want cairnobs.logs.raw", cfg.Redpanda.Topic) } if cfg.Batch.MaxSize != 500 { t.Errorf("Batch.MaxSize = %d, want 500", cfg.Batch.MaxSize) diff --git a/metadata/Dockerfile b/metadata/Dockerfile index d6be65a..461e208 100644 --- a/metadata/Dockerfile +++ b/metadata/Dockerfile @@ -1,6 +1,6 @@ # One-shot migration runner: bash + psql client baked in, migrations/*.sql # copied in at build time. Mirrors /storage/Dockerfile's shape. -# docker build -f metadata/Dockerfile -t sentry-metadata-migrate metadata/ +# docker build -f metadata/Dockerfile -t cairnobs-metadata-migrate metadata/ FROM alpine:3.20 RUN apk add --no-cache bash postgresql16-client WORKDIR /metadata diff --git a/metadata/README.md b/metadata/README.md index 2a727cd..6ddff1c 100644 --- a/metadata/README.md +++ b/metadata/README.md @@ -11,14 +11,14 @@ doesn't provide. ## Schema -Seven tables across three features, one shared database (`sentry_metadata`): +Seven tables across three features, one shared database (`cairnobs_metadata`): - `dashboards`, `dashboard_panels` — owned by `/api` (`api/internal/dashboards`) - `notification_targets`, `alert_rules`, `alert_state`, `delivery_log` — owned by `/alerting` - `audit_log` — owned by `enterprise/internal/audit` (Phase 4). Unlike every other table here, this one is **not** written through the shared - `sentry` role/pool — see "The `audit_writer` role" below. + `cairnobs` role/pool — see "The `audit_writer` role" below. "Owned" here is a documentation convention, not a technical boundary — both services connect to the same Postgres instance/database, each with @@ -39,13 +39,13 @@ written: with **only** `INSERT`/`SELECT` grants on `audit_log` — no `UPDATE`/`DELETE`/`TRUNCATE`, ever. `enterprise/internal/audit.Store` connects using this role's credentials via its **own** `pgxpool.Pool`, - never the shared `sentry` pool `api`/`alerting`'s other stores use — + never the shared `cairnobs` pool `api`/`alerting`'s other stores use — reusing the shared pool for audit writes would give audit_log's application-level credential the same `UPDATE`/`DELETE` grants every other metadata table has, silently defeating the whole point. 2. A `BEFORE UPDATE OR DELETE` trigger (`migrations/0015`-`0016`) that rejects the operation for **any** role, including the table owner - (`sentry`) — confirmed live: even `sentry` needs to explicitly + (`cairnobs`) — confirmed live: even `cairnobs` needs to explicitly `ALTER TABLE audit_log DISABLE TRIGGER audit_log_immutable` (a privileged, distinct-from-normal-access operation) before it can modify a row. This is redundant defense-in-depth independent of the @@ -89,9 +89,9 @@ Environment variables `migrate.sh` reads (all optional except |---|---| | `POSTGRES_HOST` | `localhost` | | `POSTGRES_PORT` | `5432` | -| `POSTGRES_USER` | `sentry` | +| `POSTGRES_USER` | `cairnobs` | | `POSTGRES_PASSWORD` | (empty — must be set) | -| `POSTGRES_DATABASE` | `sentry_metadata` | +| `POSTGRES_DATABASE` | `cairnobs_metadata` | | `AUDIT_WRITER_PASSWORD` | `audit-writer-dev-only` | The database itself isn't created by `migrate.sh` — the `postgres:16-alpine` diff --git a/metadata/docker-compose.yml b/metadata/docker-compose.yml index 52eae6a..15c27e6 100644 --- a/metadata/docker-compose.yml +++ b/metadata/docker-compose.yml @@ -9,8 +9,8 @@ services: ports: - "5432:5432" environment: - POSTGRES_DB: sentry_metadata - POSTGRES_USER: sentry + POSTGRES_DB: cairnobs_metadata + POSTGRES_USER: cairnobs POSTGRES_PASSWORD: "cairnobs-dev-only" # not a real secret, see root docker-compose.yml volumes: - metadata-postgres-data:/var/lib/postgresql/data diff --git a/metadata/migrate.sh b/metadata/migrate.sh index 93b8d09..6a40529 100644 --- a/metadata/migrate.sh +++ b/metadata/migrate.sh @@ -8,9 +8,9 @@ set -euo pipefail POSTGRES_HOST="${POSTGRES_HOST:-localhost}" POSTGRES_PORT="${POSTGRES_PORT:-5432}" -POSTGRES_USER="${POSTGRES_USER:-sentry}" +POSTGRES_USER="${POSTGRES_USER:-cairnobs}" POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}" -POSTGRES_DATABASE="${POSTGRES_DATABASE:-sentry_metadata}" +POSTGRES_DATABASE="${POSTGRES_DATABASE:-cairnobs_metadata}" # Password for the restricted audit-log-writer Postgres role (Phase 4 # task 4, see /docs/phase-4-isolation-design.md's audit logging # section) -- a second, narrower-granted role, not the shared diff --git a/search/README.md b/search/README.md index 59d949d..cb59f5e 100644 --- a/search/README.md +++ b/search/README.md @@ -18,7 +18,7 @@ planning. ## How it fits together ``` -ingest (gRPC front end) --> Redpanda (sentry.logs.raw) --> ingest's ClickHouse-writer consumer --> ClickHouse +ingest (gRPC front end) --> Redpanda (cairnobs.logs.raw) --> ingest's ClickHouse-writer consumer --> ClickHouse \ `--> search's own consumer --> Tantivy index ``` @@ -121,7 +121,7 @@ Environment variables (see `src/config.rs`): |---|---|---| | `GRPC_LISTEN_ADDR` | `0.0.0.0:50052` | Full socket address — Rust's parser needs one, unlike Go's `:PORT` shorthand `ingest`/`api` use | | `REDPANDA_BROKERS` | `localhost:9092` | Comma-separated broker list | -| `REDPANDA_TOPIC` | `sentry.logs.raw` | Must match `/ingest`'s topic | +| `REDPANDA_TOPIC` | `cairnobs.logs.raw` | Must match `/ingest`'s topic | | `REDPANDA_TOPIC_PARTITIONS` | `6` | Must match what `/transport/provision-topics.sh` created | | `INDEX_PATH` | `/var/lib/cairnobs-search/index` | Default (non-tenant) Tantivy index directory | | `TENANTS_INDEX_PATH` | `/var/lib/cairnobs-search/tenants` | Per-tenant index directories live under here, one subdirectory per tenant_id (Phase 4) | diff --git a/search/src/config.rs b/search/src/config.rs index f6f9487..70af6e4 100644 --- a/search/src/config.rs +++ b/search/src/config.rs @@ -67,7 +67,7 @@ impl Config { .split(',') .map(str::to_string) .collect(), - redpanda_topic: getenv("REDPANDA_TOPIC", "sentry.logs.raw"), + redpanda_topic: getenv("REDPANDA_TOPIC", "cairnobs.logs.raw"), index_path: PathBuf::from(getenv("INDEX_PATH", "/var/lib/cairnobs-search/index")), offsets_path: PathBuf::from(getenv( "OFFSETS_PATH", diff --git a/search/src/consumer.rs b/search/src/consumer.rs index 29fe122..56c6814 100644 --- a/search/src/consumer.rs +++ b/search/src/consumer.rs @@ -23,7 +23,7 @@ use crate::tenants::ActiveTenantTracker; /// `TestTenantIDHeaderKeyConstantsMatch` does on the Go side. const TENANT_ID_HEADER_KEY: &str = "tenant_id"; -/// Reads the same `sentry.logs.raw` topic ingest's ClickHouse-writer +/// Reads the same `cairnobs.logs.raw` topic ingest's ClickHouse-writer /// consumer reads, as an independent consumer group in spirit (its own /// offset tracking, own failure domain) even though rskafka doesn't speak /// Kafka's broker-side consumer-group protocol -- see offsets.rs. One diff --git a/storage/Dockerfile b/storage/Dockerfile index 1dbfde2..71cf257 100644 --- a/storage/Dockerfile +++ b/storage/Dockerfile @@ -1,7 +1,7 @@ # One-shot migration runner: bash + curl baked in, migrations/*.sql copied # in at build time. No runtime package install and no host volume mount # needed — works offline once built. -# docker build -f storage/Dockerfile -t sentry-clickhouse-migrate storage/ +# docker build -f storage/Dockerfile -t cairnobs-clickhouse-migrate storage/ FROM alpine:3.20 RUN apk add --no-cache bash curl WORKDIR /storage diff --git a/storage/README.md b/storage/README.md index 9bea3bc..4b8030a 100644 --- a/storage/README.md +++ b/storage/README.md @@ -100,7 +100,7 @@ defaults so the two stay in sync out of the box): | `CLICKHOUSE_HTTP` | `http://localhost:8123` | | `CLICKHOUSE_USER` | `default` | | `CLICKHOUSE_PASSWORD` | (empty — override, see above) | -| `CLICKHOUSE_DATABASE` | `sentry` | +| `CLICKHOUSE_DATABASE` | `cairnobs` | There's also a `Dockerfile` (bash + curl baked in, `migrations/` copied in at build time) used by the root-level `docker-compose.yml` as a one-shot diff --git a/storage/migrate.sh b/storage/migrate.sh index 155071f..cf601e3 100755 --- a/storage/migrate.sh +++ b/storage/migrate.sh @@ -13,7 +13,7 @@ set -euo pipefail CLICKHOUSE_HTTP="${CLICKHOUSE_HTTP:-http://localhost:8123}" CLICKHOUSE_USER="${CLICKHOUSE_USER:-default}" CLICKHOUSE_PASSWORD="${CLICKHOUSE_PASSWORD:-}" -DATABASE="${CLICKHOUSE_DATABASE:-sentry}" +DATABASE="${CLICKHOUSE_DATABASE:-cairnobs}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MIGRATIONS_DIR="${SCRIPT_DIR}/migrations" diff --git a/transport/README.md b/transport/README.md index 830fb15..36d192a 100644 --- a/transport/README.md +++ b/transport/README.md @@ -5,7 +5,7 @@ Redpanda for local development, plus the script that provisions the topic ## Topic naming contract -`ingest` defaults to `REDPANDA_TOPIC=sentry.logs.raw` (see +`ingest` defaults to `REDPANDA_TOPIC=cairnobs.logs.raw` (see `/ingest/internal/config`). `provision-topics.sh` defaults to the same name. These aren't wired together automatically — if you change one, change the other, or override `REDPANDA_TOPIC` consistently wherever diff --git a/transport/provision-topics.sh b/transport/provision-topics.sh index a7910c8..18f816e 100755 --- a/transport/provision-topics.sh +++ b/transport/provision-topics.sh @@ -17,7 +17,7 @@ set -euo pipefail # of ever becoming healthy. BROKERS="${REDPANDA_BROKERS:-localhost:9092}" ADMIN_HOSTS="${REDPANDA_ADMIN_HOSTS:-localhost:9644}" -TOPIC="${REDPANDA_TOPIC:-sentry.logs.raw}" +TOPIC="${REDPANDA_TOPIC:-cairnobs.logs.raw}" PARTITIONS="${REDPANDA_TOPIC_PARTITIONS:-6}" echo "Waiting for Redpanda admin API at ${ADMIN_HOSTS}..." diff --git a/web/src/lib/assets/favicon.svg b/web/src/lib/assets/favicon.svg index 2f6a1e8..4462b29 100644 --- a/web/src/lib/assets/favicon.svg +++ b/web/src/lib/assets/favicon.svg @@ -2,8 +2,8 @@ - - - + + + diff --git a/web/src/lib/assets/logo-horizontal-dark.svg b/web/src/lib/assets/logo-horizontal-dark.svg index f822879..0fa3526 100644 --- a/web/src/lib/assets/logo-horizontal-dark.svg +++ b/web/src/lib/assets/logo-horizontal-dark.svg @@ -1,10 +1,10 @@ - - - - + + + + cairn obs diff --git a/web/src/lib/assets/logo-horizontal-light.svg b/web/src/lib/assets/logo-horizontal-light.svg new file mode 100644 index 0000000..8dcd73a --- /dev/null +++ b/web/src/lib/assets/logo-horizontal-light.svg @@ -0,0 +1,12 @@ + + + + + + + + + cairn + obs + + diff --git a/web/src/lib/assets/logo-stacked-dark.svg b/web/src/lib/assets/logo-stacked-dark.svg index 3ba5c74..5a14136 100644 --- a/web/src/lib/assets/logo-stacked-dark.svg +++ b/web/src/lib/assets/logo-stacked-dark.svg @@ -1,10 +1,10 @@ - - - - + + + + cairn obs diff --git a/web/src/lib/assets/logo-stacked-light.svg b/web/src/lib/assets/logo-stacked-light.svg new file mode 100644 index 0000000..b187a8f --- /dev/null +++ b/web/src/lib/assets/logo-stacked-light.svg @@ -0,0 +1,10 @@ + + + + + + + + + cairn obs + diff --git a/web/src/lib/components/NavSidebar.svelte b/web/src/lib/components/NavSidebar.svelte index a92a549..446fc16 100644 --- a/web/src/lib/components/NavSidebar.svelte +++ b/web/src/lib/components/NavSidebar.svelte @@ -9,9 +9,10 @@ type CurrentSession, type LocalSession } from '$lib/api'; - import { getTheme, setTheme, type Theme } from '$lib/theme.svelte'; + import { getTheme, setTheme, isLight, type Theme } from '$lib/theme.svelte'; import { getDensity, toggleDensity } from '$lib/density.svelte'; - import logo from '$lib/assets/logo-horizontal-dark.svg'; + import logoDark from '$lib/assets/logo-horizontal-dark.svg'; + import logoLight from '$lib/assets/logo-horizontal-light.svg'; let { onOpenPalette, @@ -84,7 +85,7 @@
- +
diff --git a/web/src/lib/theme.svelte.ts b/web/src/lib/theme.svelte.ts index 900eef0..0504b4b 100644 --- a/web/src/lib/theme.svelte.ts +++ b/web/src/lib/theme.svelte.ts @@ -34,10 +34,33 @@ const initial = readStored(); let theme = $state(initial); apply(initial); +// Tracks the OS preference live (not just at load) so "system" stays +// accurate across a theme change the user makes outside the app, same +// as app.css/tokens.css's own `prefers-color-scheme` media query does +// automatically for CSS -- anything in JS that needs to know "is the UI +// actually light right now" (e.g. picking a light/dark logo asset) has +// to track this the same way, or it drifts from what's on screen. +let prefersLight = $state( + typeof window !== 'undefined' ? window.matchMedia('(prefers-color-scheme: light)').matches : false +); +if (typeof window !== 'undefined') { + window + .matchMedia('(prefers-color-scheme: light)') + .addEventListener('change', (e) => (prefersLight = e.matches)); +} + export function getTheme(): Theme { return theme; } +// Whether the rendered UI is in light mode right now -- explicit +// "light", or "system" while the OS itself prefers light. Mirrors +// app.css's `html[data-theme='light']` / `prefers-color-scheme: light` +// precedence exactly. +export function isLight(): boolean { + return theme === 'light' || (theme === 'system' && prefersLight); +} + export function setTheme(t: Theme) { theme = t; apply(t); diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 5353a38..3829e79 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -3,7 +3,9 @@ // /search (the old root; every prior link/shortcut/drill-down that // used to point at "/" expecting the query page now points at // "/search" explicitly, see NavSidebar/CommandPalette/drilldown.ts). - import logo from '$lib/assets/logo-stacked-dark.svg'; + import logoDark from '$lib/assets/logo-stacked-dark.svg'; + import logoLight from '$lib/assets/logo-stacked-light.svg'; + import { isLight } from '$lib/theme.svelte'; import { Button } from '$lib/components/ui'; const shortcuts: { href: string; label: string; hint: string }[] = [ @@ -15,7 +17,7 @@
- +

One query bar for filter/stats queries and free-text search across every host and service you're shipping logs from. diff --git a/web/static/icons/favicon-16.png b/web/static/icons/favicon-16.png index a6c81ae..b2c751f 100644 Binary files a/web/static/icons/favicon-16.png and b/web/static/icons/favicon-16.png differ diff --git a/web/static/icons/favicon-180.png b/web/static/icons/favicon-180.png index f09003e..36b590b 100644 Binary files a/web/static/icons/favicon-180.png and b/web/static/icons/favicon-180.png differ diff --git a/web/static/icons/favicon-32.png b/web/static/icons/favicon-32.png index dad619d..1b9499e 100644 Binary files a/web/static/icons/favicon-32.png and b/web/static/icons/favicon-32.png differ diff --git a/web/static/icons/favicon-48.png b/web/static/icons/favicon-48.png index d5f5d8c..37cf2ff 100644 Binary files a/web/static/icons/favicon-48.png and b/web/static/icons/favicon-48.png differ diff --git a/web/static/icons/favicon-512.png b/web/static/icons/favicon-512.png index de3994f..0d389a1 100644 Binary files a/web/static/icons/favicon-512.png and b/web/static/icons/favicon-512.png differ