Full rebrand across cosmetic branding, code identifiers, and infrastructure/data-plane naming, using the supplied Cairn OBS logo package. Cosmetic: favicon/logo swap (also closes a stale license-audit finding -- the old favicon was SvelteKit's unreplaced scaffold logo), new centered welcome landing page, larger/legible sidebar logo, page titles, CLAUDE.md/README/docs prose. Code identifiers: Go module path github.com/sentry/sentry -> github.com/cairnobs/cairnobs across all 13 modules and ~91 files (protoc regenerated); Rust crates sentry-agent/sentry-parser/sentry-search -> cairnobs-*; CLI sentryctl -> cairnobsctl; Terraform provider fully renamed (sentry_dashboard etc. -> cairnobs_dashboard, provider type, env vars); every session/auth cookie name; agent config paths and Windows service identity. Deliberately preserved: the gRPC wire protocol's protobuf packages (sentry.logs.v1, sentry.agent.v1) and their Go import directory (proto/sentry/...) -- renaming the wire-level package would break every currently-deployed agent binary (confirmed two real hosts, including mail.inbuxa.com, are actively streaming through this exact contract) until rebuilt and redeployed in lockstep with an ingest cutover. Only the Go module path wrapping the generated code changes. Infrastructure: every docker-compose container name (root and three component-level compose files); the Helm chart (directory, Chart.yaml, named-template helpers, all templates, values.yaml image repos); Kubernetes Operator (CRD group sentry.io -> cairnobs.io, both CRD YAML files, Go identifiers, RBAC markers); the coupled enterprise/tenantcrd package. Caught and fixed real path-coupling bugs along the way: the Helm chart's search/ingest volume mounts and the dev-only-credential detection constant vs. docker-compose.yml's literal values had to move together or a security warning would have silently stopped firing. Data plane: Postgres database sentry_metadata -> cairnobs_metadata and role sentry -> cairnobs; ClickHouse database sentry -> cairnobs; Kafka topic sentry.logs.raw -> cairnobs.logs.raw and its consumer groups. Source-level defaults, docker-compose.yml, and every migrate.sh/ provision script default updated together; already-applied migration files left untouched per this repo's immutable-migration convention. Verified at every layer: all 13 Go modules build/vet/test clean, both Rust workspaces (agent, search) build/clippy/test clean, npm run check/ build clean, docker compose config validates on all four compose files. Live-verified against a real docker stack multiple times through this work, including a final fresh-volume run confirming the actual renamed Postgres database/role, ClickHouse database, and Kafka topic all work end to end with a real login and query, zero console errors.
273 lines
12 KiB
Markdown
273 lines
12 KiB
Markdown
# cairnobs-agent
|
|
|
|
Distro-agnostic Linux/Windows log collector. On Linux, statically linked
|
|
against musl, no glibc runtime dependency. Tails journald (Linux default),
|
|
a file, Windows Event Log, or ETW, batches lines, and ships them over mTLS
|
|
gRPC to the ingest service.
|
|
|
|
**Windows support status:** the Windows-specific code
|
|
(`source/windows_eventlog.rs`, `source/etw.rs`, `service.rs`) was written
|
|
against documented Win32/ETW API shapes but has **not been compiled or run
|
|
on Windows** — no Windows toolchain was available in the environment this
|
|
was built in (confirmed: only the Linux target's std library was
|
|
installed, no way to even `cargo check --target x86_64-pc-windows-*`).
|
|
Linux builds/tests/clippy are verified clean across every feature
|
|
combination; Windows code is a first draft to compile-check and test for
|
|
real before trusting it. See `/docs/phase-1-runbook.md`.
|
|
|
|
## Workspace layout
|
|
|
|
- `cairnobs-parser` — pure-`std` RFC 5424 syslog parser with raw-passthrough
|
|
fallback. No I/O, easy to unit test in isolation.
|
|
- `cairnobs-agent` — the binary: config loading, sourcing (journald/file/
|
|
Windows Event Log/ETW), batching, mTLS gRPC client, Windows service
|
|
wrapper.
|
|
|
|
## Why one crate for both platforms, not a platform split
|
|
|
|
`config.rs`, `batch.rs`, `grpc.rs`, and `main.rs`'s event loop are already
|
|
100% cross-platform Rust — nothing in them is Linux- or Windows-specific.
|
|
Only the `source/` modules differ per platform, and that boundary already
|
|
existed before Windows support was added (it's exactly what made adding
|
|
Windows sources a matter of adding two files, not restructuring anything).
|
|
Windows-only dependencies (`windows`, `windows-service`, `quick-xml`) live
|
|
in a `[target.'cfg(windows)'.dependencies]` section in `Cargo.toml`, so
|
|
they're not in the Linux build's dependency graph at all — no crate split
|
|
needed to keep the two platforms from stepping on each other.
|
|
|
|
## Why journalctl, not libsystemd
|
|
|
|
The journald source shells out to `journalctl -f -o json` rather than
|
|
linking `libsystemd` via FFI. Statically linking libsystemd into a musl
|
|
binary is fragile — it pulls in dbus/libcap transitively and isn't designed
|
|
for static linking — and would undermine the no-glibc-runtime-deps goal
|
|
even where technically possible. `journalctl` ships on every systemd distro
|
|
this agent targets, so shelling out sidesteps the problem entirely. See
|
|
`/docs/architecture.md`.
|
|
|
|
## Building
|
|
|
|
Native build (whatever target your machine is):
|
|
|
|
```sh
|
|
cargo build --release
|
|
```
|
|
|
|
musl targets (what actually ships):
|
|
|
|
```sh
|
|
rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl
|
|
|
|
# x86_64: works with musl-gcc installed locally (musl-tools on Debian,
|
|
# musl on Arch, etc.) — the musl target is fully static by default.
|
|
cargo build --release --target x86_64-unknown-linux-musl
|
|
|
|
# aarch64 cross-compilation needs a cross toolchain; the boring, reliable
|
|
# option is `cross` (https://github.com/cross-rs/cross), which builds
|
|
# inside a Docker container with the right linker preinstalled:
|
|
cross build --release --target aarch64-unknown-linux-musl
|
|
```
|
|
|
|
Building requires `protoc` on PATH (used by `tonic-build`/`prost-build` at
|
|
compile time to generate the gRPC client from `/proto/sentry/logs/v1/logs.proto`).
|
|
|
|
Container build (see caveat below):
|
|
|
|
```sh
|
|
# from the repo root, not agent/
|
|
docker build -f agent/Dockerfile -t cairnobs-agent .
|
|
```
|
|
|
|
**Caveat:** the container image is provided for CI/completeness, but
|
|
journald sourcing needs `journalctl` and access to the host journal —
|
|
neither of which exist in the `scratch` image or are available to a
|
|
container without deliberately bind-mounting `/var/log/journal` (or
|
|
`/run/log/journal`) and the `journalctl` binary in. The intended Phase 0
|
|
deployment for journald sourcing is as a native binary managed by systemd
|
|
on the host, not containerized.
|
|
|
|
### Building for Windows
|
|
|
|
```sh
|
|
# Cross-compiling FROM Linux, for the build step only:
|
|
rustup target add x86_64-pc-windows-gnu
|
|
cargo build --release --target x86_64-pc-windows-gnu \
|
|
--no-default-features --features windows-eventlog,etw
|
|
|
|
# Natively on Windows (MSVC toolchain):
|
|
cargo build --release --target x86_64-pc-windows-msvc \
|
|
--no-default-features --features windows-eventlog,etw
|
|
```
|
|
|
|
`--no-default-features` matters: the default feature set is `journald`,
|
|
which is Linux-only (the module is `target_os = "linux"`-gated and simply
|
|
won't compile in on Windows, but there's no reason to carry the dead
|
|
feature flag). Drop `,etw` from `--features` if you only want Event Log —
|
|
see the privilege note below for why most environments will want to.
|
|
|
|
**Cross-compilation only covers the *build* step.** Running/testing the
|
|
Windows sources — actually calling `EvtSubscribe`, starting an ETW
|
|
session, registering a Windows service — needs a real or virtualized
|
|
Windows host. There is no way around that, and nothing in this repo
|
|
pretends otherwise; see `/docs/phase-1-runbook.md` for exactly what's
|
|
automatable vs. manual-only.
|
|
|
|
## Running
|
|
|
|
No CLI flags are required for the common case:
|
|
|
|
```sh
|
|
./cairnobs-agent
|
|
```
|
|
|
|
This uses the platform's conventional config path if present
|
|
(`/etc/cairnobs-agent/agent.toml` on Linux, `C:\ProgramData\CairnObsAgent\agent.toml`
|
|
on Windows), otherwise built-in defaults: journald source on Linux (whole
|
|
journal, no unit filter), service name `default`, and mTLS material
|
|
expected under the same conventional directory
|
|
(`{ca,client,client-key}.pem`). mTLS is mandatory per the project's
|
|
transport requirements, so a from-scratch run with no certs in place will
|
|
fail fast with a clear error rather than connecting insecurely.
|
|
|
|
See `config/agent.example.toml` for all fields.
|
|
|
|
```sh
|
|
./cairnobs-agent --config /path/to/agent.toml
|
|
```
|
|
|
|
## Heartbeat and unavailability alerting
|
|
|
|
Every agent sends a small, independent "still alive" record on its own
|
|
schedule (`[heartbeat]` in the config, default every 60s), separate from
|
|
whatever real log traffic is flowing — see `config/agent.example.toml`.
|
|
This isn't a new wire protocol: it's an ordinary record through the same
|
|
`PushBatch` RPC and mTLS identity every log line uses, tagged with a
|
|
`cairnobs.heartbeat=true` attribute so it's easy to filter for and doesn't
|
|
show up as noise in normal log views. Set `interval` to a plain number
|
|
plus `s`/`m`/`h` (matches the query language's own `earliest=`/`latest=`
|
|
units); `enabled = false` turns it off entirely.
|
|
|
|
The platform has no separate "agent status" concept — an agent going
|
|
quiet is just the absence of its heartbeat records, which the existing
|
|
alerting engine already detects natively via an `absence`-condition
|
|
alert rule. See `/docs/agent-heartbeat-monitoring.md` for the exact rule
|
|
to create.
|
|
|
|
## Host CPU/memory/disk metrics
|
|
|
|
Same shape as heartbeat, same reasoning: `[metrics]` in the config
|
|
(`enabled = false` by default) sends a periodic record — CPU%, memory
|
|
used/total, disk used/total for `/` — tagged `cairnobs.metrics=true`, with
|
|
the individual numbers as their own attributes (`cpu_percent`,
|
|
`mem_used_bytes`, `mem_total_bytes`, `disk_used_bytes`,
|
|
`disk_total_bytes`), queryable directly (e.g. `cpu_percent > 80`) since
|
|
the query language transparently maps any non-standard field name to
|
|
`attributes['field']` with automatic numeric casting. Powers the web
|
|
UI's "Hosts" nav section. Linux-only for now (`src/metrics.rs`) — reads
|
|
`/proc/stat`/`/proc/meminfo` and shells out to `df`, no new
|
|
dependencies, same "shell out to a boring, ubiquitous tool" precedent
|
|
`journalctl` already sets.
|
|
|
|
**Enable this on only one agent process per physical host.** It's
|
|
common for one host to run several `cairnobs-agent` processes (one per
|
|
log source, each needing its own `[agent] host` value to work around
|
|
the `agents` table's `UNIQUE (tenant_id, host)` constraint — see
|
|
`/docs/agent-management-design.md`) — turning `[metrics]` on for more
|
|
than one of them reports the same physical machine as multiple
|
|
different "hosts" with conflicting metric series. Pick the one process
|
|
using that host's real, unoverridden hostname.
|
|
|
|
## Running as a Windows service
|
|
|
|
"A native Windows service, not a WSL wrapper" means implementing the Win32
|
|
Service Control Manager protocol, not just running the binary in a
|
|
console — that's what `service.rs` (via the `windows-service` crate)
|
|
does. From an administrator shell:
|
|
|
|
```powershell
|
|
cairnobs-agent.exe install # registers the service, Automatic start, LocalSystem account
|
|
sc.exe start CairnObsAgent
|
|
sc.exe stop CairnObsAgent
|
|
cairnobs-agent.exe uninstall
|
|
```
|
|
|
|
`install`/`uninstall`/`run-service` are subcommands only present in
|
|
Windows builds (`cairnobs-agent` with no subcommand is still the normal
|
|
foreground/console run, same as on Linux) — `run-service` specifically is
|
|
what the SCM itself invokes at service start; don't run it directly.
|
|
|
|
**Known limitation:** when running as a service, there's no console
|
|
attached, so `tracing_subscriber::fmt()`'s stdout writer has nowhere to
|
|
go — logs won't be visible anywhere useful until this is redirected to a
|
|
file or a proper Windows Event Log tracing sink is written. Not addressed
|
|
in Phase 1; flagging it here rather than shipping it silently broken.
|
|
|
|
## ETW: read this before enabling it
|
|
|
|
ETW needs elevated privileges to subscribe to most providers — running
|
|
the agent under an administrator token or a service account with
|
|
`SeSystemProfilePrivilege`/ETW-specific rights. This is a real privilege
|
|
escalation, not a footnote: think about whether your environment wants
|
|
the log-shipping agent running with that level of access before turning
|
|
on the `etw` feature and an `[source] kind = "etw"` config. Event Log
|
|
alone (no elevated privileges needed) covers the common case and is what
|
|
Phase 1's exit criteria in `/CLAUDE.md` actually requires to be running.
|
|
|
|
Providers are configured by **GUID**, not friendly name — ETW's own API
|
|
requires it. Look one up with `logman query providers "<Friendly Name>"`.
|
|
|
|
## Windows Event Forwarding (WEF)
|
|
|
|
Two different things people mean by "WEF support," worth being explicit
|
|
about since they're very different amounts of work:
|
|
|
|
1. **What this repo supports today, with zero extra code:** WEF is a
|
|
native Windows-to-Windows mechanism (`wecsvc`, the built-in Windows
|
|
Event Collector role) — endpoints forward to a Windows Server acting
|
|
as collector using Windows' own mechanism, no Cairn OBS code involved in
|
|
the forwarding itself. Run this agent *on the collector box*,
|
|
subscribed to the `ForwardedEvents` channel instead of the usual three:
|
|
```toml
|
|
[source]
|
|
kind = "eventlog"
|
|
channels = ["ForwardedEvents"]
|
|
```
|
|
2. **What this repo does *not* implement:** a true agentless receiver —
|
|
Cairn OBS itself speaking the WS-Management/WinRM event-subscription
|
|
protocol so endpoints can forward directly to `ingest` without any
|
|
Windows Event Collector role or Cairn OBS agent anywhere. That's a
|
|
standalone protocol implementation (SOAP-ish subscription/heartbeat/
|
|
delivery over WinRM), not an agent or ingest-side tweak, and it's out
|
|
of scope for Phase 1. If you need this, it's a real project of its
|
|
own — say so before assuming it's a small addition.
|
|
|
|
## Testing
|
|
|
|
```sh
|
|
cargo test --workspace
|
|
```
|
|
|
|
## Feature flags
|
|
|
|
- `journald` (default) — journalctl-based journald source. `target_os =
|
|
"linux"`-gated: enabling this on a Windows build is a no-op, not a
|
|
build failure.
|
|
- `file-tail` — polling-based file tailer (no inotify dependency; doesn't
|
|
follow rename-based log rotation yet). Cross-platform, works on Windows
|
|
too.
|
|
- `windows-eventlog` — Windows Event Log via `EvtSubscribe`.
|
|
`target_os = "windows"`-gated the same way; a no-op on Linux.
|
|
- `etw` — ETW real-time session. Same gating. See the privilege section
|
|
above before enabling.
|
|
|
|
Any combination can be enabled together; `[source].kind` in config picks
|
|
which one actually runs. Building without a feature and configuring that
|
|
source at runtime fails at startup with a clear error rather than
|
|
silently doing nothing.
|
|
|
|
Dependencies added for Windows support, worth knowing about:
|
|
`windows` (Microsoft's official Win32/ETW bindings), `windows-service`
|
|
(Windows Service Control Manager wrapper), `quick-xml` (parses
|
|
EvtSubscribe's rendered event XML). All three are `[target.'cfg(windows)'.dependencies]`
|
|
— not in the Linux build's dependency graph at all.
|