ClickHouse backend

Bring your own ClickHouse and unlock the Advanced tier — raw SQL over logs, percentiles, cheap high-cardinality filters, and S3 tiering via storage policies.

The ClickHouse backend writes Rune's log pipeline into a ClickHouse table and lowers Log Explorer queries to SQL. It's the Advanced-tier backend: pick it when you want analytical power over logs, not just retention.

observability:
  enabled: true
  backend: clickhouse
  retention_days: 30                  # DELETE TTL — parts older than this are dropped
  clickhouse:
    dsn: clickhouse://runesight:[email protected]:9000/runesight
    database: runesight               # default
    table: logs                       # default
    auto_migrate: true                # default
    storage_policy: ""                # set to enable S3 tiering (below)
    s3_volume: s3                     # default
    hot_days: 7                       # used with storage_policy
KeyDefaultNotes
clickhouse.dsnRequired. clickhouse://user:pass@host:9000/db.
clickhouse.databaserunesightCreated by auto_migrate if missing.
clickhouse.tablelogsLikewise.
clickhouse.auto_migratetrueRune creates the schema on first connect.
clickhouse.storage_policy""Name of a server-side storage policy; setting it enables S3 tiering.
clickhouse.s3_volumes3The policy's volume name parts move to.
clickhouse.hot_days0Parts older than this move to s3_volume. 0 = stay hot forever.

Like Loki, this is bring-your-own: ClickHouse Cloud, existing infrastructure, or — since it's an ordinary workload — run it ON Rune with one cast:

git clone https://github.com/runestack/rune-examples
rune cast rune-examples/clickhouse/runeset --release clickhouse \
  --create-namespace --set clickhouse.password=$PASSWORD

Then the DSN host is the in-cluster DNS name: clickhouse.observability.rune:9000.

The schema: auto_migrate vs. operator-managed

With auto_migrate: true (the default), Rune connects lazily on first use and creates the database and table itself — zero-config. The table it creates:

CREATE TABLE IF NOT EXISTS runesight.logs (
  timestamp DateTime64(9) CODEC(Delta(8), ZSTD(1)),
  namespace LowCardinality(String),
  service   LowCardinality(String),
  instance  String,
  node      LowCardinality(String),
  level     LowCardinality(String),
  stream    LowCardinality(String),
  line      String CODEC(ZSTD(1)),
  labels    Map(LowCardinality(String), String),
  INDEX idx_line line TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 4
)
ENGINE = MergeTree
PARTITION BY toDate(timestamp)
ORDER BY (namespace, service, timestamp)

— day partitions, (namespace, service, timestamp) ordering, and a token bloom-filter index on line so |= "needle" filters don't full-scan.

Set auto_migrate: false when a DBA owns the schema: the table must then exist (matching columns) before runed starts ingesting, and Rune will never issue DDL.

Retention and S3 tiering

Both are expressed as table TTLs, applied by auto_migrate:

  • retention_days: 90TTL timestamp + INTERVAL 90 DAY DELETE — parts are dropped wherever they live.
  • storage_policy + hot_days: 7TTL timestamp + INTERVAL 7 DAY TO VOLUME 's3' — parts older than 7 days move from local disk to the policy's S3 volume.

Combined, that's: 7 hot days on NVMe, then S3 until day 90, then gone.

The storage policy itself is server-side ClickHouse configuration — Rune has no S3 client; it only names the policy in the table DDL. The operator adds the policy to ClickHouse's config.d. A complete, copy-paste example:

<!-- /etc/clickhouse-server/config.d/storage.xml -->
<clickhouse>
  <storage_configuration>
    <disks>
      <s3_disk>
        <type>s3</type>
        <endpoint>https://BUCKET.s3.REGION.amazonaws.com/runesight/</endpoint>
        <access_key_id>KEY</access_key_id>
        <secret_access_key>SECRET</secret_access_key>
        <metadata_path>/var/lib/clickhouse/disks/s3_disk/</metadata_path>
      </s3_disk>
    </disks>
    <policies>
      <runesight_tiered>
        <volumes>
          <hot>
            <disk>default</disk>
          </hot>
          <s3>
            <disk>s3_disk</disk>
          </s3>
        </volumes>
        <move_factor>0.1</move_factor>
      </runesight_tiered>
    </policies>
  </storage_configuration>
</clickhouse>

Then in the runefile: storage_policy: runesight_tiered, s3_volume: s3 (the volume name), hot_days: 7. Volume order matters — hot first. Any S3-compatible endpoint works (MinIO, R2, Spaces, GCS interop). The reference runeset wires this XML in via a single value, so tiering works out of the box when you provide bucket + credentials.

:::caution Changing storage_policy on an existing table is an ALTER the operator performs; auto_migrate applies the policy at table creation. Plan tiering before first ingest, or be ready to ALTER TABLE ... MODIFY TTL yourself. :::

What Advanced tier unlocks

The dashboard detects ClickHouse's capabilities and turns on:

  • SQL mode — a raw SQL editor against the logs table. The escape hatch for anything LogQL can't say: joins against labels, multi-level aggregations, GROUP BY anything.
  • Percentilesquantile_over_time(field, 0.95) over parsed numeric fields; powered by ClickHouse's native quantile functions.
  • High-cardinality filters — filtering on arbitrary labels keys stays cheap at volume (it's a Map column scan with bloom-filter help, not a label-index explosion).

One honest asymmetry: the | logfmt / | json parser stages are not lowered to SQL — Core-tier pipelines that use them run on embedded/Loki but error on ClickHouse. SQL mode is the (more powerful) replacement: JSONExtractString(line, 'field') and friends.

Operational notes

  • Lazy connectionruned starts even if ClickHouse is down; the forwarder spools and retries until it's reachable.
  • Sources page — ingestion metrics work as on every backend; store-level disk stats show "managed by the backend" (ClickHouse's system.parts is the authoritative view).
  • The adapter is integration-tested against a real ClickHouse (testcontainers) in the main repo.

See also