From 2f4c60a519fac95319db24b235999017e38d3096 Mon Sep 17 00:00:00 2001 From: truehazker <40111175+truehazker@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:22:58 +0400 Subject: [PATCH 1/4] fix(common-utils): make ClickHouse query debug logging opt-in BaseClickhouseClient.logDebugQuery dumped the full SQL of every ClickHouse query to the console unconditionally, outside the pino logger, so HYPERDX_LOG_LEVEL could not suppress it and API pod logs filled with per-query spam. Query logging is now off by default and enabled only with HYPERDX_LOG_QUERIES=true. The gate is one line in the shared base-class method every query path routes through, reusing the existing isNode helper so browser bundles stay silent. docker-compose passes the variable through to the all-in-one app container, and .env documents the toggle next to HYPERDX_LOG_LEVEL. Closes #2416 --- .changeset/gate-clickhouse-query-debug-log.md | 10 ++++++ .env | 2 ++ docker-compose.yml | 1 + .../src/clickhouse/__tests__/index.test.ts | 35 +++++++++++++++++++ packages/common-utils/src/clickhouse/index.ts | 3 ++ 5 files changed, 51 insertions(+) create mode 100644 .changeset/gate-clickhouse-query-debug-log.md diff --git a/.changeset/gate-clickhouse-query-debug-log.md b/.changeset/gate-clickhouse-query-debug-log.md new file mode 100644 index 0000000000..b05e435072 --- /dev/null +++ b/.changeset/gate-clickhouse-query-debug-log.md @@ -0,0 +1,10 @@ +--- +"@hyperdx/common-utils": patch +--- + +fix: make per-query SQL debug logging opt-in via HYPERDX_LOG_QUERIES (#2416) + +`BaseClickhouseClient.logDebugQuery` dumped raw SQL to the console on every +ClickHouse query, unconditionally and outside the pino logger, flooding API +logs with query spam. It is now off by default; set `HYPERDX_LOG_QUERIES=true` +to enable it. diff --git a/.env b/.env index 79a6f58e47..34264edd1b 100644 --- a/.env +++ b/.env @@ -25,6 +25,8 @@ HYPERDX_API_PORT=${HYPERDX_API_PORT:-8000} HYPERDX_APP_PORT=${HYPERDX_APP_PORT:-8080} HYPERDX_APP_URL=http://localhost HYPERDX_LOG_LEVEL=debug +# Set to 'true' to dump every ClickHouse query's SQL to the API logs +# HYPERDX_LOG_QUERIES=true HYPERDX_OPAMP_PORT=${HYPERDX_OPAMP_PORT:-4320} HYPERDX_BASE_PATH= diff --git a/docker-compose.yml b/docker-compose.yml index abaa35aa7f..16c794f0bb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,6 +59,7 @@ services: HYPERDX_APP_PORT: ${HYPERDX_APP_PORT} HYPERDX_APP_URL: ${HYPERDX_APP_URL} HYPERDX_LOG_LEVEL: ${HYPERDX_LOG_LEVEL} + HYPERDX_LOG_QUERIES: ${HYPERDX_LOG_QUERIES} MONGO_URI: 'mongodb://db:27017/hyperdx' SERVER_URL: http://127.0.0.1:${HYPERDX_API_PORT} OPAMP_PORT: ${HYPERDX_OPAMP_PORT} diff --git a/packages/common-utils/src/clickhouse/__tests__/index.test.ts b/packages/common-utils/src/clickhouse/__tests__/index.test.ts index a8bab44109..23fa3b5d85 100644 --- a/packages/common-utils/src/clickhouse/__tests__/index.test.ts +++ b/packages/common-utils/src/clickhouse/__tests__/index.test.ts @@ -5,6 +5,7 @@ import { isMissingColumnError, JSDataType, } from '@/clickhouse'; +import { ClickhouseClient } from '@/clickhouse/node'; describe('isMissingColumnError', () => { it.each([ @@ -124,3 +125,37 @@ describe('convertCHDataTypeToJSType', () => { expect(convertCHDataTypeToJSType('Nullable(Bool)')).toBe(JSDataType.Bool); }); }); + +describe('BaseClickhouseClient.logDebugQuery', () => { + const client = new ClickhouseClient({ host: 'http://localhost' }); + const logDebugQuery = (query: string) => (client as any).logDebugQuery(query); + let debugSpy: jest.SpyInstance; + + beforeEach(() => { + debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + delete process.env.HYPERDX_LOG_QUERIES; + }); + + it('logs the query when HYPERDX_LOG_QUERIES=true', () => { + process.env.HYPERDX_LOG_QUERIES = 'true'; + logDebugQuery('SELECT 1 FROM system.one'); + expect(debugSpy).toHaveBeenCalledWith( + 'Sending Query:', + 'SELECT 1 FROM system.one', + ); + }); + + it('stays silent otherwise', () => { + delete process.env.HYPERDX_LOG_QUERIES; + logDebugQuery('SELECT 1 FROM system.one'); + for (const value of ['', 'false', '1', 'TRUE']) { + process.env.HYPERDX_LOG_QUERIES = value; + logDebugQuery('SELECT 1 FROM system.one'); + } + expect(debugSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/common-utils/src/clickhouse/index.ts b/packages/common-utils/src/clickhouse/index.ts index ffbf89e445..a99d0e3a79 100644 --- a/packages/common-utils/src/clickhouse/index.ts +++ b/packages/common-utils/src/clickhouse/index.ts @@ -21,6 +21,7 @@ import { import { extractSettingsClauseFromEnd, hashCode, + isNode, replaceJsonExpressions, splitAndTrimWithBracket, } from '@/core/utils'; @@ -683,6 +684,8 @@ export abstract class BaseClickhouseClient { query: string, query_params: Record = {}, ): void { + if (!isNode || process.env.HYPERDX_LOG_QUERIES !== 'true') return; + let debugSql = ''; try { debugSql = parameterizedQueryToSql({ sql: query, params: query_params }); From b31d3237d8f3fd7340c6318ddec15e2a2af577a8 Mon Sep 17 00:00:00 2001 From: truehazker <40111175+truehazker@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:58:40 +0400 Subject: [PATCH 2/4] fix(docker): default HYPERDX_LOG_QUERIES to empty in compose passthrough The passthrough referenced ${HYPERDX_LOG_QUERIES} while .env only ships the variable commented out, so every `docker compose up` printed a "variable is not set" warning. Use the :- default-empty form; the gate still requires an explicit 'true', so behavior is unchanged. --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 16c794f0bb..c43c65c0f3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: HYPERDX_APP_PORT: ${HYPERDX_APP_PORT} HYPERDX_APP_URL: ${HYPERDX_APP_URL} HYPERDX_LOG_LEVEL: ${HYPERDX_LOG_LEVEL} - HYPERDX_LOG_QUERIES: ${HYPERDX_LOG_QUERIES} + HYPERDX_LOG_QUERIES: ${HYPERDX_LOG_QUERIES:-} MONGO_URI: 'mongodb://db:27017/hyperdx' SERVER_URL: http://127.0.0.1:${HYPERDX_API_PORT} OPAMP_PORT: ${HYPERDX_OPAMP_PORT} From 5e0766d9a7ef7296ab94982621b9772749f668f5 Mon Sep 17 00:00:00 2001 From: truehazker <40111175+truehazker@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:58:40 +0400 Subject: [PATCH 3/4] test(common-utils): split query-log silence test into it.each cases The disabled-state test asserted five logDebugQuery calls behind a single expect, so a failure would not name the offending value. Each non-'true' value ('', 'false', '1', 'TRUE') and the unset case now get their own test, matching the it.each idiom already used in this file. --- .../src/clickhouse/__tests__/index.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/common-utils/src/clickhouse/__tests__/index.test.ts b/packages/common-utils/src/clickhouse/__tests__/index.test.ts index 23fa3b5d85..ca3a8445bd 100644 --- a/packages/common-utils/src/clickhouse/__tests__/index.test.ts +++ b/packages/common-utils/src/clickhouse/__tests__/index.test.ts @@ -149,13 +149,18 @@ describe('BaseClickhouseClient.logDebugQuery', () => { ); }); - it('stays silent otherwise', () => { + it('stays silent when unset', () => { delete process.env.HYPERDX_LOG_QUERIES; logDebugQuery('SELECT 1 FROM system.one'); - for (const value of ['', 'false', '1', 'TRUE']) { - process.env.HYPERDX_LOG_QUERIES = value; - logDebugQuery('SELECT 1 FROM system.one'); - } expect(debugSpy).not.toHaveBeenCalled(); }); + + it.each(['', 'false', '1', 'TRUE'])( + 'stays silent when set to %j', + value => { + process.env.HYPERDX_LOG_QUERIES = value; + logDebugQuery('SELECT 1 FROM system.one'); + expect(debugSpy).not.toHaveBeenCalled(); + }, + ); }); From 28da011a8a478b9c6e57f173e38a21022bb3d190 Mon Sep 17 00:00:00 2001 From: truehazker <40111175+truehazker@users.noreply.github.com> Date: Tue, 21 Jul 2026 01:22:05 +0400 Subject: [PATCH 4/4] fix(common-utils): replace HYPERDX_LOG_QUERIES env gate with injectable customLogger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework of the query-log gate per review feedback on #2679: instead of a dedicated env var, `ClickhouseClientOptions` now accepts an optional `customLogger` (the `Logger` interface from `@clickhouse/client-common`), and `logQuery` (was `logDebugQuery`) emits through it at `trace` — silent whenever no logger is injected. - The app injects the re-exported `DefaultLogger` in dev and local mode, where the devtools console is the only place to see query SQL; prod stays silent so consoleCapture can't ship SQL to telemetry. - The API intentionally injects nothing: silent by default fixes the log flooding, and a pino adapter can be passed at any client later (trace-level emit keeps HYPERDX_LOG_LEVEL=debug deployments quiet). - The CLI's silencing override is now dead code and removed. - HYPERDX_LOG_QUERIES is gone from .env and docker-compose. --- .changeset/gate-clickhouse-query-debug-log.md | 17 ++++- .env | 2 - docker-compose.yml | 1 - packages/app/src/clickhouse.ts | 9 ++- packages/cli/src/api/client.ts | 3 - .../src/clickhouse/__tests__/index.test.ts | 66 ++++++++++++------- .../common-utils/src/clickhouse/browser.ts | 2 +- packages/common-utils/src/clickhouse/index.ts | 23 ++++--- packages/common-utils/src/clickhouse/node.ts | 2 +- 9 files changed, 82 insertions(+), 43 deletions(-) diff --git a/.changeset/gate-clickhouse-query-debug-log.md b/.changeset/gate-clickhouse-query-debug-log.md index b05e435072..50d2183654 100644 --- a/.changeset/gate-clickhouse-query-debug-log.md +++ b/.changeset/gate-clickhouse-query-debug-log.md @@ -1,10 +1,21 @@ --- "@hyperdx/common-utils": patch +"@hyperdx/api": patch +"@hyperdx/app": patch --- -fix: make per-query SQL debug logging opt-in via HYPERDX_LOG_QUERIES (#2416) +fix: route per-query SQL debug logging through an injectable logger (#2416) `BaseClickhouseClient.logDebugQuery` dumped raw SQL to the console on every ClickHouse query, unconditionally and outside the pino logger, flooding API -logs with query spam. It is now off by default; set `HYPERDX_LOG_QUERIES=true` -to enable it. +logs (and, via the browser SDK's consoleCapture, telemetry) with query spam. + +Query logging is now silent by default and routed through an optional +per-client `customLogger` on `ClickhouseClientOptions` (the `Logger` interface +from `@clickhouse/client-common`). The app enables it in dev builds and in +local mode, where queries hit ClickHouse directly and the devtools console is +the only place to see them; anywhere else, pass a `customLogger` at the client +you're debugging. + +`@hyperdx/api` is bumped without source changes: it bundles common-utils, so +its released images stop emitting the per-query dump. diff --git a/.env b/.env index 34264edd1b..79a6f58e47 100644 --- a/.env +++ b/.env @@ -25,8 +25,6 @@ HYPERDX_API_PORT=${HYPERDX_API_PORT:-8000} HYPERDX_APP_PORT=${HYPERDX_APP_PORT:-8080} HYPERDX_APP_URL=http://localhost HYPERDX_LOG_LEVEL=debug -# Set to 'true' to dump every ClickHouse query's SQL to the API logs -# HYPERDX_LOG_QUERIES=true HYPERDX_OPAMP_PORT=${HYPERDX_OPAMP_PORT:-4320} HYPERDX_BASE_PATH= diff --git a/docker-compose.yml b/docker-compose.yml index 16c794f0bb..abaa35aa7f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,6 @@ services: HYPERDX_APP_PORT: ${HYPERDX_APP_PORT} HYPERDX_APP_URL: ${HYPERDX_APP_URL} HYPERDX_LOG_LEVEL: ${HYPERDX_LOG_LEVEL} - HYPERDX_LOG_QUERIES: ${HYPERDX_LOG_QUERIES} MONGO_URI: 'mongodb://db:27017/hyperdx' SERVER_URL: http://127.0.0.1:${HYPERDX_API_PORT} OPAMP_PORT: ${HYPERDX_OPAMP_PORT} diff --git a/packages/app/src/clickhouse.ts b/packages/app/src/clickhouse.ts index 6d5def56d8..2301943660 100644 --- a/packages/app/src/clickhouse.ts +++ b/packages/app/src/clickhouse.ts @@ -9,12 +9,13 @@ import { chSql, ClickhouseClientOptions, ColumnMeta, + DefaultLogger, ResponseJSON, } from '@hyperdx/common-utils/dist/clickhouse'; import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/browser'; import { useQuery, UseQueryOptions } from '@tanstack/react-query'; -import { IS_LOCAL_MODE } from '@/config'; +import { IS_DEV, IS_LOCAL_MODE } from '@/config'; import { getLocalConnections } from '@/connection'; import api from './api'; @@ -22,9 +23,15 @@ import { DEFAULT_QUERY_TIMEOUT } from './defaults'; const PROXY_CLICKHOUSE_HOST = '/api/clickhouse-proxy'; +const queryLogger = IS_DEV || IS_LOCAL_MODE ? new DefaultLogger() : undefined; + export const getClickhouseClient = ( options: ClickhouseClientOptions = {}, ): ClickhouseClient => { + options = { + customLogger: queryLogger, + ...options, + }; if (IS_LOCAL_MODE) { const localConnections = getLocalConnections(); if (localConnections.length === 0) { diff --git a/packages/cli/src/api/client.ts b/packages/cli/src/api/client.ts index 2c57cbc99b..972c948d0e 100644 --- a/packages/cli/src/api/client.ts +++ b/packages/cli/src/api/client.ts @@ -343,9 +343,6 @@ export class ProxyClickhouseClient extends BaseClickhouseClient { }); } - // Silence the "Sending Query: ..." debug output from BaseClickhouseClient - protected override logDebugQuery(): void {} - // This subclass always builds a node client, so narrow the base class's // platform-agnostic client type to the node-specific one. protected getClient(): NodeClickHouseClient { diff --git a/packages/common-utils/src/clickhouse/__tests__/index.test.ts b/packages/common-utils/src/clickhouse/__tests__/index.test.ts index 23fa3b5d85..eace5cf9c2 100644 --- a/packages/common-utils/src/clickhouse/__tests__/index.test.ts +++ b/packages/common-utils/src/clickhouse/__tests__/index.test.ts @@ -126,36 +126,56 @@ describe('convertCHDataTypeToJSType', () => { }); }); -describe('BaseClickhouseClient.logDebugQuery', () => { - const client = new ClickhouseClient({ host: 'http://localhost' }); - const logDebugQuery = (query: string) => (client as any).logDebugQuery(query); - let debugSpy: jest.SpyInstance; - - beforeEach(() => { - debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}); +describe('BaseClickhouseClient.logQuery', () => { + const logQuery = ( + client: ClickhouseClient, + query: string, + params?: Record, + ) => (client as any).logQuery(query, params); + const makeLogger = () => ({ + trace: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), }); afterEach(() => { jest.restoreAllMocks(); - delete process.env.HYPERDX_LOG_QUERIES; }); - it('logs the query when HYPERDX_LOG_QUERIES=true', () => { - process.env.HYPERDX_LOG_QUERIES = 'true'; - logDebugQuery('SELECT 1 FROM system.one'); - expect(debugSpy).toHaveBeenCalledWith( - 'Sending Query:', - 'SELECT 1 FROM system.one', - ); + it('stays silent when no customLogger is configured', () => { + const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}); + const client = new ClickhouseClient({ host: 'http://localhost' }); + logQuery(client, 'SELECT 1 FROM system.one'); + expect(debugSpy).not.toHaveBeenCalled(); }); - it('stays silent otherwise', () => { - delete process.env.HYPERDX_LOG_QUERIES; - logDebugQuery('SELECT 1 FROM system.one'); - for (const value of ['', 'false', '1', 'TRUE']) { - process.env.HYPERDX_LOG_QUERIES = value; - logDebugQuery('SELECT 1 FROM system.one'); - } - expect(debugSpy).not.toHaveBeenCalled(); + it('logs through the customLogger passed to the client', () => { + const customLogger = makeLogger(); + const client = new ClickhouseClient({ + host: 'http://localhost', + customLogger, + }); + logQuery(client, 'SELECT 1 FROM system.one'); + expect(customLogger.trace).toHaveBeenCalledWith({ + module: 'clickhouse', + message: 'Sending query', + args: { sql: 'SELECT 1 FROM system.one' }, + }); + }); + + it('interpolates query_params into the logged SQL', () => { + const customLogger = makeLogger(); + const client = new ClickhouseClient({ + host: 'http://localhost', + customLogger, + }); + logQuery(client, 'SELECT {id:Int32}', { id: 5 }); + expect(customLogger.trace).toHaveBeenCalledWith({ + module: 'clickhouse', + message: 'Sending query', + args: { sql: 'SELECT 5' }, + }); }); }); diff --git a/packages/common-utils/src/clickhouse/browser.ts b/packages/common-utils/src/clickhouse/browser.ts index 7e52cec40c..27c69ac8e5 100644 --- a/packages/common-utils/src/clickhouse/browser.ts +++ b/packages/common-utils/src/clickhouse/browser.ts @@ -128,7 +128,7 @@ export class ClickhouseClient extends BaseClickhouseClient { this.client = this.buildClient(); } - this.logDebugQuery(query, query_params); + this.logQuery(query, query_params); let clickhouseSettings: ClickHouseSettings | undefined; // If this is the settings query, we must not process the clickhouse settings, or else we will infinitely recurse diff --git a/packages/common-utils/src/clickhouse/index.ts b/packages/common-utils/src/clickhouse/index.ts index a99d0e3a79..f6866b5f2c 100644 --- a/packages/common-utils/src/clickhouse/index.ts +++ b/packages/common-utils/src/clickhouse/index.ts @@ -3,6 +3,7 @@ import type { BaseResultSet, ClickHouseSettings, DataFormat, + Logger, ResponseHeaders, ResponseJSON, Row, @@ -21,7 +22,6 @@ import { import { extractSettingsClauseFromEnd, hashCode, - isNode, replaceJsonExpressions, splitAndTrimWithBracket, } from '@/core/utils'; @@ -33,9 +33,11 @@ export type { BaseResultSet, ClickHouseSettings, DataFormat, + Logger, ResponseJSON, Row, }; +export { DefaultLogger } from '@clickhouse/client-common'; export enum JSDataType { Array = 'array', @@ -631,6 +633,8 @@ export type ClickhouseClientOptions = { application?: string; /** Defines how long the client will wait for a response from the ClickHouse server before aborting the request, in milliseconds */ requestTimeout?: number; + /** Logger for per-query SQL debug output. When omitted, query logging is silent. */ + customLogger?: Logger; }; export abstract class BaseClickhouseClient { @@ -647,6 +651,7 @@ export abstract class BaseClickhouseClient { */ protected maxRowReadOnly: boolean; protected requestTimeout: number = 3600000; + protected readonly customLogger?: Logger; constructor({ host, @@ -655,6 +660,7 @@ export abstract class BaseClickhouseClient { queryTimeout, application, requestTimeout, + customLogger, }: ClickhouseClientOptions) { this.host = host!; this.username = username; @@ -662,6 +668,7 @@ export abstract class BaseClickhouseClient { this.queryTimeout = queryTimeout; this.maxRowReadOnly = false; this.application = application; + this.customLogger = customLogger; if (requestTimeout != null && requestTimeout >= 0) { this.requestTimeout = requestTimeout; } @@ -680,11 +687,11 @@ export abstract class BaseClickhouseClient { await this.client?.close(); } - protected logDebugQuery( + protected logQuery( query: string, query_params: Record = {}, ): void { - if (!isNode || process.env.HYPERDX_LOG_QUERIES !== 'true') return; + if (!this.customLogger) return; let debugSql = ''; try { @@ -693,11 +700,11 @@ export abstract class BaseClickhouseClient { debugSql = query; } - console.debug('--------------------------------------------------------'); - - console.debug('Sending Query:', debugSql); - - console.debug('--------------------------------------------------------'); + this.customLogger.trace({ + module: 'clickhouse', + message: 'Sending query', + args: { sql: debugSql }, + }); } protected async processClickhouseSettings({ diff --git a/packages/common-utils/src/clickhouse/node.ts b/packages/common-utils/src/clickhouse/node.ts index a43e0911b8..41951c098e 100644 --- a/packages/common-utils/src/clickhouse/node.ts +++ b/packages/common-utils/src/clickhouse/node.ts @@ -46,7 +46,7 @@ export class ClickhouseClient extends BaseClickhouseClient { queryId, shouldSkipApplySettings, }: QueryInputs): Promise> { - this.logDebugQuery(query, query_params); + this.logQuery(query, query_params); let clickhouseSettings: ClickHouseSettings | undefined; // If this is the settings query, we must not process the clickhouse settings, or else we will infinitely recurse