Skip to content
Open
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ set(BUILD_BENCHMARK OFF CACHE BOOL "Disable clickhouse-cpp benchmarks" FORCE)
add_subdirectory(third_party/clickhouse-cpp EXCLUDE_FROM_ALL)

# Collect source files
file(GLOB_RECURSE SOURCES src/*.cc)
file(GLOB_RECURSE SOURCES src/*.cc src/*.c)

# Build shared library
add_library(pg_stat_ch SHARED ${SOURCES})
Expand Down
2 changes: 2 additions & 0 deletions docker/init/00-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ CREATE TABLE pg_stat_ch.events_raw

query_id Int64 COMMENT '64-bit hash identifying normalized queries. Queries differing only in constants share the same query_id. Use for aggregating statistics across similar queries.',

parent_query_id UInt64 COMMENT 'query_id of the calling query (e.g. the plpgsql function that issued this SPI statement). 0 for top-level queries. Use WHERE parent_query_id = 0 to restrict aggregations to top-level queries and avoid double-counting CPU and duration.',

cmd_type LowCardinality(String) COMMENT 'Command type: SELECT, INSERT, UPDATE, DELETE, MERGE, UTILITY, or UNKNOWN. Use for workload characterization (read-heavy vs write-heavy).',

rows UInt64 COMMENT 'Rows returned (SELECT) or affected (INSERT/UPDATE/DELETE). HIGH: large result sets or bulk operations. LOW: point queries. Watch for unexpected HIGH values indicating missing WHERE clauses.',
Expand Down
12 changes: 12 additions & 0 deletions migrations/001_add_parent_query_id.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Migration: add parent_query_id column
--
-- Introduced in pg_stat_ch 0.4.x. Each event now carries the query_id of its
-- calling query (e.g. the plpgsql function that issued an SPI statement).
Comment on lines +3 to +4
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration header says this is introduced in pg_stat_ch 0.4.x, but the extension control file currently advertises default_version = 0.1. Please align the referenced version (or describe it without a specific version) to avoid confusing upgrade guidance.

Suggested change
-- Introduced in pg_stat_ch 0.4.x. Each event now carries the query_id of its
-- calling query (e.g. the plpgsql function that issued an SPI statement).
-- Introduces parent_query_id support. Each event now carries the query_id of
-- its calling query (e.g. the plpgsql function that issued an SPI statement).

Copilot uses AI. Check for mistakes.
-- Top-level queries emit 0. Use WHERE parent_query_id = 0 in aggregations to
-- avoid double-counting CPU and duration across nested calls.
--
-- Run against your ClickHouse instance before upgrading the extension:
-- clickhouse-client < migrations/001_add_parent_query_id.sql

ALTER TABLE pg_stat_ch.events_raw
ADD COLUMN IF NOT EXISTS parent_query_id UInt64 DEFAULT 0;
101 changes: 47 additions & 54 deletions src/config/guc.cc → src/config/guc.c
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
// pg_stat_ch GUC (Grand Unified Configuration) implementation

extern "C" {
#include "postgres.h"

#include "utils/guc.h"
}

#include <array>

#include "config/guc.h"

// GUC variable storage
bool psch_enabled = true;
bool psch_use_otel = false;
char* psch_clickhouse_host = nullptr;
char* psch_clickhouse_host = NULL;
int psch_clickhouse_port = 9000;
char* psch_clickhouse_user = nullptr;
char* psch_clickhouse_password = nullptr;
char* psch_clickhouse_database = nullptr;
char* psch_clickhouse_user = NULL;
char* psch_clickhouse_password = NULL;
char* psch_clickhouse_database = NULL;
bool psch_clickhouse_use_tls = false;
bool psch_clickhouse_skip_tls_verify = false;
char* psch_otel_endpoint = nullptr;
char* psch_hostname = nullptr;
char* psch_otel_endpoint = NULL;
char* psch_hostname = NULL;
int psch_queue_capacity = 131072;
int psch_flush_interval_ms = 200;
int psch_batch_max = 200000;
Expand All @@ -35,7 +31,7 @@ bool psch_debug_force_locked_overflow = false;

// Log level options (matches PostgreSQL's server_message_level_options pattern)
// clang-format off
static const std::array<config_enum_entry, 13> log_elevel_options = {{
static const struct config_enum_entry log_elevel_options[] = {
{"debug5", DEBUG5, false},
{"debug4", DEBUG4, false},
{"debug3", DEBUG3, false},
Expand All @@ -48,16 +44,15 @@ static const std::array<config_enum_entry, 13> log_elevel_options = {{
{"error", ERROR, false},
{"fatal", FATAL, false},
{"panic", PANIC, false},
{nullptr, 0, false},
}};
{NULL, 0, false},
};
// clang-format on

extern "C" {

// Check hook to ensure queue_capacity is a power of 2.
// Parameters follow PostgreSQL GUC check hook signature.
static bool check_psch_queue_capacity(int* newval, void** extra [[maybe_unused]],
GucSource source [[maybe_unused]]) {
static bool check_psch_queue_capacity(int* newval, void** extra, GucSource source) {
(void)extra;
(void)source;
// Check if value is positive and a power of 2
if (*newval <= 0) {
GUC_check_errdetail("pg_stat_ch.queue_capacity must be positive.");
Expand All @@ -81,12 +76,12 @@ void PschInitGuc(void) {
DefineCustomBoolVariable(
"pg_stat_ch.enabled", // name
"Enable or disable pg_stat_ch query telemetry collection.", // short_desc
nullptr, // long_desc
NULL, // long_desc
&psch_enabled, // valueAddr
true, // bootValue
PGC_SIGHUP, // context
0, // flags
nullptr, nullptr, nullptr); // hooks
NULL, NULL, NULL); // hooks

DefineCustomBoolVariable(
"pg_stat_ch.use_otel",
Expand All @@ -96,131 +91,131 @@ void PschInitGuc(void) {
false,
PGC_POSTMASTER,
0,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomStringVariable(
"pg_stat_ch.clickhouse_host",
"ClickHouse server hostname.",
nullptr,
NULL,
&psch_clickhouse_host,
"localhost",
PGC_POSTMASTER,
0,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomIntVariable(
"pg_stat_ch.clickhouse_port",
"ClickHouse server native protocol port.",
nullptr,
NULL,
&psch_clickhouse_port,
9000, // bootValue
1, 65535, // min, max
PGC_POSTMASTER,
0,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomStringVariable(
"pg_stat_ch.clickhouse_user",
"ClickHouse user name.",
nullptr,
NULL,
&psch_clickhouse_user,
"default",
PGC_POSTMASTER,
0,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomStringVariable(
"pg_stat_ch.clickhouse_password",
"ClickHouse user password.",
nullptr,
NULL,
&psch_clickhouse_password,
"",
PGC_POSTMASTER,
GUC_SUPERUSER_ONLY,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomStringVariable(
"pg_stat_ch.clickhouse_database",
"ClickHouse database name for telemetry storage.",
nullptr,
NULL,
&psch_clickhouse_database,
"pg_stat_ch",
PGC_POSTMASTER,
0,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomBoolVariable(
"pg_stat_ch.clickhouse_use_tls",
"Enable TLS for ClickHouse connections.",
nullptr,
NULL,
&psch_clickhouse_use_tls,
false,
PGC_POSTMASTER,
0,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomBoolVariable(
"pg_stat_ch.clickhouse_skip_tls_verify",
"Skip TLS certificate verification (insecure, for testing only).",
nullptr,
NULL,
&psch_clickhouse_skip_tls_verify,
false,
PGC_POSTMASTER,
0,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomStringVariable(
"pg_stat_ch.otel_endpoint",
"OpenTelemetry gRPC endpoint (host:port).",
nullptr,
NULL,
&psch_otel_endpoint,
"localhost:4317",
PGC_POSTMASTER,
0,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomStringVariable(
"pg_stat_ch.hostname",
"Override the hostname of the current machine.",
nullptr,
NULL,
&psch_hostname,
"",
PGC_POSTMASTER,
0,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomIntVariable(
"pg_stat_ch.queue_capacity",
"Maximum number of events in the shared memory queue (must be a power of 2).",
nullptr,
NULL,
&psch_queue_capacity,
131072, // bootValue
1024, 4194304, // min, max
PGC_POSTMASTER,
0,
check_psch_queue_capacity, nullptr, nullptr);
check_psch_queue_capacity, NULL, NULL);

DefineCustomIntVariable(
"pg_stat_ch.flush_interval_ms",
"Interval in milliseconds between ClickHouse export batches.",
nullptr,
NULL,
&psch_flush_interval_ms,
200, // bootValue
100, 60000, // min, max
PGC_SIGHUP,
GUC_UNIT_MS,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomIntVariable(
"pg_stat_ch.batch_max",
"Maximum number of events per ClickHouse insert batch.",
nullptr,
NULL,
&psch_batch_max,
200000, // bootValue
1, 1000000, // min, max
PGC_SIGHUP,
0,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomIntVariable(
"pg_stat_ch.otel_log_queue_size",
Expand All @@ -232,7 +227,7 @@ void PschInitGuc(void) {
512, 1048576, // min, max
PGC_POSTMASTER,
0,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomIntVariable(
"pg_stat_ch.otel_log_batch_size",
Expand All @@ -244,7 +239,7 @@ void PschInitGuc(void) {
1, 131072, // min, max
PGC_POSTMASTER,
0,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomIntVariable(
"pg_stat_ch.otel_log_max_bytes",
Expand All @@ -257,7 +252,7 @@ void PschInitGuc(void) {
65536, 64 * 1024 * 1024, // min: 64 KiB, max: 64 MiB
PGC_POSTMASTER,
GUC_UNIT_BYTE,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomIntVariable(
"pg_stat_ch.otel_log_delay_ms",
Expand All @@ -269,7 +264,7 @@ void PschInitGuc(void) {
10, 60000, // min, max
PGC_POSTMASTER,
GUC_UNIT_MS,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomIntVariable(
"pg_stat_ch.otel_metric_interval_ms",
Expand All @@ -281,7 +276,7 @@ void PschInitGuc(void) {
100, 300000, // min, max (100ms to 5min)
PGC_POSTMASTER,
GUC_UNIT_MS,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);

DefineCustomEnumVariable(
"pg_stat_ch.log_min_elevel",
Expand All @@ -290,10 +285,10 @@ void PschInitGuc(void) {
"'error' for errors only, or 'debug5' for all messages.",
&psch_log_min_elevel,
WARNING,
log_elevel_options.data(),
log_elevel_options,
PGC_SUSET,
0,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);
DefineCustomBoolVariable(
"pg_stat_ch.debug_force_locked_overflow",
"Force HandleOverflow in locked path (debug/test only).",
Expand All @@ -303,10 +298,8 @@ void PschInitGuc(void) {
false,
PGC_SUSET,
0,
nullptr, nullptr, nullptr);
NULL, NULL, NULL);
// clang-format on

EmitWarningsOnPlaceholders("pg_stat_ch");
}

} // extern "C"
2 changes: 2 additions & 0 deletions src/export/stats_exporter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ void ExportEventStats(const std::vector<PschEvent>& events, StatsExporter* expor
auto col_username = exporter->DbUserColumn();
auto col_pid = exporter->RecordInt32("pid");
auto col_query_id = exporter->RecordInt64("query_id");
auto col_parent_query_id = exporter->RecordUInt64("parent_query_id");
auto col_cmd_type = exporter->DbOperationColumn();
auto col_rows = exporter->MetricUInt64("rows");
auto col_query = exporter->DbQueryTextColumn();
Expand Down Expand Up @@ -148,6 +149,7 @@ void ExportEventStats(const std::vector<PschEvent>& events, StatsExporter* expor
col_username->Append(std::string(ev.username, ev.username_len));
col_pid->Append(ev.pid);
col_query_id->Append(static_cast<int64_t>(ev.queryid));
col_parent_query_id->Append(static_cast<int64_t>(ev.parent_query_id));
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parent_query_id is exported as a UInt64 column, but it's being cast to int64_t before appending. This will corrupt values when parent_query_id exceeds INT64_MAX. Append as uint64_t (or pass ev.parent_query_id directly) to preserve the full range.

Suggested change
col_parent_query_id->Append(static_cast<int64_t>(ev.parent_query_id));
col_parent_query_id->Append(ev.parent_query_id);

Copilot uses AI. Check for mistakes.
col_cmd_type->Append(CmdTypeToString(ev.cmd_type));
col_rows->Append(ev.rows);

Expand Down
Loading
Loading