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.
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-stdRFC 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):
cargo build --release
musl targets (what actually ships):
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):
# 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
# 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:
./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.
./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:
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:
- 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 theForwardedEventschannel instead of the usual three:[source] kind = "eventlog" channels = ["ForwardedEvents"] - 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
ingestwithout 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
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 viaEvtSubscribe.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.