Rebrand: Sentry -> Cairn OBS

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.
This commit is contained in:
2026-08-21 20:53:32 -07:00
parent 9e21ea17bb
commit 13cf9a30cb
291 changed files with 1565 additions and 1441 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
run: |
go-licenses check ./... \
--allowed_licenses=MIT,Apache-2.0,BSD-2-Clause,BSD-3-Clause,ISC,MPL-2.0,0BSD,Unlicense \
--ignore github.com/sentry/sentry \
--ignore github.com/cairnobs/cairnobs \
--ignore github.com/segmentio/asm
npm-licenses:
+1 -1
View File
@@ -6,7 +6,7 @@ search/target/
# binaries in-tree, but be defensive)
/ingest/ingest
/api/api
/cli/sentryctl
/cli/cairnobsctl
/terraform/terraform-provider-sentry
/alerting/alerting
/hack/windows-fixture/windows-fixture
+17 -17
View File
@@ -1,4 +1,4 @@
# Project: Sentry — Distributed Log Aggregation & Observability Platform
# Project: Cairn OBS — Distributed Log Aggregation & Observability Platform
## Mission
Build an open-core, Kubernetes-native centralized logging platform that rivals
@@ -22,19 +22,19 @@ described there without flagging it to me first.
- Schema-on-write with OTel semantic conventions as the default schema, with
schema-on-read fallback for unstructured text.
- Every UI action must correspond to a documented REST/gRPC call. No
UI-only logic. CLI (`sentryctl`) and Terraform provider are first-class,
not afterthoughts. **Status**: `sentryctl` has been built out phase by
UI-only logic. CLI (`cairnobsctl`) and Terraform provider are first-class,
not afterthoughts. **Status**: `cairnobsctl` has been built out phase by
phase since Phase 3. The Terraform provider (`/terraform`) only exists
as of this note -- four resources (`sentry_dashboard` and
`sentry_dashboard_panel`, both full CRUD, panels as their own resource
as of this note -- four resources (`cairnobs_dashboard` and
`cairnobs_dashboard_panel`, both full CRUD, panels as their own resource
rather than a nested block since the API manages them independently
of their parent dashboard; `sentry_alert_rule` and
`sentry_notification_target`, both create/destroy only -- `alerting`
of their parent dashboard; `cairnobs_alert_rule` and
`cairnobs_notification_target`, both create/destroy only -- `alerting`
has no `PUT /rules/{id}` or `PUT /targets/{id}` to update against),
each paired with a read-only data source, built on HashiCorp's
`terraform-plugin-framework`, reusing the exact same REST contracts
`sentryctl dashboards apply`/web's dashboard export and
`sentryctl alerts apply` already use. Tenant/RBAC resources are real,
`cairnobsctl dashboards apply`/web's dashboard export and
`cairnobsctl alerts apply` already use. Tenant/RBAC resources are real,
disclosed future work -- see
`/terraform/README.md` for the full accounting of what is and isn't
built, and the same
@@ -94,7 +94,7 @@ prominently in `/agent/README.md` and the runbook.
## What "done" looks like for Phase 2
A single query bar in the web UI and a single `sentryctl query` command
A single query bar in the web UI and a single `cairnobsctl query` command
can express filter + free-text + stats in one query (e.g. `service=api |
where status>=500 | stats count by host | sort -count`, or
`message:"connection refused" | stats count by host`), execute correctly
@@ -212,7 +212,7 @@ the fix verified Docker-free (`chrunner_test.go`'s and
tests) — see `api/queryapi/tenant_isolation_gap_test.go` for the full
accounting of all four probes, now all closed. The deployment-
topology gap that briefly was the largest one is now closed for both
Helm and docker-compose: `deploy/helm/sentry/templates/api.yaml`/
Helm and docker-compose: `deploy/helm/cairnobs/templates/api.yaml`/
`enterprise-api.yaml` are mutually exclusive on the same
`enterprise.enabled` flag that turns on RBAC/audit/SSO, rendering to the
same Service name/port either way — a Helm-deployed cluster can't
@@ -234,7 +234,7 @@ stricter still (creator/Admin/Owner only, closing a self-escalation
path). Verified against a fake store (`api/dashboards/handler_test.go`);
real integration tests exist but haven't run against a live Postgres,
same disclosed gap as the rest of this phase's Postgres-backed pieces.
`sentryctl dashboards permissions list|grant|revoke` is now the CLI
`cairnobsctl dashboards permissions list|grant|revoke` is now the CLI
surface for this — `PUT`/`DELETE /dashboards/{id}/permissions/{userId}`
previously had no caller but Go tests and curl.
`deploy/operator`'s `Tenant` CRD and `enterprise-api -provision-tenant`
@@ -483,19 +483,19 @@ phase); CI enforcement wired up and every command verified locally.
**The one real flag — Redpanda's BSL 1.1 license (confirmed against
primary sources for the pinned v24.2.7, not assumed to still be
Apache-2.0) — is resolved, not outstanding**: decision recorded
2026-08-16, accept as-is. Sentry's own use (internal Kafka-protocol
2026-08-16, accept as-is. Cairn OBS's own use (internal Kafka-protocol
transport, no resale of broker access) sits within BSL's Additional Use
Grant; the harder question — whether a third party self-hosting Sentry
Grant; the harder question — whether a third party self-hosting Cairn OBS
"as a service" using the bundled `docker-compose.yml` could trip BSL's
anti-resale restriction on Redpanda specifically — was judged unlikely
given Sentry's ingest pipeline creates fixed internal topics, not
given Cairn OBS's ingest pipeline creates fixed internal topics, not
per-end-user ones, and was accepted as a disclosed, known risk rather
than triggering a swap to Apache Kafka (real resource-footprint cost) or
dropping the bundled broker image (rougher local dev experience). See
the audit report's Redpanda section for the full reasoning, the other
two options that were considered and not chosen, and the condition under
which this should be revisited (an official hosted/managed Sentry
offering, which would make the third-party-SaaS scenario Sentry's own
which this should be revisited (an official hosted/managed Cairn OBS
offering, which would make the third-party-SaaS scenario Cairn OBS's own
rather than a hypothetical one).
Non-goals for this phase: replacing permissively-licensed dependencies
+25 -25
View File
@@ -177,6 +177,31 @@ version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cairnobs-agent"
version = "0.1.0"
dependencies = [
"anyhow",
"cairnobs-parser",
"clap",
"prost",
"quick-xml",
"serde",
"serde_json",
"tokio",
"toml",
"tonic",
"tonic-build",
"tracing",
"tracing-subscriber",
"windows",
"windows-service",
]
[[package]]
name = "cairnobs-parser"
version = "0.1.0"
[[package]]
name = "cc"
version = "1.4.2"
@@ -895,31 +920,6 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "sentry-agent"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"prost",
"quick-xml",
"sentry-parser",
"serde",
"serde_json",
"tokio",
"toml",
"tonic",
"tonic-build",
"tracing",
"tracing-subscriber",
"windows",
"windows-service",
]
[[package]]
name = "sentry-parser"
version = "0.1.0"
[[package]]
name = "serde"
version = "1.0.229"
+1 -1
View File
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["sentry-parser", "sentry-agent"]
members = ["cairnobs-parser", "cairnobs-agent"]
[workspace.package]
version = "0.1.0"
+4 -4
View File
@@ -1,6 +1,6 @@
# Build context must be the repo root (sentry/), not agent/, since this
# needs both agent/ and proto/:
# docker build -f agent/Dockerfile -t sentry-agent .
# docker build -f agent/Dockerfile -t cairnobs-agent .
FROM rust:1-alpine AS builder
RUN apk add --no-cache musl-dev protobuf-dev protobuf
@@ -14,8 +14,8 @@ WORKDIR /src/agent
# alongside the journald default rather than left as an opt-in most
# deployments would otherwise have to remember to ask for.
RUN rustup target add x86_64-unknown-linux-musl \
&& cargo build --release --target x86_64-unknown-linux-musl -p sentry-agent --features journald,file-tail
&& cargo build --release --target x86_64-unknown-linux-musl -p cairnobs-agent --features journald,file-tail
FROM scratch
COPY --from=builder /src/agent/target/x86_64-unknown-linux-musl/release/sentry-agent /sentry-agent
ENTRYPOINT ["/sentry-agent"]
COPY --from=builder /src/agent/target/x86_64-unknown-linux-musl/release/cairnobs-agent /cairnobs-agent
ENTRYPOINT ["/cairnobs-agent"]
+18 -18
View File
@@ -1,4 +1,4 @@
# sentry-agent
# cairnobs-agent
Distro-agnostic Linux/Windows log collector. On Linux, statically linked
against musl, no glibc runtime dependency. Tails journald (Linux default),
@@ -17,9 +17,9 @@ real before trusting it. See `/docs/phase-1-runbook.md`.
## Workspace layout
- `sentry-parser` — pure-`std` RFC 5424 syslog parser with raw-passthrough
- `cairnobs-parser` — pure-`std` RFC 5424 syslog parser with raw-passthrough
fallback. No I/O, easy to unit test in isolation.
- `sentry-agent` — the binary: config loading, sourcing (journald/file/
- `cairnobs-agent` — the binary: config loading, sourcing (journald/file/
Windows Event Log/ETW), batching, mTLS gRPC client, Windows service
wrapper.
@@ -75,7 +75,7 @@ Container build (see caveat below):
```sh
# from the repo root, not agent/
docker build -f agent/Dockerfile -t sentry-agent .
docker build -f agent/Dockerfile -t cairnobs-agent .
```
**Caveat:** the container image is provided for CI/completeness, but
@@ -117,11 +117,11 @@ automatable vs. manual-only.
No CLI flags are required for the common case:
```sh
./sentry-agent
./cairnobs-agent
```
This uses the platform's conventional config path if present
(`/etc/sentry-agent/agent.toml` on Linux, `C:\ProgramData\SentryAgent\agent.toml`
(`/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
@@ -132,7 +132,7 @@ fail fast with a clear error rather than connecting insecurely.
See `config/agent.example.toml` for all fields.
```sh
./sentry-agent --config /path/to/agent.toml
./cairnobs-agent --config /path/to/agent.toml
```
## Heartbeat and unavailability alerting
@@ -142,7 +142,7 @@ 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
`sentry.heartbeat=true` attribute so it's easy to filter for and doesn't
`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.
@@ -157,7 +157,7 @@ to create.
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 `sentry.metrics=true`, with
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
@@ -169,7 +169,7 @@ 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 `sentry-agent` processes (one per
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
@@ -185,14 +185,14 @@ console — that's what `service.rs` (via the `windows-service` crate)
does. From an administrator shell:
```powershell
sentry-agent.exe install # registers the service, Automatic start, LocalSystem account
sc.exe start SentryAgent
sc.exe stop SentryAgent
sentry-agent.exe uninstall
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 (`sentry-agent` with no subcommand is still the normal
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.
@@ -224,7 +224,7 @@ 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 Sentry code involved in
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
@@ -233,9 +233,9 @@ about since they're very different amounts of work:
channels = ["ForwardedEvents"]
```
2. **What this repo does *not* implement:** a true agentless receiver —
Sentry itself speaking the WS-Management/WinRM event-subscription
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 Sentry agent anywhere. That's a
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
@@ -1,12 +1,12 @@
[package]
name = "sentry-agent"
name = "cairnobs-agent"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Sentry distro-agnostic Linux/Windows log collector"
description = "Cairn OBS distro-agnostic Linux/Windows log collector"
[[bin]]
name = "sentry-agent"
name = "cairnobs-agent"
path = "src/main.rs"
[features]
@@ -21,7 +21,7 @@ windows-eventlog = []
etw = []
[dependencies]
sentry-parser = { path = "../sentry-parser" }
cairnobs-parser = { path = "../cairnobs-parser" }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "io-util", "io-std", "time", "fs", "sync", "signal"] }
tonic = { version = "0.12", features = ["tls"] }
@@ -1,12 +1,12 @@
# Example sentry-agent config. Copy to the platform's conventional path
# (/etc/sentry-agent/agent.toml on Linux, C:\ProgramData\SentryAgent\agent.toml
# Example cairnobs-agent config. Copy to the platform's conventional path
# (/etc/cairnobs-agent/agent.toml on Linux, C:\ProgramData\CairnObsAgent\agent.toml
# on Windows), or pass --config /path/to/this/file.
#
# Every field has a built-in default (see src/config.rs), so this file only
# needs to contain what you're overriding. An agent with NO config file at
# all still runs: on Linux it defaults to journald, service = "default",
# and expects mTLS material at /etc/sentry-agent/{ca,client,client-key}.pem
# (Windows equivalents under C:\ProgramData\SentryAgent\).
# and expects mTLS material at /etc/cairnobs-agent/{ca,client,client-key}.pem
# (Windows equivalents under C:\ProgramData\CairnObsAgent\).
[agent]
# host = "explicit-hostname-override" # defaults to /etc/hostname (Linux) or %COMPUTERNAME% (Windows)
@@ -41,7 +41,7 @@ flush_interval_ms = 2000
[heartbeat]
# How often this agent proves it's still alive to the platform, sent as
# its own record independent of whatever real log traffic is flowing --
# pair with an "absence" alert rule on the sentry.heartbeat attribute to
# pair with an "absence" alert rule on the cairnobs.heartbeat attribute to
# get paged when a host goes quiet. Accepts a plain number + unit: s
# (seconds), m (minutes), or h (hours) -- same vocabulary as
# earliest=/latest= in the query language. See
@@ -55,7 +55,7 @@ interval = "60s"
# Host CPU/memory/disk usage, sent as its own periodic record the same
# way heartbeat is (see web/'s "Hosts" nav section). Off by default --
# unlike heartbeat, this is a deliberate per-host decision: if several
# sentry-agent processes run on the same physical host (e.g. one per log
# cairnobs-agent processes run on the same physical host (e.g. one per log
# source), enable this on only ONE of them, or the same host will report
# multiple conflicting metric series. Linux-only for now. Root disk ("/")
# only -- not configurable in this release.
@@ -66,6 +66,6 @@ interval = "60s"
endpoint = "https://ingest.internal:4317"
[tls]
ca_cert = "/etc/sentry-agent/ca.pem"
client_cert = "/etc/sentry-agent/client.pem"
client_key = "/etc/sentry-agent/client-key.pem"
ca_cert = "/etc/cairnobs-agent/ca.pem"
client_cert = "/etc/cairnobs-agent/client.pem"
client_key = "/etc/cairnobs-agent/client-key.pem"
@@ -4,9 +4,9 @@ use std::path::{Path, PathBuf};
use std::time::Duration;
#[cfg(not(windows))]
const DEFAULT_CONFIG_PATH: &str = "/etc/sentry-agent/agent.toml";
const DEFAULT_CONFIG_PATH: &str = "/etc/cairnobs-agent/agent.toml";
#[cfg(windows)]
const DEFAULT_CONFIG_PATH: &str = r"C:\ProgramData\SentryAgent\agent.toml";
const DEFAULT_CONFIG_PATH: &str = r"C:\ProgramData\CairnObsAgent\agent.toml";
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
@@ -23,8 +23,8 @@ pub struct Config {
impl Config {
/// Loads config from `explicit_path` if given, else from the
/// platform's conventional config path if it exists
/// (`/etc/sentry-agent/agent.toml` on Linux,
/// `C:\ProgramData\SentryAgent\agent.toml` on Windows), else falls
/// (`/etc/cairnobs-agent/agent.toml` on Linux,
/// `C:\ProgramData\CairnObsAgent\agent.toml` on Windows), else falls
/// back to built-in defaults (journald source on Linux, default TLS
/// cert paths). Only an explicitly-passed `--config` path that doesn't
/// exist is an error; the conventional default path is optional.
@@ -150,7 +150,7 @@ impl Default for BatchConfig {
/// `flush_interval_ms` like real records do. This is the operator-facing
/// "polling resolution" knob: how often this agent proves it's still
/// alive, which a `condition_type = "absence"` alert rule on the
/// `sentry.heartbeat` attribute (see /docs/agent-heartbeat-monitoring.md)
/// `cairnobs.heartbeat` attribute (see /docs/agent-heartbeat-monitoring.md)
/// turns into "alert when this host goes quiet."
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
@@ -321,10 +321,10 @@ impl Default for TlsConfig {
#[cfg(not(windows))]
fn default_cert_path(name: &str) -> PathBuf {
PathBuf::from(format!("/etc/sentry-agent/{name}"))
PathBuf::from(format!("/etc/cairnobs-agent/{name}"))
}
#[cfg(windows)]
fn default_cert_path(name: &str) -> PathBuf {
PathBuf::from(format!(r"C:\ProgramData\SentryAgent\{name}"))
PathBuf::from(format!(r"C:\ProgramData\CairnObsAgent\{name}"))
}
@@ -31,7 +31,7 @@ use tokio::sync::mpsc;
use tonic::transport::Channel;
#[derive(Parser)]
#[command(name = "sentry-agent", about = "Sentry Linux/Windows log collector")]
#[command(name = "cairnobs-agent", about = "Cairn OBS Linux/Windows log collector")]
struct Cli {
/// Path to a TOML config file. Defaults to the platform's conventional
/// path if present, otherwise built-in defaults — see config::Config::load.
@@ -53,7 +53,7 @@ enum WindowsCommand {
Uninstall,
/// Entry point the Service Control Manager invokes when starting the
/// registered service. Not meant to be run directly by a user — use
/// `sentry-agent` with no subcommand for a normal foreground/console
/// `cairnobs-agent` with no subcommand for a normal foreground/console
/// run, same as on Linux.
RunService,
}
@@ -244,7 +244,7 @@ pub async fn run_agent(config_path: Option<PathBuf>) -> Result<()> {
tracing::warn!("source exited, flushing remaining batch and shutting down");
break;
};
let parsed = sentry_parser::parse(&raw.line);
let parsed = cairnobs_parser::parse(&raw.line);
let severity = to_pb_severity(raw.severity_hint.or(parsed.severity));
let mut attributes: std::collections::HashMap<String, String> =
parsed.attributes.into_iter().collect();
@@ -476,7 +476,7 @@ async fn spawn_source(source: config::SourceConfig, tx: source::LineSender) {
/// ticker's own comment above): a heartbeat that got queued behind
/// `batch.max_size` or `batch.flush_interval_ms` would defeat the point
/// of a punctual "still alive" signal. Distinguished from a real log
/// record purely by the `sentry.heartbeat` attribute -- `service` stays
/// record purely by the `cairnobs.heartbeat` attribute -- `service` stays
/// the agent's real configured service so it doesn't pollute
/// service-based dashboards/faceting with a fake value. See
/// /docs/agent-heartbeat-monitoring.md for how an absence alert rule
@@ -488,7 +488,7 @@ async fn send_heartbeat(client: &mut LogIngestClient<Channel>, host: &str, servi
service: service.to_string(),
severity: Severity::Info as i32,
message: "agent heartbeat".to_string(),
attributes: std::collections::HashMap::from([("sentry.heartbeat".to_string(), "true".to_string())]),
attributes: std::collections::HashMap::from([("cairnobs.heartbeat".to_string(), "true".to_string())]),
record_id: String::new(),
};
match grpc::send_batch(client, format!("heartbeat-{}", batch_id()), vec![record]).await {
@@ -499,7 +499,7 @@ async fn send_heartbeat(client: &mut LogIngestClient<Channel>, host: &str, servi
/// Same "no new proto, no new ingest code, no new ClickHouse schema"
/// shape as `send_heartbeat` above -- a metrics sample is just another
/// tagged `LogRecord`, distinguished by the `sentry.metrics` attribute.
/// tagged `LogRecord`, distinguished by the `cairnobs.metrics` attribute.
/// Unlike heartbeat, the numeric fields themselves are real query-language
/// attributes too (`cpu_percent`, `mem_used_bytes`, etc.) rather than
/// being folded into `message` -- confirmed before building this that
@@ -523,7 +523,7 @@ async fn send_metrics(client: &mut LogIngestClient<Channel>, host: &str, service
severity: Severity::Info as i32,
message: "host metrics".to_string(),
attributes: std::collections::HashMap::from([
("sentry.metrics".to_string(), "true".to_string()),
("cairnobs.metrics".to_string(), "true".to_string()),
("cpu_percent".to_string(), format!("{:.2}", m.cpu_percent)),
("mem_used_bytes".to_string(), m.mem_used_bytes.to_string()),
("mem_total_bytes".to_string(), m.mem_total_bytes.to_string()),
@@ -27,12 +27,12 @@ use windows_service::service_control_handler::{self, ServiceControlHandlerResult
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
use windows_service::{define_windows_service, service_dispatcher};
pub const SERVICE_NAME: &str = "SentryAgent";
pub const SERVICE_NAME: &str = "CairnObsAgent";
const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
/// Registers this binary as a Windows service: Automatic start,
/// LocalSystem account, invoked with the `run-service` subcommand (which
/// is what the SCM actually launches — not a bare `sentry-agent` with no
/// is what the SCM actually launches — not a bare `cairnobs-agent` with no
/// arguments). Requires an administrator shell.
pub fn install() -> Result<()> {
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CREATE_SERVICE)
@@ -42,7 +42,7 @@ pub fn install() -> Result<()> {
let service_info = ServiceInfo {
name: OsString::from(SERVICE_NAME),
display_name: OsString::from("Sentry Log Agent"),
display_name: OsString::from("Cairn OBS Log Agent"),
service_type: SERVICE_TYPE,
start_type: ServiceStartType::AutoStart,
error_control: ServiceErrorControl::Normal,
@@ -57,7 +57,7 @@ pub fn install() -> Result<()> {
.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)
.context("creating service")?;
service
.set_description("Ships local logs to Sentry ingest over mTLS.")
.set_description("Ships local logs to Cairn OBS ingest over mTLS.")
.context("setting service description")?;
tracing::info!(service = SERVICE_NAME, "installed Windows service");
@@ -44,7 +44,7 @@ use windows::Win32::System::Diagnostics::Etw::{
PROCESS_TRACE_MODE_REAL_TIME, TRACE_LEVEL_VERBOSE,
};
const SESSION_NAME: &str = "SentryAgentEtw";
const SESSION_NAME: &str = "CairnObsAgentEtw";
pub async fn run(providers: &[String], tx: LineSender) -> Result<()> {
let providers = providers.to_vec();
@@ -212,7 +212,7 @@ unsafe extern "system" fn event_record_callback(record: *mut EVENT_RECORD) {
// No TDH-based message rendering (see module doc comment) -- this is
// a coarse, structured summary rather than a human-authored message.
// Downstream (sentry_parser's raw-passthrough fallback) handles a
// Downstream (cairnobs_parser's raw-passthrough fallback) handles a
// non-RFC5424 line like this the same as any other raw line.
let message = format!(
"ETW event: provider={:?} id={} level={}",
@@ -226,7 +226,7 @@ fn parse_event_xml(xml: &str, channel: &str) -> Option<RawLine> {
// rendered properly. Real message-template rendering needs
// EvtFormatMessage against the provider's message-table resource --
// worth a follow-up, not required for a raw-passthrough-shaped record
// (sentry_parser's raw fallback handles this fine either way).
// (cairnobs_parser's raw fallback handles this fine either way).
let message = if event_data_values.is_empty() {
xml.to_string()
} else {
@@ -1,5 +1,5 @@
[package]
name = "sentry-parser"
name = "cairnobs-parser"
version.workspace = true
edition.workspace = true
license.workspace = true
+1 -1
View File
@@ -88,7 +88,7 @@ ignore = [
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
# Sentry's own AGPLv3-project license policy (Phase 6 license audit --
# Cairn OBS's own AGPLv3-project license policy (Phase 6 license audit --
# see /docs/compliance/license-policy.md for the full rationale per
# category). Every license actually found in this crate's dependency
# tree at audit time is listed explicitly here, not just the common
+1 -1
View File
@@ -8,7 +8,7 @@ correctness properties this implementation follows exactly).
## Running
```sh
POSTGRES_PASSWORD=sentry-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`
+11 -11
View File
@@ -1,9 +1,9 @@
// Command alerting is Sentry's alert rule evaluator and delivery
// Command alerting is Cairn OBS's alert rule evaluator and delivery
// service: rule/target CRUD, the ticker-driven ok/pending/firing
// evaluator, and the webhook/Slack/PagerDuty delivery worker. See
// /docs/phase-3-alerting-design.md. Never talks to ClickHouse/Tantivy
// directly -- rule queries run through /api's POST /query
// (internal/queryclient), same precedent sentryctl query and the web
// (internal/queryclient), same precedent cairnobsctl query and the web
// UI's dashboard panels already set.
package main
@@ -21,15 +21,15 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"golang.org/x/sync/errgroup"
"github.com/sentry/sentry/alerting/internal/config"
"github.com/sentry/sentry/alerting/internal/delivery"
"github.com/sentry/sentry/alerting/internal/evaluator"
"github.com/sentry/sentry/alerting/internal/httpapi"
"github.com/sentry/sentry/alerting/internal/httpserver"
"github.com/sentry/sentry/alerting/internal/notifystore"
"github.com/sentry/sentry/alerting/internal/queryclient"
"github.com/sentry/sentry/alerting/internal/rulestore"
"github.com/sentry/sentry/alerting/internal/sessioncheck"
"github.com/cairnobs/cairnobs/alerting/internal/config"
"github.com/cairnobs/cairnobs/alerting/internal/delivery"
"github.com/cairnobs/cairnobs/alerting/internal/evaluator"
"github.com/cairnobs/cairnobs/alerting/internal/httpapi"
"github.com/cairnobs/cairnobs/alerting/internal/httpserver"
"github.com/cairnobs/cairnobs/alerting/internal/notifystore"
"github.com/cairnobs/cairnobs/alerting/internal/queryclient"
"github.com/cairnobs/cairnobs/alerting/internal/rulestore"
"github.com/cairnobs/cairnobs/alerting/internal/sessioncheck"
)
func main() {
+1 -1
View File
@@ -1,4 +1,4 @@
module github.com/sentry/sentry/alerting
module github.com/cairnobs/cairnobs/alerting
go 1.25.0
+1 -1
View File
@@ -53,7 +53,7 @@ type EvaluatorConfig struct {
// api/internal/config.Config.DevCredentialWarnings for the full
// reasoning (duplicated here per this repo's no-shared-code-between-
// services convention).
const devOnlyCredential = "sentry-dev-only"
const devOnlyCredential = "cairnobs-dev-only"
// DevCredentialWarnings reports whether the configured Postgres
// credential still equals the literal dev-only default --
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"testing"
"time"
"github.com/sentry/sentry/alerting/internal/notifystore"
"github.com/cairnobs/cairnobs/alerting/internal/notifystore"
)
func TestBuildPayloadGenericDefaultShape(t *testing.T) {
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/sentry/sentry/alerting/internal/notifystore"
"github.com/cairnobs/cairnobs/alerting/internal/notifystore"
)
// pagerDutyPayload is PagerDuty's Events API v2 shape. routing_key comes
@@ -50,7 +50,7 @@ func buildPagerDutyPayload(target notifystore.Target, event Event) ([]byte, erro
DedupKey: event.RuleID,
Payload: pagerDutyEventPayload{
Summary: summary,
Source: "sentry",
Source: "cairnobs",
Severity: severity,
},
})
+1 -1
View File
@@ -19,7 +19,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"github.com/sentry/sentry/alerting/internal/notifystore"
"github.com/cairnobs/cairnobs/alerting/internal/notifystore"
)
// Event is the firing/resolved occurrence a payload is rendered from.
+2 -2
View File
@@ -3,8 +3,8 @@ package evaluator
import (
"fmt"
"github.com/sentry/sentry/alerting/internal/queryclient"
"github.com/sentry/sentry/alerting/internal/rulestore"
"github.com/cairnobs/cairnobs/alerting/internal/queryclient"
"github.com/cairnobs/cairnobs/alerting/internal/rulestore"
)
// evaluateCondition implements fixes 3 and 4 from
@@ -3,8 +3,8 @@ package evaluator
import (
"testing"
"github.com/sentry/sentry/alerting/internal/queryclient"
"github.com/sentry/sentry/alerting/internal/rulestore"
"github.com/cairnobs/cairnobs/alerting/internal/queryclient"
"github.com/cairnobs/cairnobs/alerting/internal/rulestore"
)
func thresholdRule(comparator rulestore.Comparator, threshold float64) rulestore.Rule {
+4 -4
View File
@@ -6,10 +6,10 @@ import (
"sync"
"time"
"github.com/sentry/sentry/alerting/internal/delivery"
"github.com/sentry/sentry/alerting/internal/notifystore"
"github.com/sentry/sentry/alerting/internal/queryclient"
"github.com/sentry/sentry/alerting/internal/rulestore"
"github.com/cairnobs/cairnobs/alerting/internal/delivery"
"github.com/cairnobs/cairnobs/alerting/internal/notifystore"
"github.com/cairnobs/cairnobs/alerting/internal/queryclient"
"github.com/cairnobs/cairnobs/alerting/internal/rulestore"
)
// Evaluator is the ticker-driven scheduler -- a bounded worker pool, not
+1 -1
View File
@@ -10,7 +10,7 @@ package evaluator
import (
"time"
"github.com/sentry/sentry/alerting/internal/rulestore"
"github.com/cairnobs/cairnobs/alerting/internal/rulestore"
)
// TransitionInput is everything ComputeTransition needs: the rule's
@@ -4,7 +4,7 @@ import (
"testing"
"time"
"github.com/sentry/sentry/alerting/internal/rulestore"
"github.com/cairnobs/cairnobs/alerting/internal/rulestore"
)
var t0 = time.Date(2026, 8, 14, 0, 0, 0, 0, time.UTC)
+2 -2
View File
@@ -11,8 +11,8 @@ import (
"log/slog"
"net/http"
"github.com/sentry/sentry/alerting/internal/notifystore"
"github.com/sentry/sentry/alerting/internal/rulestore"
"github.com/cairnobs/cairnobs/alerting/internal/notifystore"
"github.com/cairnobs/cairnobs/alerting/internal/rulestore"
)
type ruleStore interface {
+2 -2
View File
@@ -9,8 +9,8 @@ import (
"strings"
"testing"
"github.com/sentry/sentry/alerting/internal/notifystore"
"github.com/sentry/sentry/alerting/internal/rulestore"
"github.com/cairnobs/cairnobs/alerting/internal/notifystore"
"github.com/cairnobs/cairnobs/alerting/internal/rulestore"
)
type fakeRuleStore struct {
+1 -1
View File
@@ -1,6 +1,6 @@
// Package queryclient is a thin HTTP client to /api's POST /query --
// alerting never imports querylang or talks to ClickHouse/Tantivy
// directly, same precedent sentryctl query and the web UI's dashboard
// directly, same precedent cairnobsctl query and the web UI's dashboard
// panels already set: one query-execution path, reused everywhere.
package queryclient
+1 -1
View File
@@ -10,7 +10,7 @@ import (
// imported -- see this package's doc comment) -- the same cookie
// api/localauth.Handler.setCookie writes, scoped (via SESSION_COOKIE_
// DOMAIN) to cover both api's and alerting's subdomains in production.
const sessionCookieName = "sentry_local_session"
const sessionCookieName = "cairnobs_local_session"
type errorResponse struct {
Error string `json:"error"`
@@ -9,7 +9,7 @@
//
// docker run --rm --network sentry_default -v $(pwd)/../../..:/src -w /src/alerting \
// -e SESSIONCHECK_TEST_POSTGRES_ADDR=metadata-postgres:5432 \
// -e SESSIONCHECK_TEST_POSTGRES_PASSWORD=sentry-dev-only \
// -e SESSIONCHECK_TEST_POSTGRES_PASSWORD=cairnobs-dev-only \
// golang:1.25-alpine go test ./internal/sessioncheck/... -run Integration -v
package sessioncheck
+1 -1
View File
@@ -1,6 +1,6 @@
# api
Sentry's query API: a single `POST /query` endpoint accepting either the
Cairn OBS's query API: a single `POST /query` endpoint accepting either the
pipe syntax or raw SQL, compiled and routed across ClickHouse and Tantivy
by `internal/querylang`. Replaces Phase 0/1's two separate placeholder
endpoints (raw-SQL-only `/query`, free-text-only `/search`) — see
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"path"
"strings"
"github.com/sentry/sentry/api/authz"
"github.com/cairnobs/cairnobs/api/authz"
)
// store is the narrow interface Handler depends on -- *Store (store.go)
+2 -2
View File
@@ -11,7 +11,7 @@ import (
"net/http/httptest"
"testing"
"github.com/sentry/sentry/api/authz"
"github.com/cairnobs/cairnobs/api/authz"
)
func discardLogger() *slog.Logger {
@@ -259,7 +259,7 @@ func TestHandleSetConfigDenylistsSensitivePaths(t *testing.T) {
"/home/alice/.ssh/id_rsa",
"/home/alice/.ssh/authorized_keys",
"/proc/1/environ",
"/etc/sentry-agent/client-key.pem",
"/etc/cairnobs-agent/client-key.pem",
"/opt/app/../../etc/shadow",
"/opt/app/id_ed25519",
}
+6 -6
View File
@@ -22,12 +22,12 @@ import (
"strings"
"time"
"github.com/sentry/sentry/api/ai/costguard"
"github.com/sentry/sentry/api/ai/provider"
"github.com/sentry/sentry/api/ai/router"
"github.com/sentry/sentry/api/authz"
"github.com/sentry/sentry/api/internal/querylang/ir"
"github.com/sentry/sentry/api/internal/querylang/planner"
"github.com/cairnobs/cairnobs/api/ai/costguard"
"github.com/cairnobs/cairnobs/api/ai/provider"
"github.com/cairnobs/cairnobs/api/ai/router"
"github.com/cairnobs/cairnobs/api/authz"
"github.com/cairnobs/cairnobs/api/internal/querylang/ir"
"github.com/cairnobs/cairnobs/api/internal/querylang/planner"
)
// SchemaContextSource resolves the calling tenant's grounding data.
+2 -2
View File
@@ -10,8 +10,8 @@ import (
"net/http/httptest"
"testing"
"github.com/sentry/sentry/api/ai/provider"
"github.com/sentry/sentry/api/ai/router"
"github.com/cairnobs/cairnobs/api/ai/provider"
"github.com/cairnobs/cairnobs/api/ai/router"
)
type fakeProvider struct {
+2 -2
View File
@@ -29,8 +29,8 @@ import (
"net/http/httptest"
"testing"
"github.com/sentry/sentry/api/ai/provider/ollama"
"github.com/sentry/sentry/api/ai/router"
"github.com/cairnobs/cairnobs/api/ai/provider/ollama"
"github.com/cairnobs/cairnobs/api/ai/router"
)
// jsonBody marshals v for use as an http.Post body -- the integration
+1 -1
View File
@@ -21,7 +21,7 @@ import (
"strings"
"time"
"github.com/sentry/sentry/api/internal/querylang/ir"
"github.com/cairnobs/cairnobs/api/internal/querylang/ir"
)
type Level string
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"testing"
"time"
"github.com/sentry/sentry/api/internal/querylang/ir"
"github.com/cairnobs/cairnobs/api/internal/querylang/ir"
)
// A raw-row (non-aggregation) query with no time range and no explicit
+2 -2
View File
@@ -25,8 +25,8 @@ import (
"sync"
"time"
"github.com/sentry/sentry/api/ai/provider"
"github.com/sentry/sentry/api/querylang/executor"
"github.com/cairnobs/cairnobs/api/ai/provider"
"github.com/cairnobs/cairnobs/api/querylang/executor"
)
// staticFields are always present regardless of what's actually been
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"testing"
"time"
"github.com/sentry/sentry/api/ai/provider"
"github.com/sentry/sentry/api/querylang/executor"
"github.com/cairnobs/cairnobs/api/ai/provider"
"github.com/cairnobs/cairnobs/api/querylang/executor"
)
// routingFakeRunner returns a canned result keyed by a substring match
+1 -1
View File
@@ -15,7 +15,7 @@ import (
"net/http"
"strings"
"github.com/sentry/sentry/api/ai/provider"
"github.com/cairnobs/cairnobs/api/ai/provider"
)
// Client implements provider.Provider against one Ollama server and one
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"strings"
"testing"
"github.com/sentry/sentry/api/ai/provider"
"github.com/cairnobs/cairnobs/api/ai/provider"
)
// fakeOllamaServer stands in for a real Ollama server, returning the
+8 -8
View File
@@ -4,18 +4,18 @@ import (
"fmt"
"strings"
"github.com/sentry/sentry/api/ai/provider"
"github.com/cairnobs/cairnobs/api/ai/provider"
)
// grammarReference is a condensed version of
// /docs/query-language-reference.md -- every operation's system prompt
// includes this so the model is grounded in Sentry's actual pipe syntax,
// includes this so the model is grounded in Cairn OBS's actual pipe syntax,
// not whatever generic log-query DSL it may have seen in training.
// Trimmed to the parts that matter for generation/explanation (the full
// doc's prose and examples aren't needed here); kept in sync with that
// doc by hand -- if the grammar changes, this needs updating too, same
// as any other place the language is described outside its own parser.
const grammarReference = `Sentry query language (pipe syntax):
const grammarReference = `Cairn OBS query language (pipe syntax):
<base search> | <stage> | <stage> | ...
@@ -64,7 +64,7 @@ func renderSchema(s provider.SchemaContext) string {
}
func translateSystemPrompt(schema provider.SchemaContext) string {
return fmt.Sprintf(`You translate a plain-English question into a Sentry pipe-syntax query. You never explain, never execute anything, never write raw SQL unless the pipe syntax genuinely cannot express the request.
return fmt.Sprintf(`You translate a plain-English question into a Cairn OBS pipe-syntax query. You never explain, never execute anything, never write raw SQL unless the pipe syntax genuinely cannot express the request.
%s
@@ -77,7 +77,7 @@ If you cannot produce a query you're reasonably confident in, set confidence to
}
func completeSystemPrompt(schema provider.SchemaContext) string {
return fmt.Sprintf(`You suggest how to continue a partially-typed Sentry query. You are given everything typed so far; respond with ONLY the suggested continuation text (what should appear after the cursor), not the text already typed, not an explanation.
return fmt.Sprintf(`You suggest how to continue a partially-typed Cairn OBS query. You are given everything typed so far; respond with ONLY the suggested continuation text (what should appear after the cursor), not the text already typed, not an explanation.
%s
@@ -94,12 +94,12 @@ Respond with ONLY a JSON object, no other text, no markdown fences:
// ExplainRequest.RuleFindings' doc comment.
func explainSystemPrompt(hasIntent, hasFindings bool) string {
if hasFindings {
return fmt.Sprintf(`A rule-based check already found one or more real issues with a Sentry query's efficiency (e.g. a missing time range). Your only job is to phrase those findings as a short, clear, actionable suggestion for the person who wrote the query -- do not invent additional issues, do not restate the query's own syntax back at them, do not hedge with "might" or "could" about something the check already confirmed. One or two sentences.
return fmt.Sprintf(`A rule-based check already found one or more real issues with a Cairn OBS query's efficiency (e.g. a missing time range). Your only job is to phrase those findings as a short, clear, actionable suggestion for the person who wrote the query -- do not invent additional issues, do not restate the query's own syntax back at them, do not hedge with "might" or "could" about something the check already confirmed. One or two sentences.
%s`, grammarReference)
}
base := fmt.Sprintf(`You explain what a Sentry query does in plain English, for someone who may not know the query language. Be concise -- two or three sentences, not a line-by-line breakdown unless the query is unusually complex.
base := fmt.Sprintf(`You explain what a Cairn OBS query does in plain English, for someone who may not know the query language. Be concise -- two or three sentences, not a line-by-line breakdown unless the query is unusually complex.
%s`, grammarReference)
if hasIntent {
@@ -109,7 +109,7 @@ func explainSystemPrompt(hasIntent, hasFindings bool) string {
}
func fixSystemPrompt(schema provider.SchemaContext) string {
return fmt.Sprintf(`You fix a broken Sentry query given its error message. Produce a corrected query and a short explanation of what was wrong.
return fmt.Sprintf(`You fix a broken Cairn OBS query given its error message. Produce a corrected query and a short explanation of what was wrong.
%s
+1 -1
View File
@@ -14,7 +14,7 @@
// consume it).
package router
import "github.com/sentry/sentry/api/ai/provider"
import "github.com/cairnobs/cairnobs/api/ai/provider"
type Operation string
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context"
"testing"
"github.com/sentry/sentry/api/ai/provider"
"github.com/cairnobs/cairnobs/api/ai/provider"
)
// namedFakeProvider lets a test tell which configured provider actually
+2 -2
View File
@@ -19,7 +19,7 @@ func TestHTTPAuthorizerForwardsCredentialsAndParsesIdentity(t *testing.T) {
a := NewHTTPAuthorizer(srv.URL)
incoming := httptest.NewRequest(http.MethodPost, "/query", nil)
incoming.Header.Set("Cookie", "sentry_session=abc123")
incoming.Header.Set("Cookie", "cairnobs_session=abc123")
incoming.Header.Set("Authorization", "Bearer service-token-xyz")
identity, err := a.Authorize(incoming)
@@ -29,7 +29,7 @@ func TestHTTPAuthorizerForwardsCredentialsAndParsesIdentity(t *testing.T) {
if identity.TenantID != "acme" || identity.UserID != "u1" || identity.Role != RoleEditor {
t.Fatalf("unexpected identity: %+v", identity)
}
if gotCookie != "sentry_session=abc123" {
if gotCookie != "cairnobs_session=abc123" {
t.Fatalf("Cookie header not forwarded, got %q", gotCookie)
}
if gotAuth != "Bearer service-token-xyz" {
+15 -15
View File
@@ -1,4 +1,4 @@
// Command api is Sentry's query API: a single POST /query endpoint
// Command api is Cairn OBS's query API: a single POST /query endpoint
// accepting either the pipe syntax or raw SQL, compiled and routed
// across ClickHouse and search by internal/querylang. See
// queryapi and /docs/query-language-design.md for why this is
@@ -23,20 +23,20 @@ import (
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/sentry/sentry/api/agents"
"github.com/sentry/sentry/api/ai/aiapi"
"github.com/sentry/sentry/api/ai/grounding"
"github.com/sentry/sentry/api/ai/provider/ollama"
"github.com/sentry/sentry/api/ai/router"
"github.com/sentry/sentry/api/authz"
"github.com/sentry/sentry/api/dashboards"
"github.com/sentry/sentry/api/httpserver"
"github.com/sentry/sentry/api/internal/config"
"github.com/sentry/sentry/api/localauth"
"github.com/sentry/sentry/api/logretention"
"github.com/sentry/sentry/api/queryapi"
"github.com/sentry/sentry/api/querylang/executor"
"github.com/sentry/sentry/api/searchclient"
"github.com/cairnobs/cairnobs/api/agents"
"github.com/cairnobs/cairnobs/api/ai/aiapi"
"github.com/cairnobs/cairnobs/api/ai/grounding"
"github.com/cairnobs/cairnobs/api/ai/provider/ollama"
"github.com/cairnobs/cairnobs/api/ai/router"
"github.com/cairnobs/cairnobs/api/authz"
"github.com/cairnobs/cairnobs/api/dashboards"
"github.com/cairnobs/cairnobs/api/httpserver"
"github.com/cairnobs/cairnobs/api/internal/config"
"github.com/cairnobs/cairnobs/api/localauth"
"github.com/cairnobs/cairnobs/api/logretention"
"github.com/cairnobs/cairnobs/api/queryapi"
"github.com/cairnobs/cairnobs/api/querylang/executor"
"github.com/cairnobs/cairnobs/api/searchclient"
)
// groundingRefreshInterval matches chwriter.Registry/search's
+2 -2
View File
@@ -7,7 +7,7 @@ import (
"log/slog"
"net/http"
"github.com/sentry/sentry/api/authz"
"github.com/cairnobs/cairnobs/api/authz"
)
// store is the narrow interface Handler depends on -- *Store (store.go)
@@ -187,7 +187,7 @@ func (h *Handler) handleGet(w http.ResponseWriter, r *http.Request) {
func (h *Handler) handleExport(w http.ResponseWriter, r *http.Request) {
// Export is the same document GET /dashboards/{id} returns -- the
// import endpoint below consumes exactly this shape, and so does
// `sentryctl dashboards apply`, so there's one JSON contract used
// `cairnobsctl dashboards apply`, so there's one JSON contract used
// from every call site rather than a bespoke export format.
h.handleGet(w, r)
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"strings"
"testing"
"github.com/sentry/sentry/api/authz"
"github.com/cairnobs/cairnobs/api/authz"
)
// fakeStore enforces tenant scoping the same way store.go's real
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context"
"time"
"github.com/sentry/sentry/api/authz"
"github.com/cairnobs/cairnobs/api/authz"
)
// Permission is one dashboard_permissions row -- see
+1 -1
View File
@@ -11,7 +11,7 @@
//
// docker run --rm --network sentry_default -v $(pwd)/../../..:/src -w /src/api \
// -e DASHBOARDS_TEST_POSTGRES_ADDR=metadata-postgres:5432 \
// -e DASHBOARDS_TEST_POSTGRES_PASSWORD=sentry-dev-only \
// -e DASHBOARDS_TEST_POSTGRES_PASSWORD=cairnobs-dev-only \
// golang:1.25-alpine go test ./dashboards/... -run Integration -v
package dashboards
+3 -3
View File
@@ -1,4 +1,4 @@
module github.com/sentry/sentry/api
module github.com/cairnobs/cairnobs/api
go 1.25.0
@@ -6,12 +6,12 @@ require (
github.com/ClickHouse/clickhouse-go/v2 v2.48.0
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.10.0
github.com/sentry/sentry/proto v0.0.0-00010101000000-000000000000
github.com/cairnobs/cairnobs/proto v0.0.0-00010101000000-000000000000
golang.org/x/crypto v0.55.0
google.golang.org/grpc v1.83.0
)
replace github.com/sentry/sentry/proto => ../proto
replace github.com/cairnobs/cairnobs/proto => ../proto
require (
github.com/ClickHouse/ch-go v0.74.0 // indirect
+1 -1
View File
@@ -82,7 +82,7 @@ type PostgresConfig struct {
// real deployment that skips docker-compose.override.yml would
// otherwise go live with a password anyone can read straight off
// GitHub. See DevCredentialWarnings.
const devOnlyCredential = "sentry-dev-only"
const devOnlyCredential = "cairnobs-dev-only"
// DevCredentialWarnings reports which configured credentials still
// equal docker-compose.yml's literal dev-only default -- cmd/api/main.go
+1 -1
View File
@@ -33,7 +33,7 @@ func TestLoadInvalidTimeoutErrors(t *testing.T) {
// TestDevCredentialWarnings is the regression test for the
// security-audit finding that docker-compose.yml's hardcoded
// "sentry-dev-only" password has no runtime fail-safe if an operator
// "cairnobs-dev-only" password has no runtime fail-safe if an operator
// forgets to override it for a real deployment.
func TestDevCredentialWarnings(t *testing.T) {
if got := (Config{}).DevCredentialWarnings(); len(got) != 0 {
+2 -2
View File
@@ -9,8 +9,8 @@ import (
"fmt"
"strconv"
"github.com/sentry/sentry/api/internal/querylang/ast"
"github.com/sentry/sentry/api/internal/querylang/lexer"
"github.com/cairnobs/cairnobs/api/internal/querylang/ast"
"github.com/cairnobs/cairnobs/api/internal/querylang/lexer"
)
// Parse parses a pipe-syntax query. Callers are responsible for routing
+1 -1
View File
@@ -3,7 +3,7 @@ package parser
import (
"testing"
"github.com/sentry/sentry/api/internal/querylang/ast"
"github.com/cairnobs/cairnobs/api/internal/querylang/ast"
)
func TestParseSimpleFilter(t *testing.T) {
+3 -3
View File
@@ -9,9 +9,9 @@ import (
"strings"
"time"
"github.com/sentry/sentry/api/internal/querylang/ast"
"github.com/sentry/sentry/api/internal/querylang/ir"
"github.com/sentry/sentry/api/internal/querylang/parser"
"github.com/cairnobs/cairnobs/api/internal/querylang/ast"
"github.com/cairnobs/cairnobs/api/internal/querylang/ir"
"github.com/cairnobs/cairnobs/api/internal/querylang/parser"
)
// Language selects which syntax a query is written in.
@@ -5,7 +5,7 @@ import (
"testing"
"time"
"github.com/sentry/sentry/api/internal/querylang/ir"
"github.com/cairnobs/cairnobs/api/internal/querylang/ir"
)
var fixedNow = time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
+3 -3
View File
@@ -5,7 +5,7 @@ import (
"errors"
"net/http"
"github.com/sentry/sentry/api/authz"
"github.com/cairnobs/cairnobs/api/authz"
)
// sessionStore is the narrow interface Authorizer depends on -- *Store
@@ -19,7 +19,7 @@ type sessionStore interface {
// needs to know about implicitly (via credentials: 'include', not by
// name -- the browser handles the cookie, JS never reads it since it's
// HttpOnly).
const sessionCookieName = "sentry_local_session"
const sessionCookieName = "cairnobs_local_session"
// Authorizer implements api/authz.Authorizer against local_sessions --
// wiring a non-nil *Authorizer into api/cmd/api/main.go's authorizer
@@ -37,7 +37,7 @@ func NewAuthorizer(store sessionStore) *Authorizer {
var errNoCredential = errors.New("localauth: no session credential presented")
// Authorize checks Authorization: Bearer first (sentryctl and other
// Authorize checks Authorization: Bearer first (cairnobsctl and other
// non-browser callers), then the session cookie (the web UI) -- same
// precedence authz.HTTPAuthorizer's caller-side forwarding implies,
// and the same reason POST /auth/login's response body returns the raw
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"strconv"
"time"
"github.com/sentry/sentry/api/authz"
"github.com/cairnobs/cairnobs/api/authz"
)
// fakeStore implements both store (handler.go) and sessionStore
+4 -4
View File
@@ -8,7 +8,7 @@ import (
"net/http"
"time"
"github.com/sentry/sentry/api/authz"
"github.com/cairnobs/cairnobs/api/authz"
)
const maxBodyBytes = 1 << 20 // 1 MiB, same cap as queryapi/dashboards/agents
@@ -36,8 +36,8 @@ type store interface {
type CookieConfig struct {
// Domain is typically empty for local dev (host-only cookie, works
// fine when web/api are both localhost:<port>) and something like
// ".sentry.example.com" in production, so the same cookie is sent to
// api.sentry.example.com and alerting.sentry.example.com too -- see
// ".cairnobs.example.com" in production, so the same cookie is sent to
// api.cairnobs.example.com and alerting.cairnobs.example.com too -- see
// /docs (deployment runbook) for the subdomain scheme this assumes.
Domain string
// Secure defaults to true (the cookie is never sent over plain
@@ -129,7 +129,7 @@ type loginRequest struct {
type sessionResponse struct {
// Token duplicates what the Set-Cookie header already carries,
// specifically for non-browser callers with no cookie jar --
// sentryctl captures this into SENTRYCTL_TOKEN and sends it back as
// cairnobsctl captures this into CAIRNOBSCTL_TOKEN and sends it back as
// Authorization: Bearer (see authorizer.go's credentialFromRequest,
// which accepts either). The web UI ignores this field entirely and
// relies on the cookie.
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"testing"
"time"
"github.com/sentry/sentry/api/authz"
"github.com/cairnobs/cairnobs/api/authz"
)
func newTestHandler(t *testing.T, fs *fakeStore) (*Handler, *http.ServeMux) {
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"testing"
"time"
"github.com/sentry/sentry/api/authz"
"github.com/cairnobs/cairnobs/api/authz"
)
func TestLoginLimiterAllowsUpToMax(t *testing.T) {
+1 -1
View File
@@ -37,7 +37,7 @@ import (
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/sentry/sentry/api/authz"
"github.com/cairnobs/cairnobs/api/authz"
)
var (
+2 -2
View File
@@ -10,7 +10,7 @@
//
// docker run --rm --network sentry_default -v $(pwd)/../../..:/src -w /src/api \
// -e LOCALAUTH_TEST_POSTGRES_ADDR=metadata-postgres:5432 \
// -e LOCALAUTH_TEST_POSTGRES_PASSWORD=sentry-dev-only \
// -e LOCALAUTH_TEST_POSTGRES_PASSWORD=cairnobs-dev-only \
// golang:1.25-alpine go test ./localauth/... -run Integration -v
package localauth
@@ -25,7 +25,7 @@ import (
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/sentry/sentry/api/authz"
"github.com/cairnobs/cairnobs/api/authz"
)
func integrationStore(t *testing.T) *Store {
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"strconv"
"time"
"github.com/sentry/sentry/api/authz"
"github.com/cairnobs/cairnobs/api/authz"
)
const maxBodyBytes = 1 << 20 // 1 MiB, same cap as localauth/dashboards/agents
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"testing"
"time"
"github.com/sentry/sentry/api/authz"
"github.com/cairnobs/cairnobs/api/authz"
)
func discardLogger() *slog.Logger {
+1 -1
View File
@@ -13,7 +13,7 @@
// service is a genuine per-log-record dimension already, not something
// this package invents: storage/migrations/0001_create_logs_table.sql
// has always had a `service` column, and distinct services on one host
// are a real, already-supported shape (separate sentry-agent processes
// are a real, already-supported shape (separate cairnobs-agent processes
// on the same machine, each with its own agent.toml `service` -- see
// /docs/agent-management-design.md), not merely a per-agent label.
//
+5 -5
View File
@@ -1,4 +1,4 @@
// Package queryapi is Sentry's query API: a single POST /query endpoint
// Package queryapi is Cairn OBS's query API: a single POST /query endpoint
// accepting either the pipe syntax or raw SQL, compiled by
// querylang/planner and executed by querylang/executor. Replaces Phase
// 0/1's two separate placeholder endpoints (raw-SQL-only /query,
@@ -19,10 +19,10 @@ import (
"strings"
"time"
"github.com/sentry/sentry/api/ai/costguard"
"github.com/sentry/sentry/api/authz"
"github.com/sentry/sentry/api/internal/querylang/planner"
"github.com/sentry/sentry/api/querylang/executor"
"github.com/cairnobs/cairnobs/api/ai/costguard"
"github.com/cairnobs/cairnobs/api/authz"
"github.com/cairnobs/cairnobs/api/internal/querylang/planner"
"github.com/cairnobs/cairnobs/api/querylang/executor"
)
// AuditLogger is core's extension point for query audit logging --
+2 -2
View File
@@ -12,8 +12,8 @@ import (
"testing"
"time"
"github.com/sentry/sentry/api/authz"
"github.com/sentry/sentry/api/querylang/executor"
"github.com/cairnobs/cairnobs/api/authz"
"github.com/cairnobs/cairnobs/api/querylang/executor"
)
type fakeSQLRunner struct {
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"context"
"fmt"
"github.com/sentry/sentry/api/internal/querylang/ir"
"github.com/cairnobs/cairnobs/api/internal/querylang/ir"
)
type Result struct {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"testing"
"time"
"github.com/sentry/sentry/api/internal/querylang/ir"
"github.com/cairnobs/cairnobs/api/internal/querylang/ir"
)
func mustParseTime(t *testing.T, s string) time.Time {
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"strings"
"time"
"github.com/sentry/sentry/api/internal/querylang/ir"
"github.com/cairnobs/cairnobs/api/internal/querylang/ir"
)
// defaultRowLimit is the safety net when a raw-row query has neither an
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
searchv1 "github.com/sentry/sentry/proto/sentry/search/v1"
searchv1 "github.com/cairnobs/cairnobs/proto/sentry/search/v1"
)
type Client struct {
+4 -4
View File
@@ -1,9 +1,9 @@
# docker build -f cli/Dockerfile -t sentryctl cli/
# docker build -f cli/Dockerfile -t cairnobsctl cli/
FROM golang:1.25-alpine AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/sentryctl ./cmd/sentryctl
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/cairnobsctl ./cmd/cairnobsctl
FROM gcr.io/distroless/static-debian12
COPY --from=builder /out/sentryctl /sentryctl
ENTRYPOINT ["/sentryctl"]
COPY --from=builder /out/cairnobsctl /cairnobsctl
ENTRYPOINT ["/cairnobsctl"]
+19 -19
View File
@@ -1,20 +1,20 @@
# sentryctl
# cairnobsctl
Sentry's control CLI.
Cairn OBS's control CLI.
```sh
sentryctl ping # checks http://localhost:8080/healthz
sentryctl ping --api http://api.internal:8080
SENTRYCTL_API_URL=http://api.internal:8080 sentryctl ping
cairnobsctl ping # checks http://localhost:8080/healthz
cairnobsctl ping --api http://api.internal:8080
CAIRNOBSCTL_API_URL=http://api.internal:8080 cairnobsctl ping
```
Exits 0 and prints `ok` if `/api`'s `/healthz` responds 200; exits 1 with an
error on `stderr` otherwise.
```sh
sentryctl query 'service=api | where status>=500 | stats count by host'
sentryctl query 'SELECT * FROM logs LIMIT 10' --language sql
sentryctl query 'message:"connection refused"' --json
cairnobsctl query 'service=api | where status>=500 | stats count by host'
cairnobsctl query 'SELECT * FROM logs LIMIT 10' --language sql
cairnobsctl query 'message:"connection refused"' --json
```
Quote the query in your shell — pipe syntax uses `|`, which your shell
@@ -27,17 +27,17 @@ Prints a table by default (stdlib `text/tabwriter`, no new dependency);
`--json` prints the raw `{columns, rows}` response instead.
```sh
sentryctl dashboards list
sentryctl dashboards get <id>
sentryctl dashboards apply dashboard.json # imports a dashboard exported via the web UI's "Export JSON" button
cairnobsctl dashboards list
cairnobsctl dashboards get <id>
cairnobsctl dashboards apply dashboard.json # imports a dashboard exported via the web UI's "Export JSON" button
sentryctl dashboards permissions list <dashboard-id>
sentryctl dashboards permissions grant <dashboard-id> <user-id> viewer|editor
sentryctl dashboards permissions revoke <dashboard-id> <user-id>
cairnobsctl dashboards permissions list <dashboard-id>
cairnobsctl dashboards permissions grant <dashboard-id> <user-id> viewer|editor
cairnobsctl dashboards permissions revoke <dashboard-id> <user-id>
sentryctl alerts list
sentryctl alerts get <id>
sentryctl alerts apply rule.json # creates a rule from a JSON file shaped like POST /rules's body
cairnobsctl alerts list
cairnobsctl alerts get <id>
cairnobsctl alerts apply rule.json # creates a rule from a JSON file shaped like POST /rules's body
```
`dashboards permissions` is Phase 4's per-resource dashboard grant
@@ -51,7 +51,7 @@ CLI bug.
`dashboards` talks to `/api` (`--api`, same override as `query`/`ping`).
`alerts` talks to `/alerting`, a separate service with its own base URL
(`--alerting-api`, or `$SENTRYCTL_ALERTING_API_URL`, default
(`--alerting-api`, or `$CAIRNOBSCTL_ALERTING_API_URL`, default
`http://localhost:8081`) — see `/docs/phase-3-alerting-design.md`'s
component boundary for why alerting isn't just another `/api` route.
`apply` in both cases sends the file's JSON as-is to the corresponding
@@ -78,5 +78,5 @@ go test ./...
```
```sh
docker build -f Dockerfile -t sentryctl . # context is cli/, not the repo root
docker build -f Dockerfile -t cairnobsctl . # context is cli/, not the repo root
```
@@ -10,7 +10,7 @@ import (
func TestResolveTokenFromEnv(t *testing.T) {
env := func(k string) string {
if k == "SENTRYCTL_TOKEN" {
if k == "CAIRNOBSCTL_TOKEN" {
return "secret-token"
}
return ""
@@ -91,7 +91,7 @@ func TestCmdPingForwardsBearerToken(t *testing.T) {
}))
defer srv.Close()
t.Setenv("SENTRYCTL_TOKEN", "ping-token")
t.Setenv("CAIRNOBSCTL_TOKEN", "ping-token")
var stdout, stderr bytes.Buffer
code := cmdPing([]string{"--api", srv.URL}, &stdout, &stderr)
if code != 0 {
@@ -111,7 +111,7 @@ func TestCmdQueryForwardsBearerToken(t *testing.T) {
}))
defer srv.Close()
t.Setenv("SENTRYCTL_TOKEN", "query-token")
t.Setenv("CAIRNOBSCTL_TOKEN", "query-token")
var stdout, stderr bytes.Buffer
code := cmdQuery([]string{"--api", srv.URL, "service=api"}, &stdout, &stderr)
if code != 0 {
@@ -17,7 +17,7 @@ import (
func cmdAgents(args []string, stdout, stderr io.Writer) int {
if len(args) == 0 {
fmt.Fprintln(stderr, "sentryctl agents: expected a subcommand (list, get, config, restart)")
fmt.Fprintln(stderr, "cairnobsctl agents: expected a subcommand (list, get, config, restart)")
return 1
}
apiURL, rest := extractAPIFlag(args[1:], os.Getenv)
@@ -28,19 +28,19 @@ func cmdAgents(args []string, stdout, stderr io.Writer) int {
return httpGetJSON(apiURL, "/agents", token, stdout, stderr)
case "get":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl agents get: missing host")
fmt.Fprintln(stderr, "cairnobsctl agents get: missing host")
return 1
}
return httpGetJSON(apiURL, "/agents/"+rest[0], token, stdout, stderr)
case "config":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl agents config: expected a subcommand (get, set, clear)")
fmt.Fprintln(stderr, "cairnobsctl agents config: expected a subcommand (get, set, clear)")
return 1
}
return cmdAgentsConfig(rest, apiURL, token, stdout, stderr)
case "restart":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl agents restart: missing host")
fmt.Fprintln(stderr, "cairnobsctl agents restart: missing host")
return 1
}
// os.Stdin passed explicitly at this inner layer (not threaded
@@ -49,7 +49,7 @@ func cmdAgents(args []string, stdout, stderr io.Writer) int {
// call cmdAgentsRestart directly with a fake reader.
return cmdAgentsRestart(rest, apiURL, token, os.Stdin, stdout, stderr)
default:
fmt.Fprintf(stderr, "sentryctl agents: unknown subcommand %q (want list, get, config, restart)\n", args[0])
fmt.Fprintf(stderr, "cairnobsctl agents: unknown subcommand %q (want list, get, config, restart)\n", args[0])
return 1
}
}
@@ -59,7 +59,7 @@ func cmdAgentsConfig(args []string, apiURL, token string, stdout, stderr io.Writ
switch sub {
case "get":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl agents config get: missing host")
fmt.Fprintln(stderr, "cairnobsctl agents config get: missing host")
return 1
}
// Same GET /agents/{host} as plain "get" -- an agent's reported
@@ -69,18 +69,18 @@ func cmdAgentsConfig(args []string, apiURL, token string, stdout, stderr io.Writ
return httpGetJSON(apiURL, "/agents/"+rest[0], token, stdout, stderr)
case "set":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl agents config set: missing host")
fmt.Fprintln(stderr, "cairnobsctl agents config set: missing host")
return 1
}
return cmdAgentsConfigSet(rest[0], rest[1:], apiURL, token, stdout, stderr)
case "clear":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl agents config clear: missing host")
fmt.Fprintln(stderr, "cairnobsctl agents config clear: missing host")
return 1
}
return httpMutateNoBody(http.MethodDelete, apiURL, "/agents/"+rest[0]+"/config", token, "", "config override cleared -- agent will run its local agent.toml again", stdout, stderr)
default:
fmt.Fprintf(stderr, "sentryctl agents config: unknown subcommand %q (want get, set, clear)\n", sub)
fmt.Fprintf(stderr, "cairnobsctl agents config: unknown subcommand %q (want get, set, clear)\n", sub)
return 1
}
}
@@ -135,71 +135,71 @@ func cmdAgentsConfigSet(host string, flagArgs []string, apiURL, token string, st
case "--batch-max-size":
v, ok := next()
if !ok {
fmt.Fprintln(stderr, "sentryctl agents config set: --batch-max-size requires a value")
fmt.Fprintln(stderr, "cairnobsctl agents config set: --batch-max-size requires a value")
return 1
}
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
fmt.Fprintf(stderr, "sentryctl agents config set: invalid --batch-max-size %q: %v\n", v, err)
fmt.Fprintf(stderr, "cairnobsctl agents config set: invalid --batch-max-size %q: %v\n", v, err)
return 1
}
batchMaxSize = &n
case "--batch-flush-interval-ms":
v, ok := next()
if !ok {
fmt.Fprintln(stderr, "sentryctl agents config set: --batch-flush-interval-ms requires a value")
fmt.Fprintln(stderr, "cairnobsctl agents config set: --batch-flush-interval-ms requires a value")
return 1
}
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
fmt.Fprintf(stderr, "sentryctl agents config set: invalid --batch-flush-interval-ms %q: %v\n", v, err)
fmt.Fprintf(stderr, "cairnobsctl agents config set: invalid --batch-flush-interval-ms %q: %v\n", v, err)
return 1
}
batchFlushMS = &n
case "--heartbeat-interval-ms":
v, ok := next()
if !ok {
fmt.Fprintln(stderr, "sentryctl agents config set: --heartbeat-interval-ms requires a value")
fmt.Fprintln(stderr, "cairnobsctl agents config set: --heartbeat-interval-ms requires a value")
return 1
}
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
fmt.Fprintf(stderr, "sentryctl agents config set: invalid --heartbeat-interval-ms %q: %v\n", v, err)
fmt.Fprintf(stderr, "cairnobsctl agents config set: invalid --heartbeat-interval-ms %q: %v\n", v, err)
return 1
}
heartbeatMS = &n
case "--heartbeat-enabled":
v, ok := next()
if !ok {
fmt.Fprintln(stderr, "sentryctl agents config set: --heartbeat-enabled requires true or false")
fmt.Fprintln(stderr, "cairnobsctl agents config set: --heartbeat-enabled requires true or false")
return 1
}
b, err := strconv.ParseBool(v)
if err != nil {
fmt.Fprintf(stderr, "sentryctl agents config set: invalid --heartbeat-enabled %q: %v\n", v, err)
fmt.Fprintf(stderr, "cairnobsctl agents config set: invalid --heartbeat-enabled %q: %v\n", v, err)
return 1
}
heartbeatEnabled = &b
case "--journald-unit":
v, ok := next()
if !ok {
fmt.Fprintln(stderr, "sentryctl agents config set: --journald-unit requires a value (empty string clears the filter)")
fmt.Fprintln(stderr, "cairnobsctl agents config set: --journald-unit requires a value (empty string clears the filter)")
return 1
}
journaldUnit = &v
default:
fmt.Fprintf(stderr, "sentryctl agents config set: unknown flag %q\n", flag)
fmt.Fprintf(stderr, "cairnobsctl agents config set: unknown flag %q\n", flag)
return 1
}
}
if batchMaxSize == nil && batchFlushMS == nil && heartbeatMS == nil && heartbeatEnabled == nil && journaldUnit == nil {
fmt.Fprintln(stderr, "sentryctl agents config set: at least one of --batch-max-size, --batch-flush-interval-ms, --heartbeat-enabled, --heartbeat-interval-ms, --journald-unit is required")
fmt.Fprintln(stderr, "cairnobsctl agents config set: at least one of --batch-max-size, --batch-flush-interval-ms, --heartbeat-enabled, --heartbeat-interval-ms, --journald-unit is required")
return 1
}
current, err := fetchAgent(apiURL, host, token)
if err != nil {
fmt.Fprintf(stderr, "sentryctl agents config set: fetching current state: %v\n", err)
fmt.Fprintf(stderr, "cairnobsctl agents config set: fetching current state: %v\n", err)
return 1
}
@@ -232,7 +232,7 @@ func cmdAgentsConfigSet(host string, flagArgs []string, apiURL, token string, st
body, err := json.Marshal(merged)
if err != nil {
fmt.Fprintf(stderr, "sentryctl agents config set: encoding request: %v\n", err)
fmt.Fprintf(stderr, "cairnobsctl agents config set: encoding request: %v\n", err)
return 1
}
return httpPutJSON(apiURL, "/agents/"+host+"/config", token, string(body), stdout, stderr)
@@ -8,7 +8,7 @@ import (
func cmdAlerts(args []string, stdout, stderr io.Writer) int {
if len(args) == 0 {
fmt.Fprintln(stderr, "sentryctl alerts: expected a subcommand (list, get, apply)")
fmt.Fprintln(stderr, "cairnobsctl alerts: expected a subcommand (list, get, apply)")
return 1
}
alertingURL, rest := extractAlertingAPIFlag(args[1:], os.Getenv)
@@ -19,13 +19,13 @@ func cmdAlerts(args []string, stdout, stderr io.Writer) int {
return httpGetJSON(alertingURL, "/rules", token, stdout, stderr)
case "get":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl alerts get: missing rule id")
fmt.Fprintln(stderr, "cairnobsctl alerts get: missing rule id")
return 1
}
return httpGetJSON(alertingURL, "/rules/"+rest[0], token, stdout, stderr)
case "apply":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl alerts apply: missing file path")
fmt.Fprintln(stderr, "cairnobsctl alerts apply: missing file path")
return 1
}
// POST /rules accepts the same shape it returns -- a rule
@@ -33,7 +33,7 @@ func cmdAlerts(args []string, stdout, stderr io.Writer) int {
// target ID) applies directly with no reshaping.
return httpPostFileJSON(alertingURL, "/rules", token, rest[0], stdout, stderr)
default:
fmt.Fprintf(stderr, "sentryctl alerts: unknown subcommand %q (want list, get, apply)\n", args[0])
fmt.Fprintf(stderr, "cairnobsctl alerts: unknown subcommand %q (want list, get, apply)\n", args[0])
return 1
}
}
@@ -28,7 +28,7 @@ func TestExtractAlertingAPIFlagOverride(t *testing.T) {
func TestExtractAlertingAPIFlagFromEnv(t *testing.T) {
alertingURL, _ := extractAlertingAPIFlag(nil, func(k string) string {
if k == "SENTRYCTL_ALERTING_API_URL" {
if k == "CAIRNOBSCTL_ALERTING_API_URL" {
return "http://env-alerting:8081"
}
return ""
@@ -9,7 +9,7 @@ import (
func cmdDashboards(args []string, stdout, stderr io.Writer) int {
if len(args) == 0 {
fmt.Fprintln(stderr, "sentryctl dashboards: expected a subcommand (list, get, apply, permissions)")
fmt.Fprintln(stderr, "cairnobsctl dashboards: expected a subcommand (list, get, apply, permissions)")
return 1
}
apiURL, rest := extractAPIFlag(args[1:], os.Getenv)
@@ -20,13 +20,13 @@ func cmdDashboards(args []string, stdout, stderr io.Writer) int {
return httpGetJSON(apiURL, "/dashboards", token, stdout, stderr)
case "get":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl dashboards get: missing dashboard id")
fmt.Fprintln(stderr, "cairnobsctl dashboards get: missing dashboard id")
return 1
}
return httpGetJSON(apiURL, "/dashboards/"+rest[0], token, stdout, stderr)
case "apply":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl dashboards apply: missing file path")
fmt.Fprintln(stderr, "cairnobsctl dashboards apply: missing file path")
return 1
}
// The import endpoint consumes exactly the shape GET
@@ -35,12 +35,12 @@ func cmdDashboards(args []string, stdout, stderr io.Writer) int {
return httpPostFileJSON(apiURL, "/dashboards/import", token, rest[0], stdout, stderr)
case "permissions":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl dashboards permissions: expected a subcommand (list, grant, revoke)")
fmt.Fprintln(stderr, "cairnobsctl dashboards permissions: expected a subcommand (list, grant, revoke)")
return 1
}
return cmdDashboardsPermissions(rest, apiURL, token, stdout, stderr)
default:
fmt.Fprintf(stderr, "sentryctl dashboards: unknown subcommand %q (want list, get, apply, permissions)\n", args[0])
fmt.Fprintf(stderr, "cairnobsctl dashboards: unknown subcommand %q (want list, get, apply, permissions)\n", args[0])
return 1
}
}
@@ -49,8 +49,8 @@ func cmdDashboards(args []string, stdout, stderr io.Writer) int {
// surface -- PUT/DELETE /dashboards/{id}/permissions/{userId} existed
// with no caller but Go tests and curl until now (see
// /docs/phase-4-runbook.md's "Known gaps"). Kept as dashboards'
// own sub-subcommand rather than a flat sentryctl command (like
// "sentryctl dashboard-permissions grant ...") since a grant only ever
// own sub-subcommand rather than a flat cairnobsctl command (like
// "cairnobsctl dashboard-permissions grant ...") since a grant only ever
// makes sense in the context of one specific dashboard -- args[0]
// selects list/grant/revoke.
func cmdDashboardsPermissions(args []string, apiURL, token string, stdout, stderr io.Writer) int {
@@ -58,13 +58,13 @@ func cmdDashboardsPermissions(args []string, apiURL, token string, stdout, stder
switch sub {
case "list":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl dashboards permissions list: missing dashboard id")
fmt.Fprintln(stderr, "cairnobsctl dashboards permissions list: missing dashboard id")
return 1
}
return httpGetJSON(apiURL, "/dashboards/"+rest[0]+"/permissions", token, stdout, stderr)
case "grant":
if len(rest) < 3 {
fmt.Fprintln(stderr, "sentryctl dashboards permissions grant: usage: grant <dashboard-id> <user-id> <viewer|editor>")
fmt.Fprintln(stderr, "cairnobsctl dashboards permissions grant: usage: grant <dashboard-id> <user-id> <viewer|editor>")
return 1
}
dashboardID, userID, role := rest[0], rest[1], rest[2]
@@ -73,7 +73,7 @@ func cmdDashboardsPermissions(args []string, apiURL, token string, stdout, stder
// only ever raises someone as high as Editor; the server
// rejects anything else too, this just fails faster/locally.
if role != "viewer" && role != "editor" {
fmt.Fprintf(stderr, "sentryctl dashboards permissions grant: role must be \"viewer\" or \"editor\", got %q\n", role)
fmt.Fprintf(stderr, "cairnobsctl dashboards permissions grant: role must be \"viewer\" or \"editor\", got %q\n", role)
return 1
}
body := fmt.Sprintf(`{"role":%q}`, role)
@@ -81,14 +81,14 @@ func cmdDashboardsPermissions(args []string, apiURL, token string, stdout, stder
return httpMutateNoBody(http.MethodPut, apiURL, path, token, body, "granted", stdout, stderr)
case "revoke":
if len(rest) < 2 {
fmt.Fprintln(stderr, "sentryctl dashboards permissions revoke: usage: revoke <dashboard-id> <user-id>")
fmt.Fprintln(stderr, "cairnobsctl dashboards permissions revoke: usage: revoke <dashboard-id> <user-id>")
return 1
}
dashboardID, userID := rest[0], rest[1]
path := "/dashboards/" + dashboardID + "/permissions/" + userID
return httpMutateNoBody(http.MethodDelete, apiURL, path, token, "", "revoked", stdout, stderr)
default:
fmt.Fprintf(stderr, "sentryctl dashboards permissions: unknown subcommand %q (want list, grant, revoke)\n", sub)
fmt.Fprintf(stderr, "cairnobsctl dashboards permissions: unknown subcommand %q (want list, grant, revoke)\n", sub)
return 1
}
}
@@ -9,7 +9,7 @@ import (
)
// parsePingArgs resolves the api base URL for ping: --api flag wins, then
// $SENTRYCTL_API_URL, then the hardcoded default. Kept pure (env passed in
// $CAIRNOBSCTL_API_URL, then the hardcoded default. Kept pure (env passed in
// as a function) and separate from the HTTP call so it's unit-testable
// without a real environment or server.
func parsePingArgs(args []string, env func(string) string) string {
@@ -28,7 +28,7 @@ type queryArgs struct {
// parseQueryArgs is pure (env passed in, no I/O), same testability
// reasoning as parsePingArgs. Non-flag arguments are joined with spaces
// to form the query, so `sentryctl query service=api status=500` (no
// to form the query, so `cairnobsctl query service=api status=500` (no
// quotes, no shell-special characters) works without requiring users to
// quote every query -- though anything using "|" still needs shell
// quoting regardless, since that's a real shell pipe character otherwise.
@@ -97,7 +97,7 @@ func cmdQuery(args []string, stdout, stderr io.Writer) int {
}
if strings.TrimSpace(qa.query) == "" {
fmt.Fprintln(stderr, "sentryctl query: missing query string")
fmt.Fprintln(stderr, "cairnobsctl query: missing query string")
return 1
}
return runAndPrintQuery(qa.apiURL, qa.query, qa.language, qa.jsonOut, stdout, stderr)
@@ -3,7 +3,7 @@
// runbook, and api/localauth's package doc comment for the full
// feature). Same list/create/delete shape as agents/dashboards, plus a
// "login" subcommand: unlike every other resource this CLI manages,
// there's no way to get a first SENTRYCTL_TOKEN without one.
// there's no way to get a first CAIRNOBSCTL_TOKEN without one.
package main
import (
@@ -18,7 +18,7 @@ import (
func cmdUsers(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
if len(args) == 0 {
fmt.Fprintln(stderr, "sentryctl users: expected a subcommand (login, list, create, delete, reset-password)")
fmt.Fprintln(stderr, "cairnobsctl users: expected a subcommand (login, list, create, delete, reset-password)")
return 1
}
apiURL, rest := extractAPIFlag(args[1:], os.Getenv)
@@ -27,7 +27,7 @@ func cmdUsers(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
switch args[0] {
case "login":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl users login: missing username")
fmt.Fprintln(stderr, "cairnobsctl users login: missing username")
return 1
}
return cmdUsersLogin(rest[0], rest[1:], apiURL, stdin, stdout, stderr)
@@ -35,24 +35,24 @@ func cmdUsers(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
return httpGetJSON(apiURL, "/auth/users", token, stdout, stderr)
case "create":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl users create: missing username")
fmt.Fprintln(stderr, "cairnobsctl users create: missing username")
return 1
}
return cmdUsersCreate(rest[0], rest[1:], apiURL, token, stdin, stdout, stderr)
case "delete":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl users delete: missing user id")
fmt.Fprintln(stderr, "cairnobsctl users delete: missing user id")
return 1
}
return httpMutateNoBody(http.MethodDelete, apiURL, "/auth/users/"+rest[0], token, "", "user deleted", stdout, stderr)
case "reset-password":
if len(rest) == 0 {
fmt.Fprintln(stderr, "sentryctl users reset-password: missing user id")
fmt.Fprintln(stderr, "cairnobsctl users reset-password: missing user id")
return 1
}
return cmdUsersResetPassword(rest[0], rest[1:], apiURL, token, stdin, stdout, stderr)
default:
fmt.Fprintf(stderr, "sentryctl users: unknown subcommand %q (want login, list, create, delete, reset-password)\n", args[0])
fmt.Fprintf(stderr, "cairnobsctl users: unknown subcommand %q (want login, list, create, delete, reset-password)\n", args[0])
return 1
}
}
@@ -81,7 +81,7 @@ func extractPasswordStdinFlag(args []string) (useStdin bool, rest []string) {
// readPasswordFromStdin reads a single line from stdin. Not masked
// (this codebase has no terminal/raw-mode dependency to draw on -- see
// resolveToken's doc comment for the same tradeoff already accepted for
// SENTRYCTL_TOKEN); pipe the value in (`echo "$PW" | sentryctl users
// CAIRNOBSCTL_TOKEN); pipe the value in (`echo "$PW" | cairnobsctl users
// login admin`) rather than typing it at an interactive terminal where
// that matters.
func readPasswordFromStdin(stdin io.Reader) (string, error) {
@@ -103,7 +103,7 @@ type loginResponseBody struct {
}
// cmdUsersLogin prints only the raw token to stdout on success (nothing
// else) -- deliberately pipeable: `export SENTRYCTL_TOKEN=$(sentryctl
// else) -- deliberately pipeable: `export CAIRNOBSCTL_TOKEN=$(cairnobsctl
// users login admin)`.
func cmdUsersLogin(username string, _ []string, apiURL string, stdin io.Reader, stdout, stderr io.Writer) int {
password, err := readPasswordFromStdin(stdin)
@@ -39,7 +39,7 @@ func TestCmdUsersLoginPrintsOnlyTheToken(t *testing.T) {
t.Fatalf("code = %d, want 0; stderr=%s", code, stderr.String())
}
if got := strings.TrimSpace(stdout.String()); got != "abc123" {
t.Fatalf("stdout = %q, want exactly the raw token (pipeable into SENTRYCTL_TOKEN)", got)
t.Fatalf("stdout = %q, want exactly the raw token (pipeable into CAIRNOBSCTL_TOKEN)", got)
}
}
@@ -59,7 +59,7 @@ func TestCmdUsersLoginFailure(t *testing.T) {
t.Fatalf("stderr = %q, want it to surface the server's error message", stderr.String())
}
if stdout.String() != "" {
t.Fatalf("stdout = %q, want empty on failure (nothing pipeable into SENTRYCTL_TOKEN)", stdout.String())
t.Fatalf("stdout = %q, want empty on failure (nothing pipeable into CAIRNOBSCTL_TOKEN)", stdout.String())
}
}
@@ -13,7 +13,7 @@ import (
var httpClient = &http.Client{Timeout: 30 * time.Second}
// setAuth attaches SENTRYCTL_TOKEN (see resolveToken) as a Bearer
// setAuth attaches CAIRNOBSCTL_TOKEN (see resolveToken) as a Bearer
// credential, a no-op when token is empty -- matches every backend's
// nil-authorizer no-op default (see api/internal/authz.RequireRole*).
func setAuth(req *http.Request, token string) {

Some files were not shown because too many files have changed in this diff Show More