New to Kestra?
Use blueprints to kickstart your first workflows.
Audit ClickHouse slow and long-running queries via system.query_log and system.processes, alert on risk, and optionally kill one.
A query that takes ten seconds instead of one hundred milliseconds does not throw
an error - it just quietly eats CPU, memory, and concurrency slots until enough of
them pile up to degrade everything else on the cluster. This blueprint reads
ClickHouse's own system.query_log for queries that have already finished slowly,
and system.processes for queries that are still running past a time budget,
classifies the cluster into a three-state risk level, alerts on anything but a
clean bill of health, and can terminate the single longest-running active query
with ClickHouse's own KILL QUERY statement, behind two independent gates.
periodic_slow_query_check (io.kestra.plugin.core.trigger.Schedule) runs every 10 minutes, always forcing auto_remediate: "false" and dry_run: "true" through its own inputs: override regardless of this flow's defaults. Shipped disabled so you can validate a manual run first.check_historical_slow_queries (io.kestra.plugin.jdbc.clickhouse.Query, fetchType: FETCH_ONE) counts system.query_log rows with type = 'QueryFinish' in the lookback_minutes window, populating row.total_finished_queries, row.slow_query_count, row.max_duration_ms.check_active_long_queries (io.kestra.plugin.jdbc.clickhouse.Query, fetchType: FETCH_ONE) counts system.processes rows with elapsed > long_running_threshold_seconds, and picks the longest-elapsed one's query_id through a scalar subquery in the same statement, populating row.active_long_count, row.max_elapsed_seconds, row.oldest_active_query_id.evaluate_log_activity (io.kestra.plugin.core.flow.If) branches on total_finished_queries > 0.else (UNREACHABLE): alert_query_log_unreachable reports that the query log is empty for the whole window.then (log has data): nested evaluate_query_risk (io.kestra.plugin.core.flow.If) branches on slow_query_count > 0 or active_long_count > 0.else (HEALTHY): log_query_health_healthy records a clean check.then (AT_RISK): route_remediation (io.kestra.plugin.core.flow.Switch on auto_remediate) either considers remediation ("true", further gated by check_has_active_query - only a currently active query can be killed, not a finished one in the log - and then by dry_run through check_dry_run) or only logs that remediation is disabled ("false"). When every gate allows it, kill_long_running_query runs KILL QUERY WHERE query_id = 'oldest_active_query_id'. alert_query_risk always fires to Slack regardless of which remediation path ran.log_audit_complete always runs last, printing the headline numbers regardless of which branch fired.errors block alerts Slack separately if the flow itself fails outright - an unreachable cluster or bad credentials must not read as "no risk".KILL QUERY on the single longest-running active query, by query_id - it never kills an arbitrary or unrelated query, and never touches a query that has already finished.auto_remediate, dry_run) between "risk detected" and "query actually killed".SELECT * FROM system.processes by hand during an incident.Query driving two different system tables (system.query_log, system.processes) plus a KILL administrative statement, distinct from this repo's own system.mutations/system.parts watchdog and system.replicas health gate.system.query_log and system.processes run by hand answer one moment in time; they do not decide a cadence, classify the result into a three-state risk level, pick a single deterministic remediation target, gate a kill behind two independent confirmations, or notify anyone. Kestra supplies the schedule, the HEALTHY/AT_RISK/UNREACHABLE classification as a first-class branch, two independent authorization gates before any query is killed, and an execution history that shows exactly when query performance degraded and whether anything was done about it.
log_queries, on by default per ClickHouse's own documentation).SELECT on system.query_log and system.processes, and, if auto_remediate will ever be "true", permission to run KILL QUERY.Local testing:
docker run -d --name clickhouse-slow-query-gate -p 8124:8123 -p 9001:9000 clickhouse/clickhouse-server:latest
docker network connect YOUR_KESTRA_NETWORK clickhouse-slow-query-gate (check existing networks first with docker network ls; only needed if the Kestra Worker runs in a separate Docker network than this container; set clickhouse_url to jdbc:clickhouse://clickhouse-slow-query-gate:8123/default from inside that network, or jdbc:clickhouse://localhost:8124/default from the host). Host port 8124 is used here specifically so it never collides with Kestra's own UI on 8080.
CLICKHOUSE_USERNAME / CLICKHOUSE_PASSWORD: credentials used by every io.kestra.plugin.jdbc.clickhouse.Query task in this flow.SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_query_risk, alert_query_log_unreachable, and the errors block.SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_CLICKHOUSE_PASSWORD=$(echo -n '<your-local-password>' | base64). This keeps credentials out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.clickhouse_url (STRING, default jdbc:clickhouse://localhost:8124/default): JDBC URL.lookback_minutes (INT, default 60): window scanned in system.query_log.slow_query_threshold_ms (INT, default 5000): finished-query duration ceiling.long_running_threshold_seconds (INT, default 60): active-query elapsed-time ceiling.auto_remediate (SELECT: "false", "true"; default "false"): must be "true" for remediation to even be considered.dry_run (SELECT: "true", "false"; default "true"): must be explicitly "false", together with auto_remediate: "true", for KILL QUERY to actually run.outputs.check_historical_slow_queries.row.total_finished_queries / .slow_query_count / .max_duration_ms: historical query metrics, every run.outputs.check_active_long_queries.row.active_long_count / .max_elapsed_seconds / .oldest_active_query_id: live query metrics, every run; oldest_active_query_id is absent when active_long_count is 0.SELECT sleep(10) repeated) to populate both system.query_log and, while it runs, system.processes.CLICKHOUSE_USERNAME, CLICKHOUSE_PASSWORD, and SLACK_WEBHOOK_URL secrets.alert_query_risk fires once a query crosses either threshold.auto_remediate: "true" and dry_run: "true" and confirm log_dry_run_remediation describes the kill without running it.auto_remediate: "true" and dry_run: "false" while a long query is still active, and confirm kill_long_running_query runs, then verify with SELECT * FROM system.processes that it is gone.lookback_minutes at a very short window on a quiet cluster and re-run to confirm alert_query_log_unreachable fires (UNREACHABLE) instead of either HEALTHY or AT_RISK.periodic_slow_query_check once you trust the check; it always runs in the safe auto_remediate: "false" / dry_run: "true" mode regardless of what you leave the flow's own defaults set to.user or current_database from system.query_log to the Slack alert, so the team that owns the slow query is named, not just the count.exception_code/exception from system.query_log (type IN ('ExceptionBeforeStart', 'ExceptionWhileProcessing')) as a fourth signal alongside slow and long-running queries.clickhouse_url values with io.kestra.plugin.core.flow.Loop to run the same gate across every node in a cluster, since system.processes is local to the node it is queried on.Query task's own connection and is caught by the errors block, not by evaluate_log_activity - that branch only fires when the query itself succeeds but returns zero QueryFinish rows.check_has_active_query exists specifically so an AT_RISK state caused only by historical entries in system.query_log is reported as unremediable, rather than silently doing nothing while claiming a kill was considered.system.processes is per-node, not cluster-wide. On a multi-node cluster, this flow's check_active_long_queries only sees queries currently running on the node named in clickhouse_url - see How to extend for looping across nodes.system.mutations, system.parts, system.replicas, system.users): all system-table introspection and all SYSTEM/KILL administrative statements go through the same generic io.kestra.plugin.jdbc.clickhouse.Query task.auto_remediate and dry_run are independent gates, not a single boolean. Both must be "true"/"false" respectively for KILL QUERY to run; setting only one leaves the other still blocking.Switch case keys "true"/"false" are quoted strings. auto_remediate renders as the literal string "true" or "false"; unquoted true:/false: YAML map keys would parse as booleans instead and would not match.system.query_log and system.processes column names are taken verbatim from ClickHouse's own system-tables documentation, and KILL QUERY syntax from ClickHouse's own SQL reference - but no Docker container or Kestra engine was run to execute this flow end to end.