Skip to content
All flows

Ship OTel data to ClickHouse

The ClickStack (HyperDX) ingestion collector as upstream ships it: parses JSON log bodies into attributes, infers severity from level fields or the body text, and routes browser session-replay events to their own ClickHouse table.

This is the collector configuration the ClickStack distribution runs, merged from HyperDX's base and standalone halves (docker/otel-collector) so it works on a stock contrib build. The otlp/hyperdx receiver takes all three signals on gRPC :4317 and HTTP :4318 with CORS open for the browser SDK, memory_limiter and a 10,000-item, 5-second batch shape inserts the way ClickHouse wants them, and the clickhouse exporter writes the otel_* tables HyperDX reads, creating them on first start.

Logs take a detour. The routing/logs connector sends records carrying an rr-web.event attribute (HyperDX session replay) to logs/out-rrweb, where a second clickhouse/rrweb exporter writes them to the hyperdx_sessions table. Everything else goes through logs/out-default, where one transform processor parses JSON bodies into attributes, fills severity_text and severity_number from whichever level/severity spelling a library used or from the first severity word in the body, and lowercases the result so HyperDX's severity filter matches.

Set CLICKHOUSE_ENDPOINT, CLICKHOUSE_DATABASE, CLICKHOUSE_USER, and CLICKHOUSE_PASSWORD. The exporter timeout and the JSON column switch read the same HYPERDX_OTEL_* variables as upstream, with upstream's defaults, so a ClickStack .env drops in unchanged. Nothing here masks or drops data: put the PII Redaction Gateway flow in front of it if you need that.

How the data moves
receiverprocessorconnectorexporterextension
traces
otlp/hyperdxmemory_limiterbatchclickhouse
metrics
otlp/hyperdxmemory_limiterbatchclickhouse
logs/in
otlp/hyperdxrouting/logs
logs/out-default
routing/logsmemory_limitertransformbatchclickhouse
logs/out-rrweb
routing/logsmemory_limiterbatchclickhouse/rrweb

Use this flow

Open the config in Telflo and it becomes a working pipeline on the canvas: adapt what's specific to you, test it against recorded traffic, and push it to your fleet over OpAMP. Free account, no card.

Components

What's in it, and why

otlp/hyperdx

Single entry point for all three signals on gRPC :4317 and HTTP :4318, with include_metadata on and every origin and header allowed over CORS so HyperDX's browser SDK can post straight from a page.

memory_limiter

Caps the collector heap (1500 MiB, 512 MiB spike) and turns overload into backpressure at the receiver before ClickHouse-sized batches pile up in memory.

transform

Parses JSON log bodies into attributes, infers severity_text and severity_number from level/severity/log.level attributes or the first severity keyword in the body, then lowercases severity_text so HyperDX's severity filter matches.

batch

Builds the large, infrequent inserts ClickHouse wants: 10,000 items or 5 seconds, whichever comes first, instead of the 200 ms default flush.

routing/logs

Splits logs by content: records with an rr-web.event attribute go to the session-replay pipeline, everything else to the default one, so replay payloads never run through the parsing transform.

clickhouse

Writes otel_logs, otel_traces, and otel_metrics_* in the database HyperDX reads, with a 720h table TTL, create_schema on, and the JSON column type behind HYPERDX_OTEL_EXPORTER_CLICKHOUSE_JSON_ENABLE.

clickhouse/rrweb

Same connection, but logs_table_name is hyperdx_sessions: the table HyperDX's session replay view reads browser recording events from.

health_check

Liveness endpoint on :13133 for Docker and Kubernetes probes.

Notes

Gotchas

  • 1

    create_schema: true runs CREATE DATABASE and CREATE TABLE on every start, which is how a fresh ClickStack comes up with its tables; once they exist, set it to false so a TTL or engine change is a deliberate migration rather than whatever the next collector start applies.

  • 2

    The json switch (HYPERDX_OTEL_EXPORTER_CLICKHOUSE_JSON_ENABLE) makes the exporter create tables with the JSON column type instead of Map, and the exporter never alters tables that already exist, so decide before the first start against a database, not after.

  • 3

    The routing/logs match is IsMatch(attributes["rr-web.event"], ".*"), so any log record that carries an rr-web.event attribute, whatever its value, is treated as a session-replay event and skips the parsing transform.

  • 4

    Severity inference only runs on records that arrive with severity_number 0 and an empty severity_text, and the body scan only reads the first 256 characters, so a record whose only level marker sits deep in a long line is filed as info.

  • 5

    The JSON body parse merges with the upsert strategy, so a key inside the body overwrites an attribute the SDK already set with the same name, and flatten turns nested objects into dotted keys.

  • 6

    There is no file_storage extension and the clickhouse exporter runs on the default in-memory sending queue, exactly as upstream ships it; a ClickHouse outage longer than the retry window (max_elapsed_time: 300s) drops data unless you add a persistent queue or put the Fail over to S3 flow in front of it.

  • 7

    The otlp/hyperdx receiver allows every origin and header over HTTP because HyperDX's browser SDK posts straight to it; if only servers send to this collector, tighten cors before exposing :4318.

  • 8

    Metrics support in the clickhouse exporter is still alpha (traces and logs are beta), so treat the otel_metrics_* table schemas as subject to change between collector upgrades.

Sources and references (16)Show

Everything consulted while researching and fact-checking this flow, including the README of every component it uses.

Test it before your fleet runs it

Free account, no card. Open this flow in the editor, adapt it, and see what it does to real data before anything ships.