Initial implementation: NATS->S3 archiver + search/retrieve CLI
logarchiver replaces the plain Vector archiver leg of the centralized logging stack (argocd-apps #296) with a Go service that archives raw logs from NATS JetStream to S3 as zstd-compressed, OpenPGP-encrypted, indexed objects, plus an operator CLI to search the index and retrieve/decrypt archived logs. It adds the things that outgrew Vector: zstd compression, encryption keyed from Ben's Vault GPG secrets engine, a searchable ClickHouse index, and sink-conditional acks (a batch is acknowledged to JetStream only after the object is durably in S3 AND indexed). Service (`logarchiver run`): - Durable JetStream pull consumer (stream LOGS, durable archiver, subject filter default logs.k8s.vault.>), explicit acks, independent offsets. - Batch per subject by size/count/time -> NDJSON -> zstd -> encrypt -> S3 PUT -> ClickHouse index row -> ack. On any failure the batch is Nak'd and redelivered, so nothing is lost on a sink outage. - Encryption is a wrapped-DEK envelope (container LARC1): the bulk is AES-256-GCM framed under a random data key, and only that 32-byte key is OpenPGP-encrypted to the engine's public key. This is because the Vault GPG engine does whole-payload decrypt only; retrieval round-trips just the tiny wrapped key regardless of object size. Public key fetched from the engine or a mounted file (configurable); key fingerprint recorded per object; periodic pubkey refresh for rotation. - Prometheus metrics, structured slog, graceful drain on shutdown. CLI: - `search` queries the index (subject/host/time) and lists matching objects. - `fetch` downloads, decrypts via the Vault GPG engine, unzstds and emits NDJSON (optionally re-filtered by host/time). - `init-schema` creates/prints the ClickHouse archive_index DDL. - cobra `completion` subcommands. Config via file+env (k8s-friendly, secrets from env), boundaries (NATS/S3/ ClickHouse/Vault) behind interfaces with unit tests (config, batching, host/subject extraction, crypto roundtrip with a test key, ack-after-persist with fakes, search query building). go build/vet/test -race clean; golangci-lint v2 clean. Woodpecker CI: build/test/pre-commit on PR; on v* tag a container image plus a Gitea binary release + rpm-internal RPM. Docs per subcommand + architecture + retrieval runbook + deployment drop-in. Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
# Architecture
|
||||
|
||||
logarchiver is one Go binary with a service mode (`run`) and an operator CLI
|
||||
(`search`/`fetch`/`init-schema`). Boundaries to external systems (NATS, S3,
|
||||
ClickHouse, Vault) sit behind interfaces so they can be faked in tests.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Responsibility |
|
||||
|---|---|
|
||||
| `internal/config` | Config struct, YAML+env loading, validation, defaults tuned for the logging stack. |
|
||||
| `internal/event` | Extract host + timestamp from a raw JSON event (k8s and VM shapes), subject sanitization. |
|
||||
| `internal/batcher` | Group events per subject into batches; decide flush by size/count/age; carry ack tokens. |
|
||||
| `internal/crypto` | The LARC1 container: zstd + framed AES-256-GCM + OpenPGP-wrapped data key. Seal/Open. |
|
||||
| `internal/vaultgpg` | Thin client for `vault-plugin-secrets-gpg`: fetch public key, decrypt wrapped data key. |
|
||||
| `internal/s3store` | S3/Ceph RGW object storage behind an `ObjectStore` interface. |
|
||||
| `internal/index` | ClickHouse archive index (`Index` interface), DDL, pure search-SQL builder. |
|
||||
| `internal/archiver` | Orchestrates seal → S3 PUT → index write; object-key builder; public-key provider/refresh. |
|
||||
| `internal/consumer` | JetStream binding + the fetch→batch→persist→ack loop (sink-conditional acks). |
|
||||
| `internal/metrics` | Prometheus collectors and `/metrics`. |
|
||||
| `internal/cli` | cobra command tree; wires everything for each subcommand. |
|
||||
|
||||
## Service data flow
|
||||
|
||||
1. **Consume.** A durable JetStream **pull** consumer (`LOGS` stream, durable
|
||||
`archiver`, subject filter default `logs.k8s.vault.>`) is created/updated at
|
||||
startup with explicit acks and independent offsets, so logarchiver's replay
|
||||
position is decoupled from the ClickHouse transform tier. Messages are pulled
|
||||
in batches.
|
||||
2. **Route + batch.** Each message's payload is parsed just enough to extract
|
||||
the source **host** (`.host` → `.hostname` → `.kubernetes.pod_node_name`) and
|
||||
**timestamp** (`.timestamp` → `.ts`). Events are grouped by NATS **subject**.
|
||||
A batch is flushed when it hits `max_bytes` (64 MiB raw), `max_events`, or
|
||||
`max_age` (5 min) — whichever first — or on shutdown.
|
||||
3. **Seal.** The batch is rendered to NDJSON, compressed with zstd, and sealed
|
||||
into a **LARC1** object (see below).
|
||||
4. **Store.** The object is `PUT` to S3 at
|
||||
`archive/<subject>/YYYY/MM/DD/<UTCstamp>-<rand>.ndjson.zst.larc`.
|
||||
5. **Index.** One row per object is inserted into ClickHouse `logs.archive_index`.
|
||||
6. **Ack.** Only now are the batch's JetStream messages acknowledged. If seal,
|
||||
S3, or index fails, the messages are Nak'd (with a backoff) and JetStream
|
||||
redelivers them — nothing is lost on a sink outage.
|
||||
|
||||
## The LARC1 container format
|
||||
|
||||
The Vault GPG engine can only decrypt a **whole** OpenPGP message inline (no
|
||||
session-key extraction, no streaming — see the retrieval runbook). To keep
|
||||
objects any size while making retrieval cheap, logarchiver does its own hybrid
|
||||
encryption instead of PGP-encrypting the whole object:
|
||||
|
||||
```
|
||||
+-----------------------------------------------------------+
|
||||
| magic "LARC1\n" (6 bytes) |
|
||||
| header_len uint32 big-endian |
|
||||
| header JSON { v, key_name, key_fingerprint, |
|
||||
| wrapped_dek_len, nonce_prefix, |
|
||||
| frame_size, compression, cipher } |
|
||||
| wrapped_dek OpenPGP message encrypting the 32-byte DEK | <- only this goes to Vault
|
||||
| to the engine's public key (~hundreds of B) |
|
||||
| frames repeated: uint32 ct_len | ciphertext | <- zstd(NDJSON) in AES-256-GCM
|
||||
| terminated by a zero-length frame | frames; nonce = prefix||counter
|
||||
+-----------------------------------------------------------+
|
||||
```
|
||||
|
||||
- A fresh random 256-bit **DEK** and 4-byte nonce prefix are generated per
|
||||
object. Each frame is AES-256-GCM sealed with nonce `prefix||counter` and AAD
|
||||
= the frame counter (binds frame order; tampering fails the GCM tag).
|
||||
- The DEK is OpenPGP-encrypted to the engine's public key. This wrapped blob is
|
||||
a small, standard OpenPGP message — the only thing ever sent to Vault's
|
||||
decrypt endpoint, regardless of object size.
|
||||
- Compression order is NDJSON → zstd → encrypt, so decryption streams frames,
|
||||
GCM-decrypts, and feeds a streaming zstd decoder to emit NDJSON.
|
||||
|
||||
Because objects are a logarchiver-specific container (not a bare `gpg` file),
|
||||
retrieval must go through `logarchiver fetch`. This is the deliberate cost of
|
||||
supporting arbitrary object sizes against an engine that only whole-payload
|
||||
decrypts.
|
||||
|
||||
## Index schema
|
||||
|
||||
`logs.archive_index` (one row per object), `MergeTree`,
|
||||
`PARTITION BY toYYYYMM(min_ts)`, `ORDER BY (subject, min_ts, object_key)`, with a
|
||||
`bloom_filter` skip index on `hosts`:
|
||||
|
||||
| column | type | notes |
|
||||
|---|---|---|
|
||||
| object_key | String | S3 key |
|
||||
| bucket | LowCardinality(String) | e.g. `logs-archive` |
|
||||
| subject | LowCardinality(String) | NATS subject |
|
||||
| hosts | Array(LowCardinality(String)) | distinct source hosts |
|
||||
| min_ts / max_ts | DateTime64(3) | event time range |
|
||||
| event_count | UInt64 | |
|
||||
| raw_bytes / stored_bytes | UInt64 | pre/post compression+encryption |
|
||||
| compression / cipher / container_format | LowCardinality(String) | `zstd` / `AES-256-GCM` / `LARC1` |
|
||||
| key_name / key_fingerprint | LowCardinality(String) / String | Vault GPG key + 40-hex fingerprint |
|
||||
| created_at | DateTime64(3) DEFAULT now64(3) | |
|
||||
|
||||
Search overlaps the `[from,to]` window (`max_ts >= from AND min_ts <= to`),
|
||||
matches the subject glob with an anchored regex (`*` = one token, `>` = rest),
|
||||
and filters hosts with `has(hosts, …)` (exact) or `arrayExists(… match …)` (glob).
|
||||
|
||||
The DDL is owned in-cluster by the argocd bootstrap Job; `logarchiver
|
||||
init-schema` applies the same statements and `--print` emits them. Keep
|
||||
`schema/archive_index.sql` and `internal/index/ddl.go` in sync.
|
||||
|
||||
## Delivery guarantees
|
||||
|
||||
- **At-least-once**, sink-conditional. Acks happen only after S3 + index
|
||||
success. A failure after S3 but before index leaves an orphan object (still
|
||||
retrievable by prefix; reaped by bucket lifecycle) and triggers redelivery,
|
||||
which produces a fresh object — so an event may appear in two objects, never
|
||||
zero. Downstream consumers of the archive should treat events as at-least-once.
|
||||
- **Graceful shutdown** drains open batches (bounded timeout) so in-flight
|
||||
events are persisted and acked before exit.
|
||||
|
||||
## Configuration & secrets
|
||||
|
||||
Config loads defaults < YAML file < env. Secrets are never in the file:
|
||||
NATS password from `NATS_CONSUMER_PASSWORD`, S3 creds from the standard `AWS_*`
|
||||
env (cephrgw `logs-archive-s3` secret), ClickHouse password from
|
||||
`CLICKHOUSE_PASSWORD`, Vault via `VAULT_ADDR` + k8s auth (service) or ambient
|
||||
token (CLI). See [deployment.md](deployment.md).
|
||||
@@ -0,0 +1,55 @@
|
||||
# Deployment (drop-in fit with the logging stack)
|
||||
|
||||
This describes how logarchiver slots into the centralized logging stack
|
||||
(argocd-apps #296). The actual argocd manifest swap is a **separate later task**;
|
||||
this documents the wiring logarchiver is built for so that swap is mechanical.
|
||||
|
||||
## Where it runs
|
||||
|
||||
- Namespace **`logging`**, ServiceAccount **`default`** (reuses the stack's
|
||||
`VaultAuth` `default`, k8s auth mount `k8s/au/syd1`, role `default`).
|
||||
- Single-replica Deployment (independent JetStream offsets; one archiver is
|
||||
enough — scale by subject-sharding into multiple durables if ever needed).
|
||||
- Pod label **`vector.dev/exclude: "true"`** so the Vector agent does not scrape
|
||||
logarchiver's own logs (matches the Vector archiver it replaces).
|
||||
- Startup healthcheck against RGW should be lenient (RGW cred propagation is
|
||||
slow), like the Vector archiver.
|
||||
|
||||
## What it binds to (all already provided by #296)
|
||||
|
||||
| Dependency | Wiring |
|
||||
|---|---|
|
||||
| **NATS** | `nats://nats.logging.svc.cluster.local:4222`, user `log-consumer`, password from secret **`nats-auth`** key `consumer_password` → env `NATS_CONSUMER_PASSWORD`. Stream `LOGS`, durable `archiver`. |
|
||||
| **S3 (Ceph RGW)** | Bucket `logs-archive`, endpoint `https://s3.ceph.unkin.net` (path-style, region `us-east-1`), CA `/etc/vault-ca/ca.crt` (mount the `vault-ca-cert` secret). Creds from cephrgw BucketAccess secret **`logs-archive-s3`** via `envFrom` (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`; optional `S3_ENDPOINT`/`BUCKET_NAME`). |
|
||||
| **ClickHouse** | `clickhouse-logs.logging.svc.cluster.local:9000` (native), database `logs`, table `archive_index`, user `vector`, password from secret **`clickhouse-credentials`** key `password` → env `CLICKHOUSE_PASSWORD`. |
|
||||
| **Vault GPG engine** | Public key: default `pubkey_source: file` from a mounted armored key (secret/ConfigMap at `/etc/logarchiver/pubkey.asc`) — no Vault dependency on the hot path. Or `pubkey_source: vault` with k8s auth to read `gpg/keys/logarchive`. |
|
||||
|
||||
## Secrets to seed (Vault KV, reusing the `logging/default` path)
|
||||
|
||||
Nothing new is strictly required if you reuse the existing `nats-auth`,
|
||||
`logs-archive-s3`, and `clickhouse-credentials` secrets. For the file pubkey,
|
||||
add an armored public key as a mounted secret (e.g.
|
||||
`kv/kubernetes/namespace/logging/default/logarchiver-pubkey` → VaultStaticSecret
|
||||
→ file mount).
|
||||
|
||||
## Cross-repo notes
|
||||
|
||||
- **No new ServiceAccount** (reuses `default`), so no argocd-apps SA PR is needed
|
||||
for CI; the Woodpecker steps already use `serviceAccountName: default`.
|
||||
- **terraform-vault:** only needed if you (a) use `pubkey_source: vault` and the
|
||||
`logging/default` policy doesn't already permit `read gpg/keys/logarchive`, or
|
||||
(b) want a dedicated operator policy for `update gpg/decrypt/logarchive`.
|
||||
Operators today use their own human Vault tokens for `fetch`, so this is
|
||||
optional — track as a follow-up, not a blocker.
|
||||
- **ClickHouse table:** created by the argocd bootstrap Job (embed the output of
|
||||
`logarchiver init-schema --print`), not by the service at runtime.
|
||||
|
||||
## Migrating off the Vector archiver
|
||||
|
||||
The Vector archiver binds the same `LOGS`/`archiver` durable. To cut over
|
||||
safely: deploy logarchiver with a **distinct** durable (e.g.
|
||||
`durable: archiver-canary`) and a narrow `ARCHIVE_SUBJECTS` to validate objects
|
||||
+ index rows land, then repoint it to the `archiver` durable and scale the
|
||||
Vector archiver to zero. logarchiver writes a different object prefix
|
||||
(`archive/….ndjson.zst.larc`) than Vector (`raw/….log.gz`), so the two never
|
||||
collide in the bucket.
|
||||
@@ -0,0 +1,41 @@
|
||||
# `logarchiver fetch`
|
||||
|
||||
Download, decrypt, and decompress archived objects to plain NDJSON.
|
||||
|
||||
```sh
|
||||
logarchiver fetch [object-key ...] [flags]
|
||||
```
|
||||
|
||||
Objects are selected either by explicit object-key arguments, or — when no keys
|
||||
are given — by the same `--subject/--host/--from/--to` query used by
|
||||
[`search`](search.md). When `--host/--from/--to` are supplied they ALSO
|
||||
re-filter the emitted events to just the matching lines.
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Meaning |
|
||||
|---|---|
|
||||
| `-o, --output` | `-` for stdout (default), or a directory to write one NDJSON file per object. |
|
||||
| `--subject`/`--host`/`--from`/`--to`/`--limit` | Object selection (same as `search`) when no keys are given; host/time also re-filter emitted lines. |
|
||||
|
||||
## How decryption works
|
||||
|
||||
For each object, `fetch` reads the object header to learn which Vault GPG key
|
||||
wrapped it, sends only the small wrapped data key to the engine's
|
||||
`gpg/decrypt/<name>` endpoint, recovers the data key, and streams the AES-GCM
|
||||
frames through local decryption + zstd to emit NDJSON. See the
|
||||
[retrieval runbook](retrieval-runbook.md) for the full design and required creds
|
||||
(S3 read, internal CA, ambient `VAULT_TOKEN`/`~/.vault-token`).
|
||||
|
||||
## Examples
|
||||
|
||||
```sh
|
||||
# Straight to stdout by key:
|
||||
logarchiver fetch archive/logs.k8s.vault._/2026/07/27/20260727T101500Z-ab12cd34.ndjson.zst.larc -o -
|
||||
|
||||
# From a query, re-filtered to host db-1 in the last 24h, into ./out:
|
||||
logarchiver fetch --subject 'logs.vm.*' --host db-1 --from -24h -o ./out
|
||||
```
|
||||
|
||||
Exit status is non-zero if any selected object fails; per-object errors are
|
||||
reported on stderr and the remaining objects are still processed.
|
||||
@@ -0,0 +1,18 @@
|
||||
# `logarchiver init-schema`
|
||||
|
||||
Create the ClickHouse archive-index database and table (idempotent).
|
||||
|
||||
```sh
|
||||
logarchiver init-schema # execute the DDL against index.address
|
||||
logarchiver init-schema --print # print the DDL instead of executing it
|
||||
```
|
||||
|
||||
In-cluster the argocd bootstrap Job owns schema creation (a ClickHouse PostSync
|
||||
hook, mirroring the logging stack's `clickhouse-schema` job). Use
|
||||
`init-schema --print` to emit the exact `CREATE DATABASE` / `CREATE TABLE`
|
||||
statements to embed in that Job, and `init-schema` (executing) for local/dev.
|
||||
|
||||
The statements are the source-of-truth DDL from `internal/index/ddl.go`, also
|
||||
kept as [`schema/archive_index.sql`](../schema/archive_index.sql). Database and
|
||||
table names come from `index.database` / `index.table` (defaults `logs` /
|
||||
`archive_index`).
|
||||
@@ -0,0 +1,118 @@
|
||||
# Retrieval runbook & crypto design
|
||||
|
||||
This document is the honest account of how encryption and retrieval work, what
|
||||
the Vault GPG engine actually supports, and the operational steps to get plain
|
||||
NDJSON back out of an archived object.
|
||||
|
||||
## What the Vault GPG engine supports (and doesn't)
|
||||
|
||||
logarchiver encrypts to a key in Ben's `vault-plugin-secrets-gpg` engine
|
||||
(transit-style OpenPGP, mounted at `gpg/`). Relevant endpoints:
|
||||
|
||||
- `GET gpg/keys/<name>` → returns `data.public_key` (armored) and
|
||||
`data.fingerprint` (40-hex uppercase, no spaces). Used to encrypt locally.
|
||||
- `POST gpg/decrypt/<name>` with `{ "ciphertext": "<base64 of a whole OpenPGP
|
||||
message>" }` → returns `{ "plaintext": "<base64>" }`.
|
||||
|
||||
**Crucial limitation:** the decrypt endpoint does **whole-payload inline
|
||||
decryption only**. It reads the entire OpenPGP message and returns the entire
|
||||
plaintext. There is **no** session-key (PKESK) extraction, **no** chunking, and
|
||||
**no** streaming. The whole ciphertext must fit in a base64 JSON request body
|
||||
(Vault's default `max_request_size` is 32 MiB), and the whole plaintext comes
|
||||
back base64 in the response. So you cannot feed a large archive object straight
|
||||
to the engine, and you cannot ask it to decrypt only the session key.
|
||||
|
||||
## The design logarchiver chose
|
||||
|
||||
Rather than bound object sizes to the engine's request limit, logarchiver does
|
||||
hybrid encryption itself and sends the engine only a tiny key blob:
|
||||
|
||||
1. **Encrypt (service, local):** generate a random 256-bit **DEK**; encrypt the
|
||||
zstd-compressed NDJSON locally with AES-256-GCM in frames; OpenPGP-encrypt
|
||||
just the 32-byte DEK to the engine's **public** key. The wrapped DEK is a
|
||||
small standard OpenPGP message. The private key is never present.
|
||||
2. **Decrypt (CLI, retrieval):** read the object header, send **only the wrapped
|
||||
DEK** (a few hundred bytes) to `gpg/decrypt/<name>`, get the DEK back, then
|
||||
stream-decrypt the AES-GCM frames locally and zstd-decompress to NDJSON.
|
||||
|
||||
The Vault round-trip is tiny and constant regardless of object size, the engine
|
||||
needs no changes, and the private key stays in Vault. See
|
||||
[architecture.md](architecture.md#the-larc1-container-format) for the container
|
||||
layout.
|
||||
|
||||
> **Possible engine enhancement (follow-up, not required):** if the engine ever
|
||||
> grows a "decrypt session key only / return PKESK plaintext" operation, objects
|
||||
> could be plain standard OpenPGP messages while retaining tiny Vault
|
||||
> round-trips. logarchiver's self-managed DEK envelope already achieves the
|
||||
> operational goal without it, so this is optional.
|
||||
|
||||
## One-time key setup
|
||||
|
||||
Create the archive key in the engine (if it doesn't already exist) and export
|
||||
the public key for the service:
|
||||
|
||||
```sh
|
||||
export VAULT_ADDR=https://vault.service.consul:8200
|
||||
|
||||
# Create the key (idempotent; rsa-3072 default, or ed25519).
|
||||
vault write gpg/keys/logarchive identity="logarchiver <ops@unkin.net>" algorithm=ed25519
|
||||
|
||||
# Fingerprint the service records per object (sanity check):
|
||||
vault read -field=fingerprint gpg/keys/logarchive
|
||||
|
||||
# Export the armored public key for the service's file-mounted pubkey source:
|
||||
vault read -field=public_key gpg/keys/logarchive > pubkey.asc
|
||||
```
|
||||
|
||||
In k8s the service can instead read the public key directly from Vault
|
||||
(`pubkey_source: vault`, k8s auth) — no mounted file needed. The file source is
|
||||
the default because it removes any Vault dependency from the hot path.
|
||||
|
||||
## Retrieve archived logs
|
||||
|
||||
The CLI needs: S3 read creds (`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` for the
|
||||
`logs-archive` bucket), the internal CA (`s3.ca_file`), and a Vault token with
|
||||
decrypt on the key (ambient `VAULT_TOKEN` or `~/.vault-token`, exactly like
|
||||
`passv`).
|
||||
|
||||
```sh
|
||||
export VAULT_ADDR=https://vault.service.consul:8200
|
||||
vault login ... # or have ~/.vault-token
|
||||
export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=...
|
||||
|
||||
# 1. Find objects via the index.
|
||||
logarchiver search --subject 'logs.k8s.vault.>' --host node-1 \
|
||||
--from 2026-07-01 --to 2026-07-27
|
||||
|
||||
# 2a. Retrieve by the object keys search printed, to stdout:
|
||||
logarchiver fetch <object-key> [<object-key> ...] -o -
|
||||
|
||||
# 2b. Or retrieve straight from a query, re-filtered to just the matching
|
||||
# host/time events, into a directory (one NDJSON file per object):
|
||||
logarchiver fetch --subject 'logs.vm.*' --host db-1 --from -24h -o ./out
|
||||
```
|
||||
|
||||
`fetch` downloads each object, reads its header to learn which Vault key wrapped
|
||||
it (`key_name`), decrypts the wrapped DEK via the engine, streams the frames
|
||||
through local AES-GCM + zstd, and emits NDJSON. When `--host`/`--from`/`--to`
|
||||
are supplied they also re-filter the emitted lines (events lacking a parseable
|
||||
timestamp are kept, never silently dropped on time grounds).
|
||||
|
||||
## Failure modes
|
||||
|
||||
| Symptom | Likely cause | Action |
|
||||
|---|---|---|
|
||||
| `decrypt via gpg/decrypt/<name>: permission denied` | Token lacks the decrypt policy | Grant `update` on `gpg/decrypt/<name>` (and `read` on `gpg/keys/<name>`). |
|
||||
| `unwrap dek: … no plaintext` | Wrong key name / key rotated below min_decryption_version | Confirm object `key_name`; the engine tries versions ≥ min_decryption_version. |
|
||||
| `bad magic: not a logarchiver (LARC1) object` | Fetching a non-logarchiver object (e.g. an old Vector `.log.gz`) | Use the right prefix; Vector objects are plain gzip, decrypt them with gzip. |
|
||||
| `decrypt frame N: cipher: message authentication failed` | Object corruption/tampering | Object integrity is broken; re-archive from JetStream if still within retention. |
|
||||
| `pubkey fingerprint mismatch` (service) | Armored pubkey doesn't match the engine's reported fingerprint | Re-export the pubkey; the service refuses to encrypt to a mismatched key. |
|
||||
|
||||
## Key rotation
|
||||
|
||||
Rotating the engine key (`vault write gpg/keys/logarchive/rotate ...`) starts
|
||||
encrypting new objects to the new version; the service picks up the new public
|
||||
key within `crypto.refresh_interval` (default 1h) or on restart. Old objects
|
||||
remain decryptable because the engine tries every key version from
|
||||
`min_decryption_version` up. Each object records the fingerprint it was sealed
|
||||
with, so you can always tell which key version applies.
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# `logarchiver run`
|
||||
|
||||
Runs the archiver service: binds the JetStream pull consumer and drains it to
|
||||
S3 + the ClickHouse index, acking only after each object is durably persisted.
|
||||
|
||||
```sh
|
||||
logarchiver run [-c config.yaml]
|
||||
```
|
||||
|
||||
## Behaviour
|
||||
|
||||
- Creates/updates the durable consumer (`nats.stream` / `nats.durable`) with the
|
||||
configured subject filters, explicit acks, and `nats.ack_wait`.
|
||||
- Batches events per subject and flushes on `batch.max_bytes` / `max_events` /
|
||||
`max_age`. Each flush seals a LARC1 object, PUTs it to S3, writes one index
|
||||
row, then acks the batch. On any failure the batch is Nak'd for redelivery.
|
||||
- Loads the OpenPGP public key from `crypto.pubkey_source` (`file` or `vault`)
|
||||
and refreshes it every `crypto.refresh_interval`.
|
||||
- Serves Prometheus metrics and `/healthz` on `metrics.address` (default `:9090`)
|
||||
when `metrics.enabled`.
|
||||
- Handles SIGINT/SIGTERM: stops fetching, drains open batches (bounded), exits.
|
||||
|
||||
## Key env vars
|
||||
|
||||
| Env | Purpose |
|
||||
|---|---|
|
||||
| `NATS_CONSUMER_PASSWORD` | NATS `log-consumer` password (nats-auth secret). |
|
||||
| `ARCHIVE_SUBJECTS` | Space-separated subject filters (overrides `nats.subjects`). |
|
||||
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | S3 creds (cephrgw `logs-archive-s3`). |
|
||||
| `S3_ENDPOINT` / `BUCKET_NAME` | Optional S3 endpoint/bucket from the cephrgw secret. |
|
||||
| `CLICKHOUSE_PASSWORD` | ClickHouse `vector` user password. |
|
||||
| `VAULT_ADDR` | Vault address when `pubkey_source: vault`. |
|
||||
| `LOGARCHIVER_CONFIG` | Config file path (same as `-c`). |
|
||||
|
||||
## Metrics
|
||||
|
||||
`logarchiver_objects_stored_total`, `_events_archived_total`,
|
||||
`_raw_bytes_total`, `_stored_bytes_total`, `_store_failures_total`,
|
||||
`_index_failures_total`, `_messages_fetched_total`, `_acks_total`,
|
||||
`_batches_flushed_total{trigger}`, `logarchiver_pending_events`.
|
||||
@@ -0,0 +1,39 @@
|
||||
# `logarchiver search`
|
||||
|
||||
Query the ClickHouse archive index for objects matching a subject/host/time
|
||||
window. Prints one row per matching S3 object with event counts and sizes; use
|
||||
the object keys with [`fetch`](fetch.md).
|
||||
|
||||
```sh
|
||||
logarchiver search [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Meaning |
|
||||
|---|---|
|
||||
| `--subject` | NATS-style subject glob: `*` matches one token, `>` matches the rest. e.g. `logs.vm.*`, `logs.k8s.vault.>`. |
|
||||
| `--host` | Source host to match: exact, or a glob containing `*`. |
|
||||
| `--from` | Start of window: RFC3339, `YYYY-MM-DD`, or a relative duration like `-24h`. |
|
||||
| `--to` | End of window (same formats). |
|
||||
| `--limit` | Max objects (default 100; `0` = no limit). |
|
||||
| `--json` | Emit results as JSON instead of a table. |
|
||||
|
||||
An object matches the time window when its `[min_ts, max_ts]` overlaps
|
||||
`[from, to]`. Objects are ordered by `min_ts`.
|
||||
|
||||
## Examples
|
||||
|
||||
```sh
|
||||
# Vault audit logs from a node in the last day.
|
||||
logarchiver search --subject 'logs.k8s.vault.>' --host node-1 --from -24h
|
||||
|
||||
# All VM logs for a host in July, as JSON.
|
||||
logarchiver search --subject 'logs.vm.*' --host db-1 \
|
||||
--from 2026-07-01 --to 2026-08-01 --json
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
Uses `index.*` (ClickHouse address/database/table/credentials). Requires
|
||||
`index.enabled: true`.
|
||||
Reference in New Issue
Block a user