Loading...
Loading...
Use when a user wants to wire an OpenTelemetry collector into a Managed ClickStack service on ClickHouse Cloud, either by deploying a new local collector (Docker run or Docker Compose) or by configuring their own existing collector, then send rich synthetic telemetry and verify it is visible in ClickStack.
npx skill4agent add clickhouse/agent-skills clickstack-otel-collectorclickhousectldockerdocker composedockerhyperdx_ingestotel.*otel0600--env-filedocker run -edocker composeCOLLECTOR_PATHnewexistingDEPLOY_MODEcomposerun| Command prefix | Used for | Needed when |
|---|---|---|
| generate the OTLP token and SQL password | always |
| auth, resolve the service, run SQL via the Query API | always |
| parse JSON from | always |
| run/inspect the collector and the telemetry generator | new-collector path, and the optional telemetry check |
| local health check against | new-collector path |
oteldockercurlclickhousectlcurl … | shcurlCREATE USERGRANTclickhousectlSERVICE_REF0600docker run --env-fileumaskWORKDIR="${WORKDIR:-$HOME/clickstack-otel-collector}"
mkdir -p "$WORKDIR" && chmod 700 "$WORKDIR"
ENV_FILE="$WORKDIR/collector.env"
# Generate secrets WITHOUT printing them; write straight into a private file.
( umask 177
{
echo "SERVICE_REF=$SERVICE_REF"
echo "OTLP_AUTH_TOKEN=$(openssl rand -hex 32)"
echo "CLICKHOUSE_USER=hyperdx_ingest"
echo "CLICKHOUSE_PASSWORD=$(openssl rand -hex 24)Aa1-"
echo "HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE=otel"
} > "$ENV_FILE"
)
chmod 600 "$ENV_FILE"
ls -l "$ENV_FILE"CLICKHOUSE_USERCLICKHOUSE_PASSWORDCLICKHOUSE_ENDPOINTHYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASECLICKHOUSE_PASSWORDcode: 516, Authentication failedCREATE USER@:/?#%code: 516Aa1--KEY=VALUE--env-fileOTLP_AUTH_TOKENCLICKHOUSE_*WORKDIRENV_FILEWORKDIRENV_FILEWORKDIR="${WORKDIR:-$HOME/clickstack-otel-collector}"; ENV_FILE="$WORKDIR/collector.env"
[ -f "$WORKDIR/creds.env" ] && . "$WORKDIR/creds.env"; set -a; . "$ENV_FILE"; set +aWORKDIR${WORKDIR:-…}SERVICE_REFcollector.env0600grep OTLP_AUTH_TOKEN "$ENV_FILE"0600clickhousectlclickhousectlPATH|| curl … | shwhich clickhousectlcurl -fsSL https://clickhouse.com/cli | shclickhousectl cloud auth statusAPI keyActiveI need a ClickHouse Cloud Admin API key to create the ingest user and verify the data. Please don't paste it here. Instead:
In the Cloud console, open Organization → API keys → New API key, and give it the Admin role. (Developer-scoped keys can't provision the per-service Query API endpoint thatuses.)cloud service query In a separate terminal, run:bashclickhousectl cloud auth login --api-key <key-id> --api-secret <key-secret> Tell me when that's done and I'll re-check the auth status.
ActiveSERVICE_REFcloud service getgetcloud service listclickhousectl cloud auth status
clickhousectl cloud service list --json | jq -r '.[].name'SERVICE_REFclickhousectlauth statusActiveclickhousectlNo credentials found.jq# Write a private, sourceable creds file next to collector.env.
( umask 177
{ echo "export CLICKHOUSE_CLOUD_API_KEY=$(jq -r .api_key "$HOME/.clickhouse/credentials.json")"
echo "export CLICKHOUSE_CLOUD_API_SECRET=$(jq -r .api_secret "$HOME/.clickhouse/credentials.json")"
} > "$WORKDIR/creds.env"
)
chmod 600 "$WORKDIR/creds.env"clickhousectl. "$WORKDIR/creds.env"; set -a; . "$ENV_FILE"; set +aservice listclickhousectl auth statusAPI key … Activecreds.envSERVICE_REFWORKDIR="${WORKDIR:-$HOME/clickstack-otel-collector}"; ENV_FILE="$WORKDIR/collector.env"
[ -f "$WORKDIR/creds.env" ] && . "$WORKDIR/creds.env"; set -a; . "$ENV_FILE"; set +a# UUID form
clickhousectl cloud service get "$SERVICE_REF" --json > "$WORKDIR/svc.json"
# Name form (note the double quotes: service names can contain spaces or apostrophes,
# e.g. "Alex's test")
clickhousectl cloud service list --json \
| jq --arg n "$SERVICE_REF" '.[] | select(.name==$n)' > "$WORKDIR/svc.json"8443.0:8443.0SERVICE_ID=$(jq -r '.id' "$WORKDIR/svc.json")
SERVICE_NAME=$(jq -r '.name' "$WORKDIR/svc.json")
STATE=$(jq -r '.state' "$WORKDIR/svc.json")
CLICKHOUSE_ENDPOINT=$(jq -r '.endpoints[] | select(.protocol=="https")
| "https://\(.host):\(.port | tonumber | floor)"' "$WORKDIR/svc.json")
# Persist the resolved values back into the env file for later steps and docker --env-file.
# Append only if the key is not already present, so a second run does not duplicate lines.
grep -q '^SERVICE_ID=' "$ENV_FILE" || echo "SERVICE_ID=$SERVICE_ID" >> "$ENV_FILE"
grep -q '^CLICKHOUSE_ENDPOINT=' "$ENV_FILE" || echo "CLICKHOUSE_ENDPOINT=$CLICKHOUSE_ENDPOINT" >> "$ENV_FILE"
printf 'service=%q state=%s endpoint=%s\n' "$SERVICE_NAME" "$STATE" "$CLICKHOUSE_ENDPOINT"STATErunningstoppedstartingclickhousectl cloud service query --id "$SERVICE_ID" --query "SELECT version()"clickhousectlProvisioning Query API endpoint + key for service '<name>'...hyperdx_ingestotel.*hyperdx_ingest$CLICKHOUSE_PASSWORDExpect an approval prompt here. The/CREATE USERstatements below are DDL against a Cloud service, so some agent sandboxes flag them as "modifying shared production infrastructure" even whenGRANTis allowlisted. This is expected; the operations are scoped to a single dedicated ingest user and theclickhousectlschema, and the user should approve them explicitly when prompted.otel
sha256_hashIDENTIFIED WITH sha256_password BY '$CLICKHOUSE_PASSWORD'pssha256_hashsha256_passwordWORKDIR="${WORKDIR:-$HOME/clickstack-otel-collector}"; ENV_FILE="$WORKDIR/collector.env"
[ -f "$WORKDIR/creds.env" ] && . "$WORKDIR/creds.env"; set -a; . "$ENV_FILE"; set +a
# SHA-256 of the password. openssl is already a dependency; this is portable (macOS + Linux).
# Only this hash ever reaches SQL, output, or `ps`; the plaintext stays in the env file.
PW_HASH=$(printf %s "$CLICKHOUSE_PASSWORD" | openssl dgst -sha256 | awk '{print $NF}')
# Send statements ONE AT A TIME: the Query API runs over HTTP and rejects multi-statement input
# ("Multi-statements are not allowed"), so a single ; -separated batch fails.
clickhousectl cloud service query --id "$SERVICE_ID" --query \
"CREATE USER IF NOT EXISTS hyperdx_ingest IDENTIFIED WITH sha256_hash BY '$PW_HASH'"
# Re-run safe: force the password to this run's value if the user already existed.
clickhousectl cloud service query --id "$SERVICE_ID" --query \
"ALTER USER hyperdx_ingest IDENTIFIED WITH sha256_hash BY '$PW_HASH'"otel.*otelotel.*clickhousectl cloud service query --id "$SERVICE_ID" --query \
"GRANT SELECT, INSERT, CREATE DATABASE, CREATE TABLE, CREATE VIEW ON otel.* TO hyperdx_ingest"Older image builds: some earlier collector versions ran their goose migrations against a version table in thedatabase, so startup looped ondefaultuntilACCESS_DENIEDwas also granted. If you seedefault.*referencingACCESS_DENIEDin the collector logs (Step 6), add this and restart the container:defaultbashclickhousectl cloud service query --id "$SERVICE_ID" --query \ "GRANT SELECT, INSERT, CREATE TABLE ON default.* TO hyperdx_ingest"
clickhousectl cloud service query --id "$SERVICE_ID" --query "SHOW GRANTS FOR hyperdx_ingest"GRANT SELECT, INSERT, CREATE DATABASE, CREATE TABLE, CREATE VIEW ON otel.* TO hyperdx_ingestotel$WORKDIR$ENV_FILE$SERVICE_IDWORKDIR="${WORKDIR:-$HOME/clickstack-otel-collector}"; ENV_FILE="$WORKDIR/collector.env"
[ -f "$WORKDIR/creds.env" ] && . "$WORKDIR/creds.env"; set -a; . "$ENV_FILE"; set +a
docker info > /dev/nullDEPLOY_MODE=composecollector.envcat > "$WORKDIR/docker-compose.yaml" <<'EOF'
name: clickstack
services:
otel-collector:
image: clickhouse/clickstack-otel-collector:latest
container_name: clickstack-otel-collector
env_file: ./collector.env
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "13133:13133" # health
restart: unless-stopped
networks: [clickstack-net]
networks:
clickstack-net:
name: clickstack-net
EOF
# Compose refuses to adopt a clickstack-net it did not create (a leftover from the docker run
# path, a prior failed Compose run, or a DEPLOY_MODE switch), failing with "network clickstack-net
# was found but has incorrect label". If an orphan exists with no containers attached, remove it so
# Compose can recreate it with its own labels.
if docker network inspect clickstack-net >/dev/null 2>&1 \
&& [ -z "$(docker network inspect clickstack-net -f '{{range .Containers}}{{.Name}} {{end}}')" ]; then
docker network rm clickstack-net
fi
( cd "$WORKDIR" && docker compose up -d )clickstack-netDEPLOY_MODE=rundocker network create clickstack-net 2>/dev/null || true--env-file-epsdocker rm -fdocker rm -f clickstack-otel-collector 2>/dev/null || true
docker run -d \
--name clickstack-otel-collector \
--network clickstack-net \
--env-file "$ENV_FILE" \
-p 4317:4317 \
-p 4318:4318 \
-p 13133:13133 \
clickhouse/clickstack-otel-collector:latestOTLP_AUTH_TOKENCLICKHOUSE_ENDPOINTCLICKHOUSE_USERCLICKHOUSE_PASSWORDHYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASEauthorizationBearer COLLECTOR_PATH=existingrrwebotel${env:…}${env:VAR}--env-file# When running the contrib collector in Docker, pass collector.env so ${env:CLICKHOUSE_*} resolve:
# docker run -d --env-file "$ENV_FILE" -p 4317:4317 -p 4318:4318 \
# -v "$WORKDIR/your-config.yaml:/etc/otelcol-contrib/config.yaml:ro" \
# otel/opentelemetry-collector-contrib:latest
# For a non-Docker collector, export CLICKHOUSE_ENDPOINT and CLICKHOUSE_PASSWORD into its
# environment (e.g. an EnvironmentFile= in the systemd unit) before it starts.receivers:
otlp/hyperdx:
protocols:
grpc:
include_metadata: true
endpoint: "0.0.0.0:4317"
http:
cors:
allowed_origins: ["*"]
allowed_headers: ["*"]
include_metadata: true
endpoint: "0.0.0.0:4318"
processors:
batch:
memory_limiter:
limit_mib: 1500
spike_limit_mib: 512
check_interval: 5s
connectors:
routing/logs:
default_pipelines: [logs/out-default]
error_mode: ignore
table:
- context: log
statement: route() where IsMatch(attributes["rr-web.event"], ".*")
pipelines: [logs/out-rrweb]
exporters:
clickhouse:
database: otel
endpoint: ${env:CLICKHOUSE_ENDPOINT}
username: hyperdx_ingest
password: ${env:CLICKHOUSE_PASSWORD}
ttl: 720h
timeout: 5s
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 300s
clickhouse/rrweb:
database: otel
endpoint: ${env:CLICKHOUSE_ENDPOINT}
username: hyperdx_ingest
password: ${env:CLICKHOUSE_PASSWORD}
ttl: 720h
logs_table_name: hyperdx_sessions
timeout: 5s
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 300s
service:
pipelines:
traces:
receivers: [otlp/hyperdx]
processors: [memory_limiter, batch]
exporters: [clickhouse]
metrics:
receivers: [otlp/hyperdx]
processors: [memory_limiter, batch]
exporters: [clickhouse]
logs/in:
receivers: [otlp/hyperdx]
exporters: [routing/logs]
logs/out-default:
receivers: [routing/logs]
processors: [memory_limiter, batch]
exporters: [clickhouse]
logs/out-rrweb:
receivers: [routing/logs]
processors: [memory_limiter, batch]
exporters: [clickhouse/rrweb]OTLP_AUTH_TOKENbearertokenauthdocker ps --filter name=clickstack-otel-collector --format '{{.Status}}'
curl -fsS http://localhost:13133/ && echo
docker logs --tail 40 clickstack-otel-collector 2>&1 | tail -40[seed] OK ...goose: up to current file version: NEverything is ready. Begin running and processing data.docker psUp ... (healthy)ClickHouse 25.12 < 26.2, falling back to compatibility logs and traces schemascode: 516, Authentication failed: password is incorrectCLICKHOUSE_PASSWORDCLICKHOUSE_PASSWORD@ : / ? # %[HTTP 403]data size should be 0 < <huge number>CLICKHOUSE_ENDPOINThttps://<host>:8443.0ACCESS_DENIEDdefaultdefault.*telemetrygen$OTLP_AUTH_TOKENtgtelemetrygenauthorizationtelemetrygenkey="value"WORKDIR="${WORKDIR:-$HOME/clickstack-otel-collector}"; ENV_FILE="$WORKDIR/collector.env"
[ -f "$WORKDIR/creds.env" ] && . "$WORKDIR/creds.env"; set -a; . "$ENV_FILE"; set +a
TG_IMAGE=ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen:latest
NET=clickstack-net
ENDPOINT=clickstack-otel-collector:4317
TG_LOG="$WORKDIR/telemetrygen.log"; : > "$TG_LOG"
tg() {
# usage: tg <logs|traces|metrics> [extra telemetrygen flags...]
# Output (which contains the token in the echoed config) goes to $TG_LOG, never the terminal.
local signal="$1"; shift
docker run --rm --network "$NET" "$TG_IMAGE" "$signal" \
--otlp-endpoint "$ENDPOINT" \
--otlp-insecure \
--otlp-header "authorization=\"$OTLP_AUTH_TOKEN\"" \
--rate 10 --duration 15s "$@" >>"$TG_LOG" 2>&1
echo "$signal exit=$?"
}Existing-collector path: setandNETto reach your collector instead. If it runs on this host, useENDPOINTstyle access or point--network hostat its published address, and set theENDPOINTheader (or other auth) to whatever your receiver expects. Everything below is otherwise identical.authorization
rpc error: code = Canceled desc = grpc: the client connection is closing--durationtg<signal> exit=0grep -c Unauthenticated "$TG_LOG"authorizationtelemetrygenkey="value"key=valuevalue should be a string wrapped in double quotes--otlp-attributes deployment.environment="production"deployment.environment=productiontgtg logs --service checkout --severity-text Info --severity-number 9 \
--body "checkout completed" \
--otlp-attributes 'deployment.environment="production"' \
--telemetry-attributes 'http.method="POST"'
tg logs --service payment --severity-text Error --severity-number 17 \
--body "payment gateway timeout" \
--otlp-attributes 'deployment.environment="production"' \
--telemetry-attributes 'http.status_code="500"'tg traces --service checkout --child-spans 4 --span-duration 120ms --status-code Ok \
--otlp-attributes 'deployment.environment="production"' \
--telemetry-attributes 'http.route="/cart"'
tg traces --service payment --child-spans 3 --span-duration 400ms --status-code Error \
--otlp-attributes 'deployment.environment="production"' \
--telemetry-attributes 'http.route="/charge"'tg metrics --service checkout --metric-type Sum
tg metrics --service checkout --metric-type Gauge
tg metrics --service payment --metric-type Histogram--metric-typeGaugeSumHistogramExponentialHistogram--otlp-http--otlp-endpoint clickstack-otel-collector:4318clickhousectl cloud service query --id "$SERVICE_ID" --query \
"SELECT name FROM system.tables WHERE database='otel' ORDER BY name"parts.rowsclickhousectl cloud service query --id "$SERVICE_ID" --query \
"SELECT table, sum(rows) AS rows
FROM system.parts
WHERE database='otel' AND active
AND table IN ('otel_logs','otel_traces',
'otel_metrics_sum','otel_metrics_gauge',
'otel_metrics_histogram','otel_metrics_exponential_histogram',
'otel_metrics_summary')
GROUP BY table ORDER BY table"rowsotel_logsotel_tracesotel_metrics_sumotel_metrics_gaugeotel_metrics_histogramdocker logs --tail 50 clickstack-otel-collectorauthorization$OTLP_AUTH_TOKENgrep -c Unauthenticated "$TG_LOG"code = Unauthenticated desc = provided authorization does not match expected scheme or tokenCLICKHOUSE_ENDPOINThttps://:8443clickhousectl cloud service query --id "$SERVICE_ID" --query "SELECT 1"11otelhttps://console.clickhouse.cloud/services/<SERVICE_ID>/clickstack$SERVICE_IDSELECT 1✅ ClickStack is set up and ingesting telemetry for service <SERVICE_NAME> (<SERVICE_ID>).
Complete onboarding in the console (Step 8) to auto-detect the sources and see your data.
Collector
▸ New local collector via <Docker Compose | docker run> (or: configured your existing collector)
▸ Send OTLP gRPC to: localhost:4317
▸ Send OTLP HTTP to: localhost:4318
▸ Health check: http://localhost:13133/ (local collector only)
▸ Required header: authorization: <OTLP token>
(retrieve with: grep OTLP_AUTH_TOKEN <WORKDIR>/collector.env)
ClickHouse target
▸ Endpoint: <CLICKHOUSE_ENDPOINT>
▸ SQL user: hyperdx_ingest (password in <WORKDIR>/collector.env, mode 0600)
▸ Database: otel
Finish in the ClickHouse Cloud console:
▸ Open the service, select ClickStack in the left menu, then Getting Started,
and complete onboarding to auto-detect sources.
▸ https://console.clickhouse.cloud/services/<SERVICE_ID>/clickstack<WORKDIR>/collector.env0600docker runcd <WORKDIR> && docker compose downdocker compose up -ddocker stop clickstack-otel-collectordocker start clickstack-otel-collectorlocalhost:4317localhost:4318authorization# Docker Compose deployment:
( cd "$WORKDIR" && docker compose down )
# Individual Docker deployment:
docker rm -f clickstack-otel-collector
docker network rm clickstack-net 2>/dev/null || true
# Either deployment, remove the ingest user:
clickhousectl cloud service query --id "$SERVICE_ID" --query "DROP USER IF EXISTS hyperdx_ingest"
# Optionally remove the local files once they are no longer needed:
# rm -f "$WORKDIR/collector.env" "$WORKDIR/svc.json" "$WORKDIR/docker-compose.yaml"otel