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),
|
# 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,
|
# so unlike api/ingest/search this build context is just alerting/ itself,
|
||||||
# same shape as cli/Dockerfile:
|
# 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
|
FROM golang:1.25-alpine AS builder
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . .
|
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
|
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
|
(different tables — see `/metadata/README.md`), and to `/api`'s
|
||||||
`POST /query` over plain HTTP for rule evaluation. Never connects to
|
`POST /query` over plain HTTP for rule evaluation. Never connects to
|
||||||
ClickHouse or Tantivy directly.
|
ClickHouse or Tantivy directly.
|
||||||
@@ -48,8 +48,8 @@ only, not separate delivery paths.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `HTTP_LISTEN_ADDR` | `:8081` |
|
| `HTTP_LISTEN_ADDR` | `:8081` |
|
||||||
| `POSTGRES_ADDR` | `localhost:5432` |
|
| `POSTGRES_ADDR` | `localhost:5432` |
|
||||||
| `POSTGRES_DATABASE` | `sentry_metadata` |
|
| `POSTGRES_DATABASE` | `cairnobs_metadata` |
|
||||||
| `POSTGRES_USERNAME` | `sentry` |
|
| `POSTGRES_USERNAME` | `cairnobs` |
|
||||||
| `POSTGRES_PASSWORD` | (empty — must be set) |
|
| `POSTGRES_PASSWORD` | (empty — must be set) |
|
||||||
| `API_QUERY_URL` | `http://localhost:8080` |
|
| `API_QUERY_URL` | `http://localhost:8080` |
|
||||||
| `CORS_ALLOWED_ORIGIN` | `*` |
|
| `CORS_ALLOWED_ORIGIN` | `*` |
|
||||||
@@ -94,5 +94,5 @@ go test ./...
|
|||||||
```
|
```
|
||||||
|
|
||||||
```sh
|
```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"),
|
HTTPListenAddr: getenv("HTTP_LISTEN_ADDR", ":8081"),
|
||||||
Postgres: PostgresConfig{
|
Postgres: PostgresConfig{
|
||||||
Addr: getenv("POSTGRES_ADDR", "localhost:5432"),
|
Addr: getenv("POSTGRES_ADDR", "localhost:5432"),
|
||||||
Database: getenv("POSTGRES_DATABASE", "sentry_metadata"),
|
Database: getenv("POSTGRES_DATABASE", "cairnobs_metadata"),
|
||||||
Username: getenv("POSTGRES_USERNAME", "sentry"),
|
Username: getenv("POSTGRES_USERNAME", "cairnobs"),
|
||||||
Password: getenv("POSTGRES_PASSWORD", ""),
|
Password: getenv("POSTGRES_PASSWORD", ""),
|
||||||
},
|
},
|
||||||
APIQueryURL: getenv("API_QUERY_URL", "http://localhost:8080"),
|
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")
|
t.Skip("SESSIONCHECK_TEST_POSTGRES_ADDR not set -- skipping live-Postgres integration test")
|
||||||
}
|
}
|
||||||
password := os.Getenv("SESSIONCHECK_TEST_POSTGRES_PASSWORD")
|
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)
|
pool, err := pgxpool.New(context.Background(), dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("opening pool: %v", err)
|
t.Fatalf("opening pool: %v", err)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Build context must be the repo root (sentry/), since this needs both
|
# 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
|
# api/ and proto/ (api now speaks gRPC to /search, using proto's checked-in
|
||||||
# Go bindings via the `replace` directive in api/go.mod):
|
# 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
|
FROM golang:1.25-alpine AS builder
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ Environment variables (see `internal/config/config.go`):
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `HTTP_LISTEN_ADDR` | `:8080` | |
|
| `HTTP_LISTEN_ADDR` | `:8080` | |
|
||||||
| `CLICKHOUSE_ADDR` | `localhost:9000` | Native protocol port |
|
| `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` |
|
| `SEARCH_GRPC_ADDR` | `localhost:50052` | Must match `/search`'s `GRPC_LISTEN_ADDR` |
|
||||||
| `QUERY_TIMEOUT_SECONDS` | `30` | Per-request timeout |
|
| `QUERY_TIMEOUT_SECONDS` | `30` | Per-request timeout |
|
||||||
| `CORS_ALLOWED_ORIGIN` | `*` | Wide open by default since there's no auth yet; tighten together |
|
| `CORS_ALLOWED_ORIGIN` | `*` | Wide open by default since there's no auth yet; tighten together |
|
||||||
@@ -66,7 +66,7 @@ go test ./...
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
# from the repo root, not api/
|
# from the repo root, not api/
|
||||||
docker build -f api/Dockerfile -t sentry-api .
|
docker build -f api/Dockerfile -t cairnobs-api .
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing notes
|
## 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")
|
t.Skip("DASHBOARDS_TEST_POSTGRES_ADDR not set -- skipping live-Postgres integration test")
|
||||||
}
|
}
|
||||||
password := os.Getenv("DASHBOARDS_TEST_POSTGRES_PASSWORD")
|
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)
|
pool, err := pgxpool.New(context.Background(), dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("opening pool: %v", err)
|
t.Fatalf("opening pool: %v", err)
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ type LocalAuthConfig struct {
|
|||||||
SessionTTL time.Duration
|
SessionTTL time.Duration
|
||||||
// CookieDomain empty means a host-only cookie (fine for local dev,
|
// CookieDomain empty means a host-only cookie (fine for local dev,
|
||||||
// where web/api are both localhost:<port>). Set to e.g.
|
// where web/api are both localhost:<port>). Set to e.g.
|
||||||
// ".sentry.example.com" in production so the cookie is also sent to
|
// ".cairnobs.example.com" in production so the cookie is also sent to
|
||||||
// api.sentry.example.com/alerting.sentry.example.com.
|
// api.cairnobs.example.com/alerting.cairnobs.example.com.
|
||||||
CookieDomain string
|
CookieDomain string
|
||||||
// CookieSecure defaults true (never sent over plain HTTP) --
|
// CookieSecure defaults true (never sent over plain HTTP) --
|
||||||
// deliberately opt-out via LOCAL_AUTH_COOKIE_SECURE=false, only
|
// deliberately opt-out via LOCAL_AUTH_COOKIE_SECURE=false, only
|
||||||
@@ -108,14 +108,14 @@ func Load() (Config, error) {
|
|||||||
HTTPListenAddr: getenv("HTTP_LISTEN_ADDR", ":8080"),
|
HTTPListenAddr: getenv("HTTP_LISTEN_ADDR", ":8080"),
|
||||||
ClickHouse: ClickHouseConfig{
|
ClickHouse: ClickHouseConfig{
|
||||||
Addr: getenv("CLICKHOUSE_ADDR", "localhost:9000"),
|
Addr: getenv("CLICKHOUSE_ADDR", "localhost:9000"),
|
||||||
Database: getenv("CLICKHOUSE_DATABASE", "sentry"),
|
Database: getenv("CLICKHOUSE_DATABASE", "cairnobs"),
|
||||||
Username: getenv("CLICKHOUSE_USERNAME", "default"),
|
Username: getenv("CLICKHOUSE_USERNAME", "default"),
|
||||||
Password: getenv("CLICKHOUSE_PASSWORD", ""),
|
Password: getenv("CLICKHOUSE_PASSWORD", ""),
|
||||||
},
|
},
|
||||||
Postgres: PostgresConfig{
|
Postgres: PostgresConfig{
|
||||||
Addr: getenv("POSTGRES_ADDR", "localhost:5432"),
|
Addr: getenv("POSTGRES_ADDR", "localhost:5432"),
|
||||||
Database: getenv("POSTGRES_DATABASE", "sentry_metadata"),
|
Database: getenv("POSTGRES_DATABASE", "cairnobs_metadata"),
|
||||||
Username: getenv("POSTGRES_USERNAME", "sentry"),
|
Username: getenv("POSTGRES_USERNAME", "cairnobs"),
|
||||||
Password: getenv("POSTGRES_PASSWORD", ""),
|
Password: getenv("POSTGRES_PASSWORD", ""),
|
||||||
},
|
},
|
||||||
// Search service's gRPC address (see /search) -- default matches
|
// 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")
|
t.Skip("LOCALAUTH_TEST_POSTGRES_ADDR not set -- skipping live-Postgres integration test")
|
||||||
}
|
}
|
||||||
password := os.Getenv("LOCALAUTH_TEST_POSTGRES_PASSWORD")
|
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)
|
pool, err := pgxpool.New(context.Background(), dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("opening pool: %v", err)
|
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)
|
t.Errorf("GetPasswordHashByID for an unknown ID: err = %v, want ErrNotFound", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 30
|
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.
|
# waits on this completing successfully before it starts.
|
||||||
redpanda-provision:
|
redpanda-provision:
|
||||||
build:
|
build:
|
||||||
@@ -120,13 +120,13 @@ services:
|
|||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
container_name: cairnobs-metadata-postgres
|
container_name: cairnobs-metadata-postgres
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: sentry_metadata
|
POSTGRES_DB: cairnobs_metadata
|
||||||
POSTGRES_USER: sentry
|
POSTGRES_USER: cairnobs
|
||||||
POSTGRES_PASSWORD: "cairnobs-dev-only" # not a real secret, same framing as CLICKHOUSE_PASSWORD above
|
POSTGRES_PASSWORD: "cairnobs-dev-only" # not a real secret, same framing as CLICKHOUSE_PASSWORD above
|
||||||
volumes:
|
volumes:
|
||||||
- metadata-postgres-data:/var/lib/postgresql/data
|
- metadata-postgres-data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U sentry -d sentry_metadata"]
|
test: ["CMD-SHELL", "pg_isready -U cairnobs -d cairnobs_metadata"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 30
|
retries: 30
|
||||||
@@ -143,12 +143,12 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
POSTGRES_HOST: "metadata-postgres"
|
POSTGRES_HOST: "metadata-postgres"
|
||||||
POSTGRES_PORT: "5432"
|
POSTGRES_PORT: "5432"
|
||||||
POSTGRES_USER: "sentry"
|
POSTGRES_USER: "cairnobs"
|
||||||
POSTGRES_PASSWORD: "cairnobs-dev-only"
|
POSTGRES_PASSWORD: "cairnobs-dev-only"
|
||||||
POSTGRES_DATABASE: "sentry_metadata"
|
POSTGRES_DATABASE: "cairnobs_metadata"
|
||||||
# Password for the restricted audit_writer Postgres role (Phase 4
|
# Password for the restricted audit_writer Postgres role (Phase 4
|
||||||
# task 4) -- INSERT+SELECT only on audit_log, never UPDATE/DELETE,
|
# 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.
|
# role every other store uses. See /docs/phase-4-isolation-design.md.
|
||||||
AUDIT_WRITER_PASSWORD: "audit-writer-dev-only"
|
AUDIT_WRITER_PASSWORD: "audit-writer-dev-only"
|
||||||
|
|
||||||
@@ -185,19 +185,19 @@ services:
|
|||||||
# compose file provisions one.
|
# compose file provisions one.
|
||||||
#
|
#
|
||||||
# AGENT_REGISTRY_POSTGRES_ADDR enables agent inventory/remote
|
# 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
|
# shared Postgres role api/dashboards already uses (agent
|
||||||
# inventory carries no tamper-evidence requirement, unlike
|
# inventory carries no tamper-evidence requirement, unlike
|
||||||
# audit_log's dedicated restricted role). Set here (unlike
|
# audit_log's dedicated restricted role). Set here (unlike
|
||||||
# ENTERPRISE_AUTH_URL above) since this feature has no multi-
|
# ENTERPRISE_AUTH_URL above) since this feature has no multi-
|
||||||
# tenancy prerequisite -- it works the same in single-tenant core.
|
# tenancy prerequisite -- it works the same in single-tenant core.
|
||||||
AGENT_REGISTRY_POSTGRES_ADDR: "metadata-postgres:5432"
|
AGENT_REGISTRY_POSTGRES_ADDR: "metadata-postgres:5432"
|
||||||
AGENT_REGISTRY_POSTGRES_USERNAME: "sentry"
|
AGENT_REGISTRY_POSTGRES_USERNAME: "cairnobs"
|
||||||
AGENT_REGISTRY_POSTGRES_PASSWORD: "cairnobs-dev-only"
|
AGENT_REGISTRY_POSTGRES_PASSWORD: "cairnobs-dev-only"
|
||||||
volumes:
|
volumes:
|
||||||
- ./hack/dev-certs/out:/etc/cairnobs-ingest:ro
|
- ./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
|
# offset tracking, own failure domain — see /search/README.md) and
|
||||||
# builds a Tantivy full-text index over the message field.
|
# builds a Tantivy full-text index over the message field.
|
||||||
search:
|
search:
|
||||||
@@ -255,8 +255,8 @@ services:
|
|||||||
CLICKHOUSE_PASSWORD: "cairnobs-dev-only"
|
CLICKHOUSE_PASSWORD: "cairnobs-dev-only"
|
||||||
SEARCH_GRPC_ADDR: "search:50052"
|
SEARCH_GRPC_ADDR: "search:50052"
|
||||||
POSTGRES_ADDR: "metadata-postgres:5432"
|
POSTGRES_ADDR: "metadata-postgres:5432"
|
||||||
POSTGRES_DATABASE: "sentry_metadata"
|
POSTGRES_DATABASE: "cairnobs_metadata"
|
||||||
POSTGRES_USERNAME: "sentry"
|
POSTGRES_USERNAME: "cairnobs"
|
||||||
POSTGRES_PASSWORD: "cairnobs-dev-only"
|
POSTGRES_PASSWORD: "cairnobs-dev-only"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
# alerting (Phase 3 task 5) depends_on api -- without this, that
|
# alerting (Phase 3 task 5) depends_on api -- without this, that
|
||||||
@@ -293,8 +293,8 @@ services:
|
|||||||
- "8081:8081"
|
- "8081:8081"
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_ADDR: "metadata-postgres:5432"
|
POSTGRES_ADDR: "metadata-postgres:5432"
|
||||||
POSTGRES_DATABASE: "sentry_metadata"
|
POSTGRES_DATABASE: "cairnobs_metadata"
|
||||||
POSTGRES_USERNAME: "sentry"
|
POSTGRES_USERNAME: "cairnobs"
|
||||||
POSTGRES_PASSWORD: "cairnobs-dev-only"
|
POSTGRES_PASSWORD: "cairnobs-dev-only"
|
||||||
# Resolves to whichever of api/enterprise-api is actually active --
|
# Resolves to whichever of api/enterprise-api is actually active --
|
||||||
# enterprise-api declares a `default.aliases: [api]` network alias
|
# 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).
|
# secret. Must be at least 32 bytes (see internal/config.Load).
|
||||||
ENTERPRISE_SESSION_SIGNING_KEY: "cairnobs-dev-only-session-signing-key-32bytes+"
|
ENTERPRISE_SESSION_SIGNING_KEY: "cairnobs-dev-only-session-signing-key-32bytes+"
|
||||||
POSTGRES_ADDR: "metadata-postgres:5432"
|
POSTGRES_ADDR: "metadata-postgres:5432"
|
||||||
POSTGRES_DATABASE: "sentry_metadata"
|
POSTGRES_DATABASE: "cairnobs_metadata"
|
||||||
POSTGRES_USERNAME: "sentry"
|
POSTGRES_USERNAME: "cairnobs"
|
||||||
POSTGRES_PASSWORD: "cairnobs-dev-only"
|
POSTGRES_PASSWORD: "cairnobs-dev-only"
|
||||||
# Where the browser lands after internal/loginhandler sets a
|
# Where the browser lands after internal/loginhandler sets a
|
||||||
# session cookie -- web's mapped host port (see web's build args
|
# session cookie -- web's mapped host port (see web's build args
|
||||||
@@ -391,8 +391,8 @@ services:
|
|||||||
CLICKHOUSE_ADMIN_PASSWORD: "cairnobs-dev-only"
|
CLICKHOUSE_ADMIN_PASSWORD: "cairnobs-dev-only"
|
||||||
SEARCH_GRPC_ADDR: "search:50052"
|
SEARCH_GRPC_ADDR: "search:50052"
|
||||||
POSTGRES_ADDR: "metadata-postgres:5432"
|
POSTGRES_ADDR: "metadata-postgres:5432"
|
||||||
POSTGRES_DATABASE: "sentry_metadata"
|
POSTGRES_DATABASE: "cairnobs_metadata"
|
||||||
POSTGRES_USERNAME: "sentry"
|
POSTGRES_USERNAME: "cairnobs"
|
||||||
POSTGRES_PASSWORD: "cairnobs-dev-only"
|
POSTGRES_PASSWORD: "cairnobs-dev-only"
|
||||||
AUDIT_WRITER_USERNAME: "audit_writer"
|
AUDIT_WRITER_USERNAME: "audit_writer"
|
||||||
AUDIT_WRITER_PASSWORD: "audit-writer-dev-only"
|
AUDIT_WRITER_PASSWORD: "audit-writer-dev-only"
|
||||||
@@ -434,8 +434,8 @@ services:
|
|||||||
REDPANDA_BROKERS: "redpanda:9092"
|
REDPANDA_BROKERS: "redpanda:9092"
|
||||||
CLICKHOUSE_ADDR: "clickhouse:9000"
|
CLICKHOUSE_ADDR: "clickhouse:9000"
|
||||||
POSTGRES_ADDR: "metadata-postgres:5432"
|
POSTGRES_ADDR: "metadata-postgres:5432"
|
||||||
POSTGRES_DATABASE: "sentry_metadata"
|
POSTGRES_DATABASE: "cairnobs_metadata"
|
||||||
POSTGRES_USERNAME: "sentry"
|
POSTGRES_USERNAME: "cairnobs"
|
||||||
POSTGRES_PASSWORD: "cairnobs-dev-only"
|
POSTGRES_PASSWORD: "cairnobs-dev-only"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "/enterprise-ingest", "-healthcheck"]
|
test: ["CMD", "/enterprise-ingest", "-healthcheck"]
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ already configured for.
|
|||||||
## Data model
|
## Data model
|
||||||
|
|
||||||
`metadata/migrations/0037_create_agents.sql`: one `agents` table, one
|
`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
|
dashboards/alert_rules already live in — not a new database, matching
|
||||||
this project's established "shared schema, different services own
|
this project's established "shared schema, different services own
|
||||||
different tables" shape. `tenant_id` defaults to `'default'` for
|
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)
|
## 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
|
shape as `dashboards`/`alerts`, plus a `config` sub-subcommand
|
||||||
(mirroring `dashboards permissions`) since an override has its own
|
(mirroring `dashboards permissions`) since an override has its own
|
||||||
get/set/clear lifecycle distinct from the agent resource itself:
|
get/set/clear lifecycle distinct from the agent resource itself:
|
||||||
|
|
||||||
```
|
```
|
||||||
sentryctl agents list|get <host>
|
cairnobsctl agents list|get <host>
|
||||||
sentryctl agents config get <host>|clear <host>
|
cairnobsctl agents config get <host>|clear <host>
|
||||||
sentryctl agents config set <host> [--batch-max-size N] [--batch-flush-interval-ms N]
|
cairnobsctl agents config set <host> [--batch-max-size N] [--batch-flush-interval-ms N]
|
||||||
[--heartbeat-enabled true|false] [--heartbeat-interval-ms N]
|
[--heartbeat-enabled true|false] [--heartbeat-interval-ms N]
|
||||||
[--journald-unit UNIT]
|
[--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
|
`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). |
|
| `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`). |
|
| `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. |
|
| `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. |
|
| `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)
|
## 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.
|
new credentials and still never touches ClickHouse/Postgres.
|
||||||
|
|
||||||
**The deployment-topology gap is closed for both Helm and
|
**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`,
|
`enterprise-api.yaml` are mutually exclusive on `enterprise.enabled`,
|
||||||
rendering to the same Service name and port either way, so a
|
rendering to the same Service name and port either way, so a
|
||||||
Helm-deployed cluster can't accidentally run the wrong binary — the same
|
Helm-deployed cluster can't accidentally run the wrong binary — the same
|
||||||
|
|||||||
@@ -8510,7 +8510,7 @@
|
|||||||
"direct_or_transitive": "direct",
|
"direct_or_transitive": "direct",
|
||||||
"ecosystem": "docker-image",
|
"ecosystem": "docker-image",
|
||||||
"flagged": "True",
|
"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"
|
"classification": "c"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ docker compose up -d --build
|
|||||||
```
|
```
|
||||||
|
|
||||||
This builds and starts, in dependency order: `redpanda` → `redpanda-provision`
|
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) →
|
`clickhouse-migrate` (applies `/storage/migrations`, then exits) →
|
||||||
`ingest` and `api` → `web`.
|
`ingest` and `api` → `web`.
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ In another terminal, **after** the agent is running and connected
|
|||||||
started won't be picked up):
|
started won't be picked up):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
logger "hello from sentry phase 0"
|
logger "hello from cairnobs phase 0"
|
||||||
```
|
```
|
||||||
|
|
||||||
`logger` (part of util-linux, present on virtually every Linux distro)
|
`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
|
starts every service in the file). Open `http://localhost:3000`, run the
|
||||||
default query (`SELECT * FROM logs
|
default query (`SELECT * FROM logs
|
||||||
ORDER BY timestamp DESC LIMIT 100`), and look for a row with
|
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:**
|
**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"}'
|
-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):**
|
itself):**
|
||||||
|
|
||||||
```sh
|
```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
|
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
|
at-least-once design (see `/ingest/README.md`), so this may just need
|
||||||
more time rather than intervention.
|
more time rather than intervention.
|
||||||
3. `docker compose exec redpanda rpk topic list` — confirm
|
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`.**
|
**`docker compose up` fails on `service_completed_successfully`.**
|
||||||
You're likely on Compose v1 (`docker-compose`, hyphenated) rather than v2
|
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
|
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`.
|
`RUST_LOG=info` is set on the `search` service in `docker-compose.yml`.
|
||||||
|
|
||||||
### A2. Generate a log line and confirm both query paths agree
|
### 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
|
Create a dashboard and a couple of panels, either through the web UI
|
||||||
(`http://localhost:3000/dashboards` → "+ Create" → "+ Add panel") or via
|
(`http://localhost:3000/dashboards` → "+ Create" → "+ Add panel") or via
|
||||||
`sentryctl`:
|
`cairnobsctl`:
|
||||||
|
|
||||||
```sh
|
```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
|
**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):
|
needed):
|
||||||
|
|
||||||
```sh
|
```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 .
|
-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
|
```sh
|
||||||
curl -X POST http://localhost:8081/targets -H 'Content-Type: application/json' -d '{
|
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 '{
|
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):
|
rule via the `/alerts/new` form, watched it transition in the browser):
|
||||||
the rule transitions `ok` → `firing` on its first evaluation
|
the rule transitions `ok` → `firing` on its first evaluation
|
||||||
(`for_minutes: 0`), the delivery log shows `firing / sent / 200`, and
|
(`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**
|
Also verified live: a threshold rule whose query returns **zero rows**
|
||||||
records `last_eval_status: "error"` with the exact expected message
|
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
|
scheduler, and materially larger rule counts (10,000+), are both
|
||||||
explicitly out of scope for this phase.
|
explicitly out of scope for this phase.
|
||||||
|
|
||||||
## 5. Confirm `sentryctl`
|
## 5. Confirm `cairnobsctl`
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sentryctl dashboards list
|
cairnobsctl dashboards list
|
||||||
sentryctl dashboards apply exported-dashboard.json
|
cairnobsctl dashboards apply exported-dashboard.json
|
||||||
sentryctl alerts list
|
cairnobsctl alerts list
|
||||||
sentryctl alerts apply rule.json
|
cairnobsctl alerts apply rule.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Both `dashboards` and `alerts` hit the exact same REST endpoints the web
|
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
|
## 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
|
Phase 3, per `/docs/phase-4-isolation-design.md`'s existing schema
|
||||||
additions (`tenants`, the `tenant_id` backfill on `alert_state`/
|
additions (`tenants`, the `tenant_id` backfill on `alert_state`/
|
||||||
`delivery_log`). New tables, continuing that migration sequence:
|
`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
|
query"), decided here rather than left ambiguous: hand-written queries
|
||||||
get the identical assessment an AI-generated one would, so there's
|
get the identical assessment an AI-generated one would, so there's
|
||||||
real parity, but retroactively hard-blocking existing dashboard/
|
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
|
behavioral change this phase didn't set out to make and could break
|
||||||
real existing usage. `warnings` is `omitempty` -- a client that
|
real existing usage. `warnings` is `omitempty` -- a client that
|
||||||
doesn't look for it sees no shape change at all. All existing
|
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
|
already live-verified repeatedly for Explain/Fix/Optimize's own
|
||||||
"unavailable" states, not new untested UI shape.
|
"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
|
Same posture as the UI, enforced identically regardless of how the
|
||||||
result was produced: a low-confidence, non-compiling, or cost-guard-blocked
|
result was produced: a low-confidence, non-compiling, or cost-guard-blocked
|
||||||
translation is never run, even with `--execute` -- confirmed by
|
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
|
**Genuinely verified against a live Postgres**, not just unit-tested
|
||||||
against a fake `InteractionLogger`: `metadata/migrations/0036` was
|
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+
|
(`docker compose up -d --build metadata-migrate`, confirmed via `\d+
|
||||||
audit_log` before/after showing `ai_interaction` added to the
|
audit_log` before/after showing `ai_interaction` added to the
|
||||||
`event_type` CHECK constraint), and two new tests in
|
`event_type` CHECK constraint), and two new tests in
|
||||||
@@ -622,7 +622,7 @@ ends up with:
|
|||||||
handler/routing logic via `fakeProvider` -- fast, deterministic, no
|
handler/routing logic via `fakeProvider` -- fast, deterministic, no
|
||||||
network, all run in CI today.
|
network, all run in CI today.
|
||||||
2. **Integration tests** (this task, new): the mock-Ollama-server suite
|
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
|
`httptest.Server`-backed coverage of `--nl`/`--execute` (already
|
||||||
written during Track B, task 11) -- proves the plumbing (HTTP routing,
|
written during Track B, task 11) -- proves the plumbing (HTTP routing,
|
||||||
JSON contracts, `planner.Compile`/`costguard.Assess` integration,
|
JSON contracts, `planner.Compile`/`costguard.Assess` integration,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ document is verification only.
|
|||||||
Every AI operation (`complete`, `explain`, `fix`, `optimize`,
|
Every AI operation (`complete`, `explain`, `fix`, `optimize`,
|
||||||
`translate`, and the audit-logging endpoint behind it) has been run
|
`translate`, and the audit-logging endpoint behind it) has been run
|
||||||
end-to-end against a real `docker compose` stack — real HTTP requests
|
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
|
`api/ai/provider/ollama.Client`, over a real network call, into a real
|
||||||
process answering Ollama's actual `/api/chat` wire contract. **No real
|
process answering Ollama's actual `/api/chat` wire contract. **No real
|
||||||
model weights are used anywhere in this verification** — see
|
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:
|
`OLLAMA_BASE_URL` at it via a throwaway compose override:
|
||||||
|
|
||||||
```sh
|
```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 \
|
-v "$(pwd)/hack/mock-ollama:/src" -w /src golang:1.25-alpine \
|
||||||
sh -c "go build -o /tmp/mock-ollama . && /tmp/mock-ollama"
|
sh -c "go build -o /tmp/mock-ollama . && /tmp/mock-ollama"
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ wired into a stack anyone else might reach:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker compose up -d api # drops back to the plain env, no -f override
|
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
|
rm /tmp/docker-compose.ai-verify.yml
|
||||||
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/ai/translate -d '{}'
|
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/ai/translate -d '{}'
|
||||||
# 404 -- confirms AI routes are unregistered again
|
# 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:
|
`localhost:8080`, open the Search page's query bar:
|
||||||
|
|
||||||
- Type a partial query and pause — ghost text should appear inline
|
- 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
|
ghost text just silently stops appearing (no error toast, no
|
||||||
console noise) — this is the "graceful degradation" requirement,
|
console noise) — this is the "graceful degradation" requirement,
|
||||||
not incidental behavior.
|
not incidental behavior.
|
||||||
@@ -149,7 +149,7 @@ separately click "Run query".
|
|||||||
CLI:
|
CLI:
|
||||||
|
|
||||||
```sh
|
```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
|
# 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.
|
1. Accept or dismiss a Fix/Optimize/Translate suggestion in the web UI.
|
||||||
2. Confirm a row landed in `audit_log`:
|
2. Confirm a row landed in `audit_log`:
|
||||||
```sh
|
```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;"
|
-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
|
`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:
|
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
|
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
|
pipe_stage := "where" bool_expr
|
||||||
| "stats" agg_call ("," agg_call)* ["by" field ("," field)*]
|
| "stats" agg_call ("," agg_call)* ["by" field ("," field)*]
|
||||||
| "sort" sort_field ("," sort_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.
|
real, no disclaimer needed for this specific claim.
|
||||||
|
|
||||||
**Both Helm and docker-compose now close this.**
|
**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
|
mutually exclusive, gated on opposite sides of the same
|
||||||
`enterprise.enabled` flag, rendering to the same Service name/port — so
|
`enterprise.enabled` flag, rendering to the same Service name/port — so
|
||||||
a Helm-deployed cluster runs exactly one of the two binaries, chosen by
|
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
|
the same flag that turns on RBAC/audit/SSO, not a second
|
||||||
independently-forgettable decision. Verified by parsing (not
|
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
|
Deployment either way, with the right image. `docker-compose.yml`'s
|
||||||
`api`/`enterprise-api` services are now the analogous mutually-exclusive
|
`api`/`enterprise-api` services are now the analogous mutually-exclusive
|
||||||
choice, gated behind `COMPOSE_PROFILES` (`.env` checks in
|
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)
|
└─▶ external IdP (OIDC authorization code flow)
|
||||||
└─▶ Postgres (rbacstore: users, tenant_memberships)
|
└─▶ 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 →
|
Ingest path (agent → Redpanda → ingest → ClickHouse, and Redpanda →
|
||||||
@@ -442,12 +442,12 @@ layer":
|
|||||||
|
|
||||||
1. A dedicated `audit_writer` Postgres role with only `INSERT`+`SELECT`
|
1. A dedicated `audit_writer` Postgres role with only `INSERT`+`SELECT`
|
||||||
grants (`metadata/migrations/0012-0014`), via its **own**
|
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.
|
store uses.
|
||||||
2. A `BEFORE UPDATE OR DELETE ... RAISE EXCEPTION` trigger
|
2. A `BEFORE UPDATE OR DELETE ... RAISE EXCEPTION` trigger
|
||||||
(`metadata/migrations/0015-0016`) that rejects the operation for
|
(`metadata/migrations/0015-0016`) that rejects the operation for
|
||||||
*any* role, including the table owner — confirmed live: even the
|
*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
|
trigger, a privileged operation distinct from ordinary application
|
||||||
access.
|
access.
|
||||||
|
|
||||||
@@ -547,10 +547,10 @@ terms:
|
|||||||
cluster/trusted network (`api`/`alerting`/`web`), never exposed
|
cluster/trusted network (`api`/`alerting`/`web`), never exposed
|
||||||
publicly. Nothing in this codebase enforces that at the network layer;
|
publicly. Nothing in this codebase enforces that at the network layer;
|
||||||
it's a deployment responsibility (NetworkPolicy, or equivalent) not
|
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
|
- `ENTERPRISE_SESSION_SIGNING_KEY`, ClickHouse/Postgres passwords, and
|
||||||
(once minted) the `alerting` service token are all K8s `Secret`
|
(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
|
secrets.yaml`) — standard K8s `Secret` semantics apply (base64, not
|
||||||
encrypted at rest without a cluster-level `EncryptionConfiguration`).
|
encrypted at rest without a cluster-level `EncryptionConfiguration`).
|
||||||
No secrets-manager integration (Vault, cloud KMS) exists; the chart
|
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 *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*, 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 |
|
| 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` |
|
| 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 — 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 |
|
| 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
|
# imports existed; the repo-root context below is the same shape
|
||||||
# enterprise-api's and enterprise-ingest's Dockerfiles already use for
|
# enterprise-api's and enterprise-ingest's Dockerfiles already use for
|
||||||
# the identical reason.
|
# 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
|
FROM golang:1.25-alpine AS builder
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . .
|
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
|
- `/alerting`'s `queryclient` presents a `RoleService` Bearer token
|
||||||
(`API_SERVICE_TOKEN`) when configured -- see
|
(`API_SERVICE_TOKEN`) when configured -- see
|
||||||
`/docs/phase-4-isolation-design.md`'s `alerting`↔`api` gap.
|
`/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.
|
request when set.
|
||||||
- `internal/rbacstore`: full CRUD over `users`/`tenants`/
|
- `internal/rbacstore`: full CRUD over `users`/`tenants`/
|
||||||
`tenant_memberships`/`data_sources` (`metadata/migrations/0017-0032`).
|
`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
|
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
|
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
|
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
|
`SELECT_TENANT_REDIRECT_URL` (defaults to
|
||||||
`{POST_LOGIN_REDIRECT_URL}/select-tenant`) instead of completing the
|
`{POST_LOGIN_REDIRECT_URL}/select-tenant`) instead of completing the
|
||||||
login. From there:
|
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
|
-- needed no code in this module at all, since `search`'s
|
||||||
`IndexRegistry` already lived in AGPL core); and deployment-topology
|
`IndexRegistry` already lived in AGPL core); and deployment-topology
|
||||||
routing (does traffic actually reach `enterprise-api` instead of
|
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
|
`docker-compose.yml` (`enterprise.enabled` / `COMPOSE_PROFILES`), see
|
||||||
CLAUDE.md.
|
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,
|
A real multi-tenant deployment runs `ingest -mode=server` (agent-facing,
|
||||||
tags records, unchanged) alongside `enterprise-ingest` (consumer,
|
tags records, unchanged) alongside `enterprise-ingest` (consumer,
|
||||||
per-tenant writes) *instead of* `ingest -mode=consumer` -- see `deploy/
|
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
|
credential-validation requirement and this mode split together, since
|
||||||
write-routing is only meaningful once records actually carry a
|
write-routing is only meaningful once records actually carry a
|
||||||
tenant_id to route on) and `docker-compose.yml`'s `enterprise-ingest`
|
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/session/ issues/validates signed session + RoleService tokens
|
||||||
internal/authhandler/ POST /internal/authorize, GET /auth/features
|
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/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/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/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
|
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
|
```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
|
## 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
|
re-running `-grant-membership-*` with a different
|
||||||
`-grant-membership-role` (`SetMembership`'s upsert already supports
|
`-grant-membership-role` (`SetMembership`'s upsert already supports
|
||||||
it). `dashboard_permissions` grants have no `enterprise-auth` flag and
|
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
|
covers them over the HTTP endpoints `api/dashboards`' handler already
|
||||||
exposes (`PUT`/`DELETE /dashboards/{id}/permissions/{userId}`,
|
exposes (`PUT`/`DELETE /dashboards/{id}/permissions/{userId}`,
|
||||||
`GET .../permissions`), see `/cli/README.md`.
|
`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
|
`internal/tenantcrd` sync step is a documented no-op in this deployment
|
||||||
shape, same as everywhere else this codebase has an "off unless
|
shape, same as everywhere else this codebase has an "off unless
|
||||||
configured" optional dependency. It only does anything in a real
|
configured" optional dependency. It only does anything in a real
|
||||||
cluster with `deploy/helm/sentry`'s `tenantOperator.enabled=true` -- see
|
cluster with `deploy/helm/cairnobs`'s `tenantOperator.enabled=true` -- see
|
||||||
`/deploy/helm/sentry/README.md`'s "Trying the two-tenant example."
|
`/deploy/helm/cairnobs/README.md`'s "Trying the two-tenant example."
|
||||||
|
|
||||||
`-provision-tenant` creates the tenant/data_source rows in rbacstore if
|
`-provision-tenant` creates the tenant/data_source rows in rbacstore if
|
||||||
they don't exist, provisions ClickHouse, persists the credentials, and
|
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` |
|
| `HTTP_LISTEN_ADDR` | `:8082` |
|
||||||
| `POSTGRES_ADDR` | `localhost:5432` |
|
| `POSTGRES_ADDR` | `localhost:5432` |
|
||||||
| `POSTGRES_DATABASE` | `sentry_metadata` |
|
| `POSTGRES_DATABASE` | `cairnobs_metadata` |
|
||||||
| `POSTGRES_USERNAME` | `sentry` |
|
| `POSTGRES_USERNAME` | `cairnobs` |
|
||||||
| `POSTGRES_PASSWORD` | (empty) |
|
| `POSTGRES_PASSWORD` | (empty) |
|
||||||
| `OIDC_ISSUER_URL` | (empty — OIDC discovery skipped if unset) |
|
| `OIDC_ISSUER_URL` | (empty — OIDC discovery skipped if unset) |
|
||||||
| `OIDC_CLIENT_ID` | (empty) |
|
| `OIDC_CLIENT_ID` | (empty) |
|
||||||
@@ -554,8 +554,8 @@ to, see `tenantprovision.ProvisionClickHouse`'s doc comment).
|
|||||||
| `CLICKHOUSE_ADMIN_PASSWORD` | (empty) |
|
| `CLICKHOUSE_ADMIN_PASSWORD` | (empty) |
|
||||||
| `SEARCH_GRPC_ADDR` | `localhost:50052` |
|
| `SEARCH_GRPC_ADDR` | `localhost:50052` |
|
||||||
| `POSTGRES_ADDR` | `localhost:5432` |
|
| `POSTGRES_ADDR` | `localhost:5432` |
|
||||||
| `POSTGRES_DATABASE` | `sentry_metadata` |
|
| `POSTGRES_DATABASE` | `cairnobs_metadata` |
|
||||||
| `POSTGRES_USERNAME` | `sentry` |
|
| `POSTGRES_USERNAME` | `cairnobs` |
|
||||||
| `POSTGRES_PASSWORD` | (empty) |
|
| `POSTGRES_PASSWORD` | (empty) |
|
||||||
| `AUDIT_WRITER_USERNAME` | `audit_writer` |
|
| `AUDIT_WRITER_USERNAME` | `audit_writer` |
|
||||||
| `AUDIT_WRITER_PASSWORD` | (empty) |
|
| `AUDIT_WRITER_PASSWORD` | (empty) |
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Same shape as every other Go service's Dockerfile in this repo --
|
# Same shape as every other Go service's Dockerfile in this repo --
|
||||||
# context must be the repo root (needs both enterprise/ and proto/, like
|
# context must be the repo root (needs both enterprise/ and proto/, like
|
||||||
# api/Dockerfile does for api/ + proto/), not enterprise/ alone.
|
# 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
|
FROM golang:1.25-alpine AS builder
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
// keeps running plain api/cmd/api, unchanged; a real multi-tenant
|
// keeps running plain api/cmd/api, unchanged; a real multi-tenant
|
||||||
// deployment runs this one instead.
|
// 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
|
// enterprise-api.yaml) and docker-compose.yml (COMPOSE_PROFILES) now
|
||||||
// make this the deployment-topology choice, not just a binary sitting
|
// make this the deployment-topology choice, not just a binary sitting
|
||||||
// unused alongside api's -- see this repo's CLAUDE.md. `search`'s write
|
// 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/,
|
# context must be the repo root (needs ingest/, proto/, and enterprise/,
|
||||||
# like enterprise-api/Dockerfile does for api/ + proto/ + enterprise/),
|
# like enterprise-api/Dockerfile does for api/ + proto/ + enterprise/),
|
||||||
# not enterprise/ alone.
|
# 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
|
FROM golang:1.25-alpine AS builder
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Command enterprise-ingest is the multi-tenant-aware alternative to
|
// Command enterprise-ingest is the multi-tenant-aware alternative to
|
||||||
// running `ingest -mode=consumer` -- reads the same shared
|
// 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
|
// server half (PushBatch) produces onto (see that binary's doc
|
||||||
// comment), but writes each record into its own tenant's dedicated
|
// comment), but writes each record into its own tenant's dedicated
|
||||||
// ClickHouse database (enterprise/internal/chwriter) instead of the one
|
// ClickHouse database (enterprise/internal/chwriter) instead of the one
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
// alongside) `ingest -mode=consumer` -- `ingest -mode=server` (the
|
// alongside) `ingest -mode=consumer` -- `ingest -mode=server` (the
|
||||||
// agent-facing half, which tags records with a tenant_id via
|
// agent-facing half, which tags records with a tenant_id via
|
||||||
// TenantResolver) keeps running unchanged and unconditionally either
|
// 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.
|
// changes.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|||||||
@@ -125,8 +125,8 @@ func Load() (Config, error) {
|
|||||||
},
|
},
|
||||||
Postgres: PostgresConfig{
|
Postgres: PostgresConfig{
|
||||||
Addr: getenv("POSTGRES_ADDR", "localhost:5432"),
|
Addr: getenv("POSTGRES_ADDR", "localhost:5432"),
|
||||||
Database: getenv("POSTGRES_DATABASE", "sentry_metadata"),
|
Database: getenv("POSTGRES_DATABASE", "cairnobs_metadata"),
|
||||||
Username: getenv("POSTGRES_USERNAME", "sentry"),
|
Username: getenv("POSTGRES_USERNAME", "cairnobs"),
|
||||||
Password: getenv("POSTGRES_PASSWORD", ""),
|
Password: getenv("POSTGRES_PASSWORD", ""),
|
||||||
},
|
},
|
||||||
AI: AIConfig{
|
AI: AIConfig{
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
// section. Two independent defenses back the "no update/delete path
|
// section. Two independent defenses back the "no update/delete path
|
||||||
// from the application layer" requirement -- both verified against a
|
// from the application layer" requirement -- both verified against a
|
||||||
// live Postgres, not just written: audit_writer (this package's own
|
// 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
|
// role every other store uses) has only INSERT+SELECT grants, and a
|
||||||
// BEFORE UPDATE OR DELETE trigger (metadata/migrations/0015-0016)
|
// BEFORE UPDATE OR DELETE trigger (metadata/migrations/0015-0016)
|
||||||
// rejects the operation for *any* role, including the table owner --
|
// 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
|
// disabling the trigger, a privileged operation distinct from ordinary
|
||||||
// application access.
|
// application access.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func testPool(t *testing.T, user, password string) *pgxpool.Pool {
|
|||||||
if addr == "" {
|
if addr == "" {
|
||||||
t.Skip("AUDIT_TEST_POSTGRES_ADDR not set -- skipping live-Postgres integration test")
|
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)
|
pool, err := pgxpool.New(context.Background(), dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("opening pool: %v", err)
|
t.Fatalf("opening pool: %v", err)
|
||||||
@@ -65,7 +65,7 @@ func cleanupAuditLog(t *testing.T, adminPool *pgxpool.Pool) {
|
|||||||
|
|
||||||
func TestAppendAndVerifyChainRealPostgres(t *testing.T) {
|
func TestAppendAndVerifyChainRealPostgres(t *testing.T) {
|
||||||
writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD"))
|
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)
|
cleanupAuditLog(t, adminPool)
|
||||||
defer 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.
|
// field on QueryAuditEntry -- matching that type's own doc comment.
|
||||||
func TestQueryAPILoggerWritesAttributedToContextIdentity(t *testing.T) {
|
func TestQueryAPILoggerWritesAttributedToContextIdentity(t *testing.T) {
|
||||||
writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD"))
|
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)
|
cleanupAuditLog(t, adminPool)
|
||||||
defer cleanupAuditLog(t, adminPool)
|
defer cleanupAuditLog(t, adminPool)
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ func TestQueryAPILoggerWritesAttributedToContextIdentity(t *testing.T) {
|
|||||||
|
|
||||||
func TestQueryAPILoggerRefusesWithoutIdentity(t *testing.T) {
|
func TestQueryAPILoggerRefusesWithoutIdentity(t *testing.T) {
|
||||||
writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD"))
|
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)
|
cleanupAuditLog(t, adminPool)
|
||||||
defer 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.
|
// confidence/accepted/edited fields that don't have dedicated columns.
|
||||||
func TestAIInteractionLoggerWritesAttributedToContextIdentity(t *testing.T) {
|
func TestAIInteractionLoggerWritesAttributedToContextIdentity(t *testing.T) {
|
||||||
writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD"))
|
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)
|
cleanupAuditLog(t, adminPool)
|
||||||
defer cleanupAuditLog(t, adminPool)
|
defer cleanupAuditLog(t, adminPool)
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ func TestAIInteractionLoggerWritesAttributedToContextIdentity(t *testing.T) {
|
|||||||
|
|
||||||
func TestAIInteractionLoggerRefusesWithoutIdentity(t *testing.T) {
|
func TestAIInteractionLoggerRefusesWithoutIdentity(t *testing.T) {
|
||||||
writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD"))
|
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)
|
cleanupAuditLog(t, adminPool)
|
||||||
defer cleanupAuditLog(t, adminPool)
|
defer cleanupAuditLog(t, adminPool)
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ func TestAIInteractionLoggerRefusesWithoutIdentity(t *testing.T) {
|
|||||||
// would also pass.
|
// would also pass.
|
||||||
func TestVerifyChainDetectsTampering(t *testing.T) {
|
func TestVerifyChainDetectsTampering(t *testing.T) {
|
||||||
writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD"))
|
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)
|
cleanupAuditLog(t, adminPool)
|
||||||
defer cleanupAuditLog(t, adminPool)
|
defer cleanupAuditLog(t, adminPool)
|
||||||
|
|
||||||
@@ -270,7 +270,7 @@ func TestVerifyChainDetectsTampering(t *testing.T) {
|
|||||||
// unit test of the Go code alone.
|
// unit test of the Go code alone.
|
||||||
func TestAppendConcurrentWritesProduceAValidChain(t *testing.T) {
|
func TestAppendConcurrentWritesProduceAValidChain(t *testing.T) {
|
||||||
writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD"))
|
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)
|
cleanupAuditLog(t, adminPool)
|
||||||
defer cleanupAuditLog(t, adminPool)
|
defer cleanupAuditLog(t, adminPool)
|
||||||
|
|
||||||
@@ -316,7 +316,7 @@ func TestAppendConcurrentWritesProduceAValidChain(t *testing.T) {
|
|||||||
// the previous checkpoint's hash.
|
// the previous checkpoint's hash.
|
||||||
func TestCheckpointerRun(t *testing.T) {
|
func TestCheckpointerRun(t *testing.T) {
|
||||||
writerPool := testPool(t, "audit_writer", os.Getenv("AUDIT_TEST_POSTGRES_PASSWORD"))
|
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)
|
cleanupAuditLog(t, adminPool)
|
||||||
defer cleanupAuditLog(t, adminPool)
|
defer cleanupAuditLog(t, adminPool)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
// chrunner.RunSQL resolves exactly one tenant per call from ctx (a
|
// chrunner.RunSQL resolves exactly one tenant per call from ctx (a
|
||||||
// single request always belongs to one identity); WriteBatch resolves
|
// single request always belongs to one identity); WriteBatch resolves
|
||||||
// per *record*, since one Kafka batch pulled off the shared
|
// 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
|
// tenants -- see ingest/internal/grpcserver's doc comment for why
|
||||||
// there's one shared topic, not topic-per-tenant.
|
// there's one shared topic, not topic-per-tenant.
|
||||||
package chwriter
|
package chwriter
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ func Load() (Config, error) {
|
|||||||
HTTPListenAddr: getenv("HTTP_LISTEN_ADDR", ":8082"),
|
HTTPListenAddr: getenv("HTTP_LISTEN_ADDR", ":8082"),
|
||||||
Postgres: PostgresConfig{
|
Postgres: PostgresConfig{
|
||||||
Addr: getenv("POSTGRES_ADDR", "localhost:5432"),
|
Addr: getenv("POSTGRES_ADDR", "localhost:5432"),
|
||||||
Database: getenv("POSTGRES_DATABASE", "sentry_metadata"),
|
Database: getenv("POSTGRES_DATABASE", "cairnobs_metadata"),
|
||||||
Username: getenv("POSTGRES_USERNAME", "sentry"),
|
Username: getenv("POSTGRES_USERNAME", "cairnobs"),
|
||||||
Password: getenv("POSTGRES_PASSWORD", ""),
|
Password: getenv("POSTGRES_PASSWORD", ""),
|
||||||
},
|
},
|
||||||
OIDC: OIDCConfig{
|
OIDC: OIDCConfig{
|
||||||
|
|||||||
@@ -74,26 +74,26 @@ func Load() (Config, error) {
|
|||||||
ClickHouseAddr: getenv("CLICKHOUSE_ADDR", "localhost:9000"),
|
ClickHouseAddr: getenv("CLICKHOUSE_ADDR", "localhost:9000"),
|
||||||
Postgres: PostgresConfig{
|
Postgres: PostgresConfig{
|
||||||
Addr: getenv("POSTGRES_ADDR", "localhost:5432"),
|
Addr: getenv("POSTGRES_ADDR", "localhost:5432"),
|
||||||
Database: getenv("POSTGRES_DATABASE", "sentry_metadata"),
|
Database: getenv("POSTGRES_DATABASE", "cairnobs_metadata"),
|
||||||
Username: getenv("POSTGRES_USERNAME", "sentry"),
|
Username: getenv("POSTGRES_USERNAME", "cairnobs"),
|
||||||
Password: getenv("POSTGRES_PASSWORD", ""),
|
Password: getenv("POSTGRES_PASSWORD", ""),
|
||||||
},
|
},
|
||||||
Redpanda: RedpandaConfig{
|
Redpanda: RedpandaConfig{
|
||||||
Brokers: strings.Split(getenv("REDPANDA_BROKERS", "localhost:9092"), ","),
|
Brokers: strings.Split(getenv("REDPANDA_BROKERS", "localhost:9092"), ","),
|
||||||
// Same default topic ingest/internal/config uses -- this
|
// 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)
|
// ingest/cmd/ingest's server half (agent-facing PushBatch)
|
||||||
// produces onto; there's no per-tenant topic, see
|
// produces onto; there's no per-tenant topic, see
|
||||||
// ingest/internal/grpcserver's doc comment.
|
// 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
|
// 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
|
// single-tenant `ingest -mode=consumer` must never share a
|
||||||
// group (each message would only ever reach one of them,
|
// group (each message would only ever reach one of them,
|
||||||
// silently splitting traffic) even though in practice a
|
// silently splitting traffic) even though in practice a
|
||||||
// real multi-tenant deployment runs this binary *instead
|
// real multi-tenant deployment runs this binary *instead
|
||||||
// of*, not alongside, `ingest -mode=consumer`.
|
// 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
|
// role schema (metadata/migrations/0017-0021) described in
|
||||||
// /docs/phase-4-rbac-design.md: users (global SSO identity), tenants,
|
// /docs/phase-4-rbac-design.md: users (global SSO identity), tenants,
|
||||||
// and tenant_memberships (per-tenant role). It uses the same shared
|
// 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
|
// enterprise/internal/audit's deliberately separate, narrower-granted
|
||||||
// pool) -- ordinary read/write CRUD on control-plane config, not an
|
// pool) -- ordinary read/write CRUD on control-plane config, not an
|
||||||
// append-only ledger, so it has no analogous reason to restrict its own
|
// 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")
|
t.Skip("RBACSTORE_TEST_POSTGRES_ADDR not set -- skipping live-Postgres integration test")
|
||||||
}
|
}
|
||||||
password := os.Getenv("RBACSTORE_TEST_POSTGRES_PASSWORD")
|
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)
|
pool, err := pgxpool.New(context.Background(), dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("opening pool: %v", err)
|
t.Fatalf("opening pool: %v", err)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func TestNewRejectsMissingConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNewRejectsMissingIDPMetadata(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 {
|
if err == nil {
|
||||||
t.Fatalf("expected an error when IDPMetadata is missing")
|
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."
|
// redirect rather than just "the code compiles."
|
||||||
func TestLoginURLBuildsAgainstRealIDPMetadata(t *testing.T) {
|
func TestLoginURLBuildsAgainstRealIDPMetadata(t *testing.T) {
|
||||||
sp, err := New(Config{
|
sp, err := New(Config{
|
||||||
EntityID: "https://sentry.example.com/saml/metadata",
|
EntityID: "https://cairnobs.example.com/saml/metadata",
|
||||||
ACSURL: "https://sentry.example.com/saml/acs",
|
ACSURL: "https://cairnobs.example.com/saml/acs",
|
||||||
IDPMetadata: fakeIDPMetadata(),
|
IDPMetadata: fakeIDPMetadata(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
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
|
# 2. Run a webhook-sink so the (never-firing, by design) rules have a
|
||||||
# valid notification target to point at:
|
# 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 .
|
-p 9099:9099 -v $(pwd)/../webhook-sink:/src -w /src golang:1.25-alpine go run .
|
||||||
|
|
||||||
# 3. Run the load test:
|
# 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")
|
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")
|
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")
|
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() {
|
func main() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Build context must be the repo root (sentry/), not ingest/, since this
|
# Build context must be the repo root (sentry/), not ingest/, since this
|
||||||
# needs both ingest/ and proto/:
|
# 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
|
FROM golang:1.25-alpine AS builder
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|||||||
@@ -86,13 +86,13 @@ full list and defaults) — no config file format for Phase 0:
|
|||||||
| Var | Default | Purpose |
|
| Var | Default | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `GRPC_LISTEN_ADDR` | `:4317` | Agent-facing gRPC listen address |
|
| `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_CERT_FILE` / `TLS_KEY_FILE` | `/etc/cairnobs-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_CLIENT_CA_FILE` | `/etc/cairnobs-ingest/ca.pem` | CA used to verify agent client certs |
|
||||||
| `REDPANDA_BROKERS` | `localhost:9092` | Comma-separated broker list |
|
| `REDPANDA_BROKERS` | `localhost:9092` | Comma-separated broker list |
|
||||||
| `REDPANDA_TOPIC` | `sentry.logs.raw` | Must match the topic provisioned in `/transport` |
|
| `REDPANDA_TOPIC` | `cairnobs.logs.raw` | Must match the topic provisioned in `/transport` |
|
||||||
| `REDPANDA_CONSUMER_GROUP` | `sentry-ingest` | Consumer group id |
|
| `REDPANDA_CONSUMER_GROUP` | `cairnobs-ingest` | Consumer group id |
|
||||||
| `CLICKHOUSE_ADDR` | `localhost:9000` | Native protocol port, not HTTP |
|
| `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_MAX_SIZE` | `500` | Records per ClickHouse batch insert |
|
||||||
| `CONSUMER_BATCH_FLUSH_INTERVAL_MS` | `2000` | Max time a partial batch waits before flushing |
|
| `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 |
|
| `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
|
Requires `google.golang.org/protobuf/cmd/protoc-gen-go` and
|
||||||
`google.golang.org/grpc/cmd/protoc-gen-go-grpc` only if you're
|
`google.golang.org/grpc/cmd/protoc-gen-go-grpc` only if you're
|
||||||
regenerating `/proto`'s Go bindings — ingest itself just imports the
|
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`).
|
`replace` directive in `go.mod`, pointing at `../proto`).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# from the repo root, not ingest/
|
# from the repo root, not ingest/
|
||||||
docker build -f ingest/Dockerfile -t sentry-ingest .
|
docker build -f ingest/Dockerfile -t cairnobs-ingest .
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing notes
|
## Testing notes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Package agentregistry is the Postgres-backed implementation of
|
// Package agentregistry is the Postgres-backed implementation of
|
||||||
// grpcserver.AgentRegistry -- ingest's half of agent inventory/remote
|
// grpcserver.AgentRegistry -- ingest's half of agent inventory/remote
|
||||||
// config (see /docs/agent-management-design.md). Writes into the same
|
// 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-
|
// inventory and edit-config views (api/agents), the same shared-schema-
|
||||||
// different-services shape alerting and api already use for dashboards/
|
// different-services shape alerting and api already use for dashboards/
|
||||||
// alert_rules.
|
// alert_rules.
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ type Config struct {
|
|||||||
// AgentRegistry enables agent inventory/remote config
|
// AgentRegistry enables agent inventory/remote config
|
||||||
// (internal/agentregistry, internal/grpcserver.AgentRegistry) when
|
// (internal/agentregistry, internal/grpcserver.AgentRegistry) when
|
||||||
// Postgres.Addr is set -- same "off unless configured" shape as
|
// Postgres.Addr is set -- same "off unless configured" shape as
|
||||||
// EnterpriseAuthURL above. Writes into the same sentry_metadata
|
// EnterpriseAuthURL above. Writes into the same cairnobs_metadata
|
||||||
// database api/web already use, via the same shared "sentry" role
|
// database api/web already use, via the same shared "cairnobs" role
|
||||||
// every other non-audit table in this schema uses (unlike
|
// every other non-audit table in this schema uses (unlike
|
||||||
// audit_log's dedicated restricted role -- agent inventory carries
|
// audit_log's dedicated restricted role -- agent inventory carries
|
||||||
// no tamper-evidence requirement).
|
// no tamper-evidence requirement).
|
||||||
@@ -110,12 +110,12 @@ func Load() (Config, error) {
|
|||||||
},
|
},
|
||||||
Redpanda: RedpandaConfig{
|
Redpanda: RedpandaConfig{
|
||||||
Brokers: strings.Split(getenv("REDPANDA_BROKERS", "localhost:9092"), ","),
|
Brokers: strings.Split(getenv("REDPANDA_BROKERS", "localhost:9092"), ","),
|
||||||
Topic: getenv("REDPANDA_TOPIC", "sentry.logs.raw"),
|
Topic: getenv("REDPANDA_TOPIC", "cairnobs.logs.raw"),
|
||||||
ConsumerGroup: getenv("REDPANDA_CONSUMER_GROUP", "sentry-ingest"),
|
ConsumerGroup: getenv("REDPANDA_CONSUMER_GROUP", "cairnobs-ingest"),
|
||||||
},
|
},
|
||||||
ClickHouse: ClickHouseConfig{
|
ClickHouse: ClickHouseConfig{
|
||||||
Addr: getenv("CLICKHOUSE_ADDR", "localhost:9000"),
|
Addr: getenv("CLICKHOUSE_ADDR", "localhost:9000"),
|
||||||
Database: getenv("CLICKHOUSE_DATABASE", "sentry"),
|
Database: getenv("CLICKHOUSE_DATABASE", "cairnobs"),
|
||||||
Username: getenv("CLICKHOUSE_USERNAME", "default"),
|
Username: getenv("CLICKHOUSE_USERNAME", "default"),
|
||||||
Password: getenv("CLICKHOUSE_PASSWORD", ""),
|
Password: getenv("CLICKHOUSE_PASSWORD", ""),
|
||||||
},
|
},
|
||||||
@@ -123,8 +123,8 @@ func Load() (Config, error) {
|
|||||||
AgentRegistry: AgentRegistryConfig{
|
AgentRegistry: AgentRegistryConfig{
|
||||||
Postgres: PostgresConfig{
|
Postgres: PostgresConfig{
|
||||||
Addr: getenv("AGENT_REGISTRY_POSTGRES_ADDR", ""),
|
Addr: getenv("AGENT_REGISTRY_POSTGRES_ADDR", ""),
|
||||||
Database: getenv("AGENT_REGISTRY_POSTGRES_DATABASE", "sentry_metadata"),
|
Database: getenv("AGENT_REGISTRY_POSTGRES_DATABASE", "cairnobs_metadata"),
|
||||||
Username: getenv("AGENT_REGISTRY_POSTGRES_USERNAME", "sentry"),
|
Username: getenv("AGENT_REGISTRY_POSTGRES_USERNAME", "cairnobs"),
|
||||||
Password: getenv("AGENT_REGISTRY_POSTGRES_PASSWORD", ""),
|
Password: getenv("AGENT_REGISTRY_POSTGRES_PASSWORD", ""),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ func TestLoadDefaults(t *testing.T) {
|
|||||||
if cfg.GRPC.ListenAddr != ":4317" {
|
if cfg.GRPC.ListenAddr != ":4317" {
|
||||||
t.Errorf("GRPC.ListenAddr = %q, want :4317", cfg.GRPC.ListenAddr)
|
t.Errorf("GRPC.ListenAddr = %q, want :4317", cfg.GRPC.ListenAddr)
|
||||||
}
|
}
|
||||||
if cfg.Redpanda.Topic != "sentry.logs.raw" {
|
if cfg.Redpanda.Topic != "cairnobs.logs.raw" {
|
||||||
t.Errorf("Redpanda.Topic = %q, want sentry.logs.raw", cfg.Redpanda.Topic)
|
t.Errorf("Redpanda.Topic = %q, want cairnobs.logs.raw", cfg.Redpanda.Topic)
|
||||||
}
|
}
|
||||||
if cfg.Batch.MaxSize != 500 {
|
if cfg.Batch.MaxSize != 500 {
|
||||||
t.Errorf("Batch.MaxSize = %d, want 500", cfg.Batch.MaxSize)
|
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
|
# One-shot migration runner: bash + psql client baked in, migrations/*.sql
|
||||||
# copied in at build time. Mirrors /storage/Dockerfile's shape.
|
# 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
|
FROM alpine:3.20
|
||||||
RUN apk add --no-cache bash postgresql16-client
|
RUN apk add --no-cache bash postgresql16-client
|
||||||
WORKDIR /metadata
|
WORKDIR /metadata
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ doesn't provide.
|
|||||||
|
|
||||||
## Schema
|
## 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`)
|
- `dashboards`, `dashboard_panels` — owned by `/api` (`api/internal/dashboards`)
|
||||||
- `notification_targets`, `alert_rules`, `alert_state`, `delivery_log` —
|
- `notification_targets`, `alert_rules`, `alert_state`, `delivery_log` —
|
||||||
owned by `/alerting`
|
owned by `/alerting`
|
||||||
- `audit_log` — owned by `enterprise/internal/audit` (Phase 4). Unlike
|
- `audit_log` — owned by `enterprise/internal/audit` (Phase 4). Unlike
|
||||||
every other table here, this one is **not** written through the shared
|
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 —
|
"Owned" here is a documentation convention, not a technical boundary —
|
||||||
both services connect to the same Postgres instance/database, each with
|
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
|
with **only** `INSERT`/`SELECT` grants on `audit_log` — no
|
||||||
`UPDATE`/`DELETE`/`TRUNCATE`, ever. `enterprise/internal/audit.Store`
|
`UPDATE`/`DELETE`/`TRUNCATE`, ever. `enterprise/internal/audit.Store`
|
||||||
connects using this role's credentials via its **own** `pgxpool.Pool`,
|
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
|
reusing the shared pool for audit writes would give audit_log's
|
||||||
application-level credential the same `UPDATE`/`DELETE` grants every
|
application-level credential the same `UPDATE`/`DELETE` grants every
|
||||||
other metadata table has, silently defeating the whole point.
|
other metadata table has, silently defeating the whole point.
|
||||||
2. A `BEFORE UPDATE OR DELETE` trigger (`migrations/0015`-`0016`) that
|
2. A `BEFORE UPDATE OR DELETE` trigger (`migrations/0015`-`0016`) that
|
||||||
rejects the operation for **any** role, including the table owner
|
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
|
`ALTER TABLE audit_log DISABLE TRIGGER audit_log_immutable` (a
|
||||||
privileged, distinct-from-normal-access operation) before it can
|
privileged, distinct-from-normal-access operation) before it can
|
||||||
modify a row. This is redundant defense-in-depth independent of the
|
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_HOST` | `localhost` |
|
||||||
| `POSTGRES_PORT` | `5432` |
|
| `POSTGRES_PORT` | `5432` |
|
||||||
| `POSTGRES_USER` | `sentry` |
|
| `POSTGRES_USER` | `cairnobs` |
|
||||||
| `POSTGRES_PASSWORD` | (empty — must be set) |
|
| `POSTGRES_PASSWORD` | (empty — must be set) |
|
||||||
| `POSTGRES_DATABASE` | `sentry_metadata` |
|
| `POSTGRES_DATABASE` | `cairnobs_metadata` |
|
||||||
| `AUDIT_WRITER_PASSWORD` | `audit-writer-dev-only` |
|
| `AUDIT_WRITER_PASSWORD` | `audit-writer-dev-only` |
|
||||||
|
|
||||||
The database itself isn't created by `migrate.sh` — the `postgres:16-alpine`
|
The database itself isn't created by `migrate.sh` — the `postgres:16-alpine`
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: sentry_metadata
|
POSTGRES_DB: cairnobs_metadata
|
||||||
POSTGRES_USER: sentry
|
POSTGRES_USER: cairnobs
|
||||||
POSTGRES_PASSWORD: "cairnobs-dev-only" # not a real secret, see root docker-compose.yml
|
POSTGRES_PASSWORD: "cairnobs-dev-only" # not a real secret, see root docker-compose.yml
|
||||||
volumes:
|
volumes:
|
||||||
- metadata-postgres-data:/var/lib/postgresql/data
|
- metadata-postgres-data:/var/lib/postgresql/data
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ set -euo pipefail
|
|||||||
|
|
||||||
POSTGRES_HOST="${POSTGRES_HOST:-localhost}"
|
POSTGRES_HOST="${POSTGRES_HOST:-localhost}"
|
||||||
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
|
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
|
||||||
POSTGRES_USER="${POSTGRES_USER:-sentry}"
|
POSTGRES_USER="${POSTGRES_USER:-cairnobs}"
|
||||||
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}"
|
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
|
# Password for the restricted audit-log-writer Postgres role (Phase 4
|
||||||
# task 4, see /docs/phase-4-isolation-design.md's audit logging
|
# task 4, see /docs/phase-4-isolation-design.md's audit logging
|
||||||
# section) -- a second, narrower-granted role, not the shared
|
# section) -- a second, narrower-granted role, not the shared
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ planning.
|
|||||||
## How it fits together
|
## 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
|
`--> 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 |
|
| `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_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 |
|
| `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 |
|
| `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) |
|
| `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(',')
|
.split(',')
|
||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.collect(),
|
.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")),
|
index_path: PathBuf::from(getenv("INDEX_PATH", "/var/lib/cairnobs-search/index")),
|
||||||
offsets_path: PathBuf::from(getenv(
|
offsets_path: PathBuf::from(getenv(
|
||||||
"OFFSETS_PATH",
|
"OFFSETS_PATH",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use crate::tenants::ActiveTenantTracker;
|
|||||||
/// `TestTenantIDHeaderKeyConstantsMatch` does on the Go side.
|
/// `TestTenantIDHeaderKeyConstantsMatch` does on the Go side.
|
||||||
const TENANT_ID_HEADER_KEY: &str = "tenant_id";
|
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
|
/// consumer reads, as an independent consumer group in spirit (its own
|
||||||
/// offset tracking, own failure domain) even though rskafka doesn't speak
|
/// offset tracking, own failure domain) even though rskafka doesn't speak
|
||||||
/// Kafka's broker-side consumer-group protocol -- see offsets.rs. One
|
/// 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
|
# One-shot migration runner: bash + curl baked in, migrations/*.sql copied
|
||||||
# in at build time. No runtime package install and no host volume mount
|
# in at build time. No runtime package install and no host volume mount
|
||||||
# needed — works offline once built.
|
# 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
|
FROM alpine:3.20
|
||||||
RUN apk add --no-cache bash curl
|
RUN apk add --no-cache bash curl
|
||||||
WORKDIR /storage
|
WORKDIR /storage
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ defaults so the two stay in sync out of the box):
|
|||||||
| `CLICKHOUSE_HTTP` | `http://localhost:8123` |
|
| `CLICKHOUSE_HTTP` | `http://localhost:8123` |
|
||||||
| `CLICKHOUSE_USER` | `default` |
|
| `CLICKHOUSE_USER` | `default` |
|
||||||
| `CLICKHOUSE_PASSWORD` | (empty — override, see above) |
|
| `CLICKHOUSE_PASSWORD` | (empty — override, see above) |
|
||||||
| `CLICKHOUSE_DATABASE` | `sentry` |
|
| `CLICKHOUSE_DATABASE` | `cairnobs` |
|
||||||
|
|
||||||
There's also a `Dockerfile` (bash + curl baked in, `migrations/` copied in
|
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
|
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_HTTP="${CLICKHOUSE_HTTP:-http://localhost:8123}"
|
||||||
CLICKHOUSE_USER="${CLICKHOUSE_USER:-default}"
|
CLICKHOUSE_USER="${CLICKHOUSE_USER:-default}"
|
||||||
CLICKHOUSE_PASSWORD="${CLICKHOUSE_PASSWORD:-}"
|
CLICKHOUSE_PASSWORD="${CLICKHOUSE_PASSWORD:-}"
|
||||||
DATABASE="${CLICKHOUSE_DATABASE:-sentry}"
|
DATABASE="${CLICKHOUSE_DATABASE:-cairnobs}"
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
MIGRATIONS_DIR="${SCRIPT_DIR}/migrations"
|
MIGRATIONS_DIR="${SCRIPT_DIR}/migrations"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Redpanda for local development, plus the script that provisions the topic
|
|||||||
|
|
||||||
## Topic naming contract
|
## 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
|
`/ingest/internal/config`). `provision-topics.sh` defaults to the same
|
||||||
name. These aren't wired together automatically — if you change one,
|
name. These aren't wired together automatically — if you change one,
|
||||||
change the other, or override `REDPANDA_TOPIC` consistently wherever
|
change the other, or override `REDPANDA_TOPIC` consistently wherever
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ set -euo pipefail
|
|||||||
# of ever becoming healthy.
|
# of ever becoming healthy.
|
||||||
BROKERS="${REDPANDA_BROKERS:-localhost:9092}"
|
BROKERS="${REDPANDA_BROKERS:-localhost:9092}"
|
||||||
ADMIN_HOSTS="${REDPANDA_ADMIN_HOSTS:-localhost:9644}"
|
ADMIN_HOSTS="${REDPANDA_ADMIN_HOSTS:-localhost:9644}"
|
||||||
TOPIC="${REDPANDA_TOPIC:-sentry.logs.raw}"
|
TOPIC="${REDPANDA_TOPIC:-cairnobs.logs.raw}"
|
||||||
PARTITIONS="${REDPANDA_TOPIC_PARTITIONS:-6}"
|
PARTITIONS="${REDPANDA_TOPIC_PARTITIONS:-6}"
|
||||||
|
|
||||||
echo "Waiting for Redpanda admin API at ${ADMIN_HOSTS}..."
|
echo "Waiting for Redpanda admin API at ${ADMIN_HOSTS}..."
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<!-- Favicon: simplified 3-stone mark on terminal-dark rounded square -->
|
<!-- Favicon: simplified 3-stone mark on terminal-dark rounded square -->
|
||||||
<rect x="0" y="0" width="64" height="64" rx="14" fill="#0D1117"/>
|
<rect x="0" y="0" width="64" height="64" rx="14" fill="#0D1117"/>
|
||||||
<g>
|
<g>
|
||||||
<rect x="12" y="38" width="40" height="13" rx="6.5" fill="#FF8A00" transform="rotate(-2 32 44.5)"/>
|
<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"/>
|
||||||
<rect x="18" y="24" width="28" height="12" rx="6" fill="#FF9D2E" transform="rotate(3 32 30)"/>
|
<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"/>
|
||||||
<rect x="23" y="12" width="18" height="10" rx="5" fill="#FFB157" transform="rotate(-3 32 17)"/>
|
<path d="M24,21 L21,16 L25,12 L32,11 L38,13 L41,17 L38,21 L31,22 L27,20 Z" fill="#FFB157"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</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">
|
<svg width="600" height="140" viewBox="0 0 600 140" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Horizontal lockup for dark backgrounds -->
|
<!-- Horizontal lockup for dark backgrounds -->
|
||||||
<g transform="translate(20,10) scale(0.6)">
|
<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)"/>
|
<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"/>
|
||||||
<rect x="48" y="106" width="104" height="38" rx="18" fill="#FF8A00" transform="rotate(2.5 100 125)"/>
|
<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"/>
|
||||||
<rect x="64" y="73" width="72" height="34" rx="16" fill="#FF9D2E" transform="rotate(-3 100 90)"/>
|
<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"/>
|
||||||
<rect x="78" y="43" width="44" height="30" rx="14" fill="#FFB157" transform="rotate(3.5 100 58)"/>
|
<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>
|
</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="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>
|
<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">
|
<svg width="360" height="360" viewBox="0 0 360 360" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Stacked lockup for dark backgrounds / square placements -->
|
<!-- Stacked lockup for dark backgrounds / square placements -->
|
||||||
<g transform="translate(80,30) scale(1.0)">
|
<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)"/>
|
<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"/>
|
||||||
<rect x="48" y="106" width="104" height="38" rx="18" fill="#FF8A00" transform="rotate(2.5 100 125)"/>
|
<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"/>
|
||||||
<rect x="64" y="73" width="72" height="34" rx="16" fill="#FF9D2E" transform="rotate(-3 100 90)"/>
|
<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"/>
|
||||||
<rect x="78" y="43" width="44" height="30" rx="14" fill="#FFB157" transform="rotate(3.5 100 58)"/>
|
<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>
|
</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>
|
<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>
|
</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 CurrentSession,
|
||||||
type LocalSession
|
type LocalSession
|
||||||
} from '$lib/api';
|
} 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 { 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 {
|
let {
|
||||||
onOpenPalette,
|
onOpenPalette,
|
||||||
@@ -84,7 +85,7 @@
|
|||||||
<aside class="sidebar" class:mobile-open={mobileOpen}>
|
<aside class="sidebar" class:mobile-open={mobileOpen}>
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<a href="/" class="brand-link" onclick={onCloseMobile}>
|
<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>
|
</a>
|
||||||
<button type="button" class="close-mobile" onclick={onCloseMobile} aria-label="Close menu">✕</button>
|
<button type="button" class="close-mobile" onclick={onCloseMobile} aria-label="Close menu">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,10 +34,33 @@ const initial = readStored();
|
|||||||
let theme = $state<Theme>(initial);
|
let theme = $state<Theme>(initial);
|
||||||
apply(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 {
|
export function getTheme(): Theme {
|
||||||
return 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) {
|
export function setTheme(t: Theme) {
|
||||||
theme = t;
|
theme = t;
|
||||||
apply(t);
|
apply(t);
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
// /search (the old root; every prior link/shortcut/drill-down that
|
// /search (the old root; every prior link/shortcut/drill-down that
|
||||||
// used to point at "/" expecting the query page now points at
|
// used to point at "/" expecting the query page now points at
|
||||||
// "/search" explicitly, see NavSidebar/CommandPalette/drilldown.ts).
|
// "/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';
|
import { Button } from '$lib/components/ui';
|
||||||
|
|
||||||
const shortcuts: { href: string; label: string; hint: string }[] = [
|
const shortcuts: { href: string; label: string; hint: string }[] = [
|
||||||
@@ -15,7 +17,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<img src={logo} alt="Cairn OBS" class="logo" />
|
<img src={isLight() ? logoLight : logoDark} alt="Cairn OBS" class="logo" />
|
||||||
<p class="tagline">
|
<p class="tagline">
|
||||||
One query bar for filter/stats queries and free-text search across every host and service
|
One query bar for filter/stats queries and free-text search across every host and service
|
||||||
you're shipping logs from.
|
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 |