Extends the agent, ingest, storage, api, and web with Windows Event Log/ETW sourcing and Tantivy-backed free-text search, per the approved Phase 1 plan. - CLAUDE.md: materialized on disk (never existed as a file before) with a new Phase 1 "done looks like" section. - agent: Windows Event Log (EvtSubscribe) and ETW sources, Windows service wrapper (install/uninstall/run-service), both feature- and target_os-gated so Linux builds/tests/clippy stay unaffected. Also fixed two pre-existing Phase 0 clippy gaps (dead-code on default-features-only builds, a type-inference edge case) found while testing every feature combination properly for the first time. UNVERIFIED on real Windows -- no Windows toolchain existed anywhere in the build environment; flagged prominently in three places. - proto/ingest: new record_id field, assigned once server-side in ingest's gRPC front end so ClickHouse and Tantivy agree on the same ID for the same record. - storage: record_id column + bloom filter index, verified against a live ClickHouse. - search: new service, Tantivy index, rskafka consumer as an independent second consumer group on the same Redpanda topic ingest already reads. - api/web: new /search endpoint and page, sharing the query page's result-table shape and component. - hack/windows-fixture: sends realistic Windows-shaped data straight to ingest, so the pipeline's handling of it is verifiable without a Windows host. Verified end-to-end on the live docker-compose stack: the same record_id comes back from both /query and /search for the same log line, including for windows-fixture's synthetic Windows Event Log data. Real bugs found and fixed along the way: api/Dockerfile missing proto/ in its build context, search's logs being completely silent (RUST_LOG gap), and search/target/ missing from .gitignore/.dockerignore.
114 lines
5.1 KiB
Markdown
114 lines
5.1 KiB
Markdown
# storage
|
|
|
|
ClickHouse schema and migration tooling for Sentry's analytical store.
|
|
|
|
## Schema
|
|
|
|
One table, `logs` (Phase 0 columns plus `record_id`, added in
|
|
`migrations/0002_add_record_id.sql`):
|
|
|
|
```sql
|
|
CREATE TABLE logs
|
|
(
|
|
`timestamp` DateTime64(9, 'UTC'),
|
|
`host` String,
|
|
`service` String,
|
|
`severity` LowCardinality(String),
|
|
`message` String,
|
|
`attributes` Map(String, String),
|
|
`record_id` UUID DEFAULT generateUUIDv4()
|
|
)
|
|
ENGINE = MergeTree
|
|
PARTITION BY toDate(timestamp)
|
|
ORDER BY (service, timestamp)
|
|
-- plus: INDEX record_id_idx record_id TYPE bloom_filter GRANULARITY 4
|
|
```
|
|
|
|
**`record_id`** (Phase 1) is the stable per-record identifier Tantivy's
|
|
full-text search joins back to this table with — `/ingest`'s gRPC front
|
|
end assigns it once, server-side, before a record is produced to
|
|
Redpanda (see `/ingest/README.md` for why it has to happen exactly once,
|
|
upstream of both the ClickHouse-writer and Tantivy-indexer consumers).
|
|
Added via `ALTER TABLE ... ADD COLUMN` + `ADD INDEX` rather than changing
|
|
`ORDER BY`: `ORDER BY (service, timestamp)` is the proven time-range-scan
|
|
access pattern from Phase 0 and shouldn't be disturbed for a
|
|
fundamentally different access pattern (point lookups by ID). A
|
|
data-skipping bloom filter index on `record_id` serves the `WHERE
|
|
record_id IN (...)` lookup Tantivy-backed search results need, without
|
|
touching the primary sort order. The `DEFAULT generateUUIDv4()` is a
|
|
safety net, not the normal path — every row `/ingest` writes explicitly
|
|
supplies its own `record_id` from the proto message; the default only
|
|
matters for rows written some other way.
|
|
|
|
Notes on choices that weren't fully specified by the task description:
|
|
|
|
- **`DateTime64(9, 'UTC')`** (nanosecond precision) rather than second or
|
|
millisecond precision, to match the agent's `timestamp_unix_nano` field
|
|
end to end without truncation.
|
|
- **`severity` as `LowCardinality(String)`**, not a numeric OTel
|
|
`SeverityNumber`. `/ingest`'s `normalize` package writes short text
|
|
values (`TRACE`/`DEBUG`/`INFO`/`WARN`/`ERROR`/`FATAL`/`UNSPECIFIED`).
|
|
`LowCardinality` gets you most of the storage/query efficiency of an enum
|
|
without committing to one at the schema level. Splitting into a proper
|
|
`SeverityNumber` + `SeverityText` pair (full OTel shape) is one of the
|
|
open questions already flagged in `/docs/architecture.md`.
|
|
- **`PARTITION BY toDate(timestamp)`** (daily partitions) and
|
|
**`ORDER BY (service, timestamp)`** are exactly what the task asked for
|
|
— service-scoped queries over a time range are the dominant access
|
|
pattern this is optimized for.
|
|
- No TTL/retention clause yet — also an open question in architecture.md,
|
|
deferred until storage sizing is a real concern.
|
|
|
|
## Migration tooling: a plain SQL-file runner, not golang-migrate
|
|
|
|
`migrate.sh` applies `migrations/*.sql` in filename order over
|
|
ClickHouse's HTTP interface, tracking what's applied in a
|
|
`schema_migrations` table. Chosen over `golang-migrate` for Phase 0
|
|
because there's exactly one migration to run — pulling in a migration
|
|
framework (another dependency, another thing to configure/vendor) for a
|
|
single `CREATE TABLE` is exactly the kind of premature machinery this
|
|
project's conventions say to avoid. Revisit `golang-migrate` once there's
|
|
real schema churn across environments (rollback support, checksums,
|
|
concurrent-apply safety become worth their cost at that point, not before).
|
|
|
|
**Convention:** one DDL statement per migration file. The ClickHouse HTTP
|
|
interface isn't reliably multi-statement, so `migrate.sh` doesn't try to
|
|
split multi-statement files — keep each migration to a single statement.
|
|
|
|
## Running
|
|
|
|
```sh
|
|
docker compose up -d # starts a standalone ClickHouse for local work
|
|
CLICKHOUSE_PASSWORD=sentry-dev-only ./migrate.sh # applies migrations/*.sql
|
|
```
|
|
|
|
`CLICKHOUSE_PASSWORD` here has to match whatever `docker-compose.yml` set
|
|
on the `clickhouse` service — found this the hard way running the Phase 0
|
|
runbook for real: the official ClickHouse image silently disables *all*
|
|
network access (including the published port, not just container-to-
|
|
container traffic) for the `default` user unless `CLICKHOUSE_USER` or
|
|
`CLICKHOUSE_PASSWORD` is a genuinely non-empty value. An empty
|
|
`CLICKHOUSE_PASSWORD=""` still triggers the lockdown — it has to actually
|
|
have a value. Not a real secret, just what this image demands.
|
|
|
|
Environment variables `migrate.sh` reads (all optional except
|
|
`CLICKHOUSE_PASSWORD` as of the above, matching `/ingest`'s ClickHouse
|
|
defaults so the two stay in sync out of the box):
|
|
|
|
| Var | Default |
|
|
|---|---|
|
|
| `CLICKHOUSE_HTTP` | `http://localhost:8123` |
|
|
| `CLICKHOUSE_USER` | `default` |
|
|
| `CLICKHOUSE_PASSWORD` | (empty — override, see above) |
|
|
| `CLICKHOUSE_DATABASE` | `sentry` |
|
|
|
|
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
|
|
init service — no runtime package install, no host volume mount needed.
|
|
|
|
## Adding a migration
|
|
|
|
Add `migrations/000N_description.sql` with the next sequential number and
|
|
a single DDL statement. `migrate.sh` picks it up automatically — no
|
|
registration step.
|