Finish the Cairn OBS rename through services, docs, and assets
The rename commit before this one covered module paths and the obvious user-facing strings; this is the rest of it -- the places where "sentry" was a default value, a filename, or a picture rather than a word in a sentence. Defaults that changed: CLICKHOUSE_DATABASE (sentry -> cairnobs), POSTGRES_DATABASE (sentry_metadata -> cairnobs_metadata), and POSTGRES_USERNAME (sentry -> cairnobs), across api/alerting/ingest and the enterprise binaries, plus the compose files and migrate scripts that create those objects. These are *defaults*, so a deployment that sets them explicitly is unaffected -- but any deployment relying on the old defaults must have its environment updated before it picks this up, or it will come up pointing at a database that doesn't exist. Also: the light-mode logo variants (the dark ones existed alone, so the landing page and sidebar rendered a dark mark on a light background), regenerated favicons, and the docs/README/threat-model prose that still said Sentry.
@@ -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 . .
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:<port>). 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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 <host>
|
||||
sentryctl agents config get <host>|clear <host>
|
||||
sentryctl agents config set <host> [--batch-max-size N] [--batch-flush-interval-ms N]
|
||||
cairnobsctl agents list|get <host>
|
||||
cairnobsctl agents config get <host>|clear <host>
|
||||
cairnobsctl agents config set <host> [--batch-max-size N] [--batch-flush-interval-ms N]
|
||||
[--heartbeat-enabled true|false] [--heartbeat-interval-ms N]
|
||||
[--journald-unit UNIT]
|
||||
sentryctl agents restart <host> [--yes]
|
||||
cairnobsctl agents restart <host> [--yes]
|
||||
```
|
||||
|
||||
`config set` is the one command with real logic beyond a thin HTTP
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)*
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 . .
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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 . .
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 . .
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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", ""),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}..."
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<!-- Favicon: simplified 3-stone mark on terminal-dark rounded square -->
|
||||
<rect x="0" y="0" width="64" height="64" rx="14" fill="#0D1117"/>
|
||||
<g>
|
||||
<rect x="12" y="38" width="40" height="13" rx="6.5" fill="#FF8A00" transform="rotate(-2 32 44.5)"/>
|
||||
<rect x="18" y="24" width="28" height="12" rx="6" fill="#FF9D2E" transform="rotate(3 32 30)"/>
|
||||
<rect x="23" y="12" width="18" height="10" rx="5" fill="#FFB157" transform="rotate(-3 32 17)"/>
|
||||
<path d="M14,50 L10,45 L13,40 L22,37 L34,40 L44,37 L53,41 L50,47 L40,51 L26,48 Z" fill="#FF8A00"/>
|
||||
<path d="M20,35 L16,30 L19,25 L28,23 L36,26 L44,23 L47,28 L43,34 L32,36 L24,33 Z" fill="#FF9D2E"/>
|
||||
<path d="M24,21 L21,16 L25,12 L32,11 L38,13 L41,17 L38,21 L31,22 L27,20 Z" fill="#FFB157"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 551 B After Width: | Height: | Size: 550 B |
@@ -1,10 +1,10 @@
|
||||
<svg width="600" height="140" viewBox="0 0 600 140" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Horizontal lockup for dark backgrounds -->
|
||||
<g transform="translate(20,10) scale(0.6)">
|
||||
<rect x="30" y="142" width="140" height="42" rx="20" fill="#E6690E" transform="rotate(-1.5 100 163)"/>
|
||||
<rect x="48" y="106" width="104" height="38" rx="18" fill="#FF8A00" transform="rotate(2.5 100 125)"/>
|
||||
<rect x="64" y="73" width="72" height="34" rx="16" fill="#FF9D2E" transform="rotate(-3 100 90)"/>
|
||||
<rect x="78" y="43" width="44" height="30" rx="14" fill="#FFB157" transform="rotate(3.5 100 58)"/>
|
||||
<path d="M32,184 L20,172 L26,155 L18,146 L46,138 L82,144 L112,134 L150,140 L176,150 L182,168 L170,184 L142,192 L100,182 L64,190 Z" fill="#E6690E"/>
|
||||
<path d="M54,144 L42,134 L50,118 L44,108 L68,100 L96,106 L118,98 L142,106 L156,116 L150,132 L160,142 L132,150 L100,140 L70,148 Z" fill="#FF8A00"/>
|
||||
<path d="M68,106 L58,96 L64,82 L60,72 L80,66 L100,72 L116,64 L132,74 L138,86 L130,98 L138,106 L112,112 L88,104 L72,110 Z" fill="#FF9D2E"/>
|
||||
<path d="M80,70 L72,60 L78,48 L74,40 L92,36 L106,42 L116,34 L126,44 L124,56 L130,66 L108,72 L92,64 L82,70 Z" fill="#FFB157"/>
|
||||
</g>
|
||||
<text x="160" y="87" font-family="'JetBrains Mono','Fira Code',ui-monospace,monospace" font-size="48" font-weight="700" fill="#F2F2F2" letter-spacing="-1">cairn</text>
|
||||
<text x="342" y="87" font-family="'JetBrains Mono','Fira Code',ui-monospace,monospace" font-size="48" font-weight="700" fill="#FF8A00" letter-spacing="1"> obs</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 1017 B After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg width="600" height="140" viewBox="0 0 600 140" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Horizontal lockup for light backgrounds -->
|
||||
<g transform="translate(20,10) scale(0.6)">
|
||||
<path d="M32,184 L20,172 L26,155 L18,146 L46,138 L82,144 L112,134 L150,140 L176,150 L182,168 L170,184 L142,192 L100,182 L64,190 Z" fill="#E6690E"/>
|
||||
<path d="M54,144 L42,134 L50,118 L44,108 L68,100 L96,106 L118,98 L142,106 L156,116 L150,132 L160,142 L132,150 L100,140 L70,148 Z" fill="#FF8A00"/>
|
||||
<path d="M68,106 L58,96 L64,82 L60,72 L80,66 L100,72 L116,64 L132,74 L138,86 L130,98 L138,106 L112,112 L88,104 L72,110 Z" fill="#FF9D2E"/>
|
||||
<path d="M80,70 L72,60 L78,48 L74,40 L92,36 L106,42 L116,34 L126,44 L124,56 L130,66 L108,72 L92,64 L82,70 Z" fill="#FFB157"/>
|
||||
</g>
|
||||
<text x="160" y="87" font-family="'JetBrains Mono','Fira Code',ui-monospace,monospace" font-size="48" font-weight="700" fill="#111315" letter-spacing="-1">cairn</text>
|
||||
<text x="342" y="87" font-family="'JetBrains Mono','Fira Code',ui-monospace,monospace" font-size="48" font-weight="700" fill="#E6690E" letter-spacing="1"> obs</text>
|
||||
<rect x="540" y="49" width="14" height="38" fill="#E6690E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg width="360" height="360" viewBox="0 0 360 360" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Stacked lockup for dark backgrounds / square placements -->
|
||||
<g transform="translate(80,30) scale(1.0)">
|
||||
<rect x="30" y="142" width="140" height="42" rx="20" fill="#E6690E" transform="rotate(-1.5 100 163)"/>
|
||||
<rect x="48" y="106" width="104" height="38" rx="18" fill="#FF8A00" transform="rotate(2.5 100 125)"/>
|
||||
<rect x="64" y="73" width="72" height="34" rx="16" fill="#FF9D2E" transform="rotate(-3 100 90)"/>
|
||||
<rect x="78" y="43" width="44" height="30" rx="14" fill="#FFB157" transform="rotate(3.5 100 58)"/>
|
||||
<path d="M32,184 L20,172 L26,155 L18,146 L46,138 L82,144 L112,134 L150,140 L176,150 L182,168 L170,184 L142,192 L100,182 L64,190 Z" fill="#E6690E"/>
|
||||
<path d="M54,144 L42,134 L50,118 L44,108 L68,100 L96,106 L118,98 L142,106 L156,116 L150,132 L160,142 L132,150 L100,140 L70,148 Z" fill="#FF8A00"/>
|
||||
<path d="M68,106 L58,96 L64,82 L60,72 L80,66 L100,72 L116,64 L132,74 L138,86 L130,98 L138,106 L112,112 L88,104 L72,110 Z" fill="#FF9D2E"/>
|
||||
<path d="M80,70 L72,60 L78,48 L74,40 L92,36 L106,42 L116,34 L126,44 L124,56 L130,66 L108,72 L92,64 L82,70 Z" fill="#FFB157"/>
|
||||
</g>
|
||||
<text x="180" y="290" font-family="'JetBrains Mono','Fira Code',ui-monospace,monospace" font-size="42" font-weight="700" fill="#F2F2F2" letter-spacing="-1" text-anchor="middle">cairn <tspan fill="#FF8A00">obs</tspan></text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 859 B After Width: | Height: | Size: 1017 B |
@@ -0,0 +1,10 @@
|
||||
<svg width="360" height="360" viewBox="0 0 360 360" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Stacked lockup for light backgrounds / square placements -->
|
||||
<g transform="translate(80,30) scale(1.0)">
|
||||
<path d="M32,184 L20,172 L26,155 L18,146 L46,138 L82,144 L112,134 L150,140 L176,150 L182,168 L170,184 L142,192 L100,182 L64,190 Z" fill="#E6690E"/>
|
||||
<path d="M54,144 L42,134 L50,118 L44,108 L68,100 L96,106 L118,98 L142,106 L156,116 L150,132 L160,142 L132,150 L100,140 L70,148 Z" fill="#FF8A00"/>
|
||||
<path d="M68,106 L58,96 L64,82 L60,72 L80,66 L100,72 L116,64 L132,74 L138,86 L130,98 L138,106 L112,112 L88,104 L72,110 Z" fill="#FF9D2E"/>
|
||||
<path d="M80,70 L72,60 L78,48 L74,40 L92,36 L106,42 L116,34 L126,44 L124,56 L130,66 L108,72 L92,64 L82,70 Z" fill="#FFB157"/>
|
||||
</g>
|
||||
<text x="180" y="290" font-family="'JetBrains Mono','Fira Code',ui-monospace,monospace" font-size="42" font-weight="700" fill="#111315" letter-spacing="-1" text-anchor="middle">cairn <tspan fill="#E6690E">obs</tspan></text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1018 B |
@@ -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 @@
|
||||
<aside class="sidebar" class:mobile-open={mobileOpen}>
|
||||
<div class="brand">
|
||||
<a href="/" class="brand-link" onclick={onCloseMobile}>
|
||||
<img src={logo} alt="Cairn OBS" class="brand-logo" />
|
||||
<img src={isLight() ? logoLight : logoDark} alt="Cairn OBS" class="brand-logo" />
|
||||
</a>
|
||||
<button type="button" class="close-mobile" onclick={onCloseMobile} aria-label="Close menu">✕</button>
|
||||
</div>
|
||||
|
||||
@@ -34,10 +34,33 @@ const initial = readStored();
|
||||
let theme = $state<Theme>(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);
|
||||
|
||||
@@ -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 @@
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<img src={logo} alt="Cairn OBS" class="logo" />
|
||||
<img src={isLight() ? logoLight : logoDark} alt="Cairn OBS" class="logo" />
|
||||
<p class="tagline">
|
||||
One query bar for filter/stats queries and free-text search across every host and service
|
||||
you're shipping logs from.
|
||||
|
||||
|
Before Width: | Height: | Size: 493 B After Width: | Height: | Size: 535 B |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 912 B After Width: | Height: | Size: 968 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 11 KiB |