Skip to content

Deployment

Deployment

A production checklist for running the ClickHouse OTEL store.

1. ClickHouse

Any ClickHouse ≥ 23.8 works (tested against 24.8 and 26.7). Run it however you run stateful services — a managed ClickHouse, a container, or a binary.

docker run -d --name clickhouse \
  -p 8123:8123 -p 9000:9000 \
  --ulimit nofile=262144:262144 \
  clickhouse/clickhouse-server:24.8

Host access (the one gotcha)

The official image locks the default user to connections from inside the container (::1/127.0.0.1). Requests from your app arrive through the port mapping as the Docker gateway IP and are rejected with an auth error. Either:

  • Set a password-e CLICKHOUSE_PASSWORD=… opens the user to any network; put the same value in TELEMETRY_STORE_CLICKHOUSE_PASSWORD. (Recommended.)
  • or mount an override opening the network:
    <!-- users.d/open.xml -->
    <clickhouse><users><default>
      <networks replace="replace"><ip>::/0</ip></networks>
    </default></users></clickhouse>
    

Put ClickHouse on a private network and never expose 8123/9000 publicly.

2. Schema

php artisan telemetry-store:install

Idempotent (CREATE TABLE IF NOT EXISTS). Re-run after upgrading. Retention is a per-table TTL from telemetry-store.retention_days (default 30); ClickHouse drops whole day-partitions, so retention is essentially free. Bump it and re-run, or ALTER TABLE … MODIFY TTL for an immediate change.

Sizing: budget by ingested rows/day × retention. Start with the defaults and watch system.parts / disk. Columnar compression is generous — in a local load probe, 500k log rows were ~5.8 MiB on disk and 200k spans ~1.2 MiB.

Measured (single-node, local load probe)

A rough sense of scale (one ClickHouse container, synchronous batched inserts):

Signal Ingest Read latency (over 500k logs / 200k spans / 10k metric series)
Logs ~200k rows/s line filter 19ms · labelValues 5ms
Traces ~180k rows/s error search 18ms · duration filter 24ms
Metrics ~145k points/s p95-by-route 7ms · _count increase 5ms · quantile range 6ms

Not a benchmark, but it shows the schema is indexed/partitioned well enough that dashboard queries stay in single/low-double-digit milliseconds at that volume.

3. Ingest

Point the emitter's OTLP exporter at this app — no OpenTelemetry Collector:

# in the app(s) emitting telemetry
TELEMETRY_OTLP_ENDPOINT=https://dashboard.example.com/telemetry-store

Secure the endpoint (both are opt-in; set at least one in production):

TELEMETRY_STORE_INGEST_TOKEN=$(openssl rand -hex 32)   # emitter sends it as a bearer token
TELEMETRY_STORE_INGEST_ALLOWED_IPS=10.0.0.0/8          # optional IP allowlist

Batching / throughput. Inserts use ClickHouse [async inserts] (async_insert=1, wait_for_async_insert=0 in telemetry-store.clickhouse.settings) — ClickHouse buffers writes server-side and flushes in batches, which is what you want under load. Keep wait=0 in production (fire-and-forget, low latency); set wait=1 only in tests that read immediately after writing. Run the ingest route on a queue/worker-friendly path if you expect very high volume.

4. Connect the dashboard

In cboxdk/laravel-telemetry-ui, point each connection at the store. Mix and match — you can move one signal at a time:

// config/telemetry-ui.php
'connections' => [
    'logs'    => ['driver' => 'clickhouse-logs',    'url' => env('TELEMETRY_STORE_CLICKHOUSE_URL'), 'database' => 'telemetry'],
    'traces'  => ['driver' => 'clickhouse-traces',  'url' => env('TELEMETRY_STORE_CLICKHOUSE_URL'), 'database' => 'telemetry'],
    'metrics' => ['driver' => 'clickhouse-metrics', 'url' => env('TELEMETRY_STORE_CLICKHOUSE_URL'), 'database' => 'telemetry'],
],

Connection config keys: url, database, username, password, timeout. Anything omitted falls back to telemetry-store.clickhouse.*.

5. Verify

  • curl "$CH/?query=SELECT+count()+FROM+telemetry.otel_logs" after some traffic.
  • Open the dashboard — Logs, Traces and a metrics page should populate.

Known limits

  • Metric naming assumes the emitter's Prometheus conventions (Ingest\PromName); a custom emitter with different unit suffixes needs that map adjusted.
  • Counter rate/increase is a windowed max−min (no counter-reset stitching); histogram quantiles sum bucket counts across the window. Good for dashboards; not a metrics-accuracy SLA.
  • Ingest is synchronous per request (relying on ClickHouse async inserts for batching); a dedicated spool/queue is a future option for extreme volume.