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 01/10] 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 02/10] 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 03/10] 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 04/10] 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 From 968b3c612b18a4df6a3af8aed9e96f52294bb974 Mon Sep 17 00:00:00 2001 From: truehazker <40111175+truehazker@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:18:25 +0400 Subject: [PATCH 05/10] fix(common-utils): :loud_sound: log SQL queries at debug level instead of trace The maintainer asked for query logging at the debug level so it follows the existing HYPERDX_LOG_LEVEL behavior (enabled with HYPERDX_LOG_LEVEL=debug, silent above that). Switch logQuery from customLogger.trace to customLogger.debug and update the unit tests. --- packages/common-utils/src/clickhouse/__tests__/index.test.ts | 4 ++-- packages/common-utils/src/clickhouse/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/common-utils/src/clickhouse/__tests__/index.test.ts b/packages/common-utils/src/clickhouse/__tests__/index.test.ts index eace5cf9c2..8dca0df748 100644 --- a/packages/common-utils/src/clickhouse/__tests__/index.test.ts +++ b/packages/common-utils/src/clickhouse/__tests__/index.test.ts @@ -158,7 +158,7 @@ describe('BaseClickhouseClient.logQuery', () => { customLogger, }); logQuery(client, 'SELECT 1 FROM system.one'); - expect(customLogger.trace).toHaveBeenCalledWith({ + expect(customLogger.debug).toHaveBeenCalledWith({ module: 'clickhouse', message: 'Sending query', args: { sql: 'SELECT 1 FROM system.one' }, @@ -172,7 +172,7 @@ describe('BaseClickhouseClient.logQuery', () => { customLogger, }); logQuery(client, 'SELECT {id:Int32}', { id: 5 }); - expect(customLogger.trace).toHaveBeenCalledWith({ + expect(customLogger.debug).toHaveBeenCalledWith({ module: 'clickhouse', message: 'Sending query', args: { sql: 'SELECT 5' }, diff --git a/packages/common-utils/src/clickhouse/index.ts b/packages/common-utils/src/clickhouse/index.ts index f6866b5f2c..fef175dda9 100644 --- a/packages/common-utils/src/clickhouse/index.ts +++ b/packages/common-utils/src/clickhouse/index.ts @@ -700,7 +700,7 @@ export abstract class BaseClickhouseClient { debugSql = query; } - this.customLogger.trace({ + this.customLogger.debug({ module: 'clickhouse', message: 'Sending query', args: { sql: debugSql }, From b9b523f3b548de9b927786c834e7e2bafd6e346f Mon Sep 17 00:00:00 2001 From: truehazker <40111175+truehazker@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:18:51 +0400 Subject: [PATCH 06/10] fix(api): :loud_sound: inject pino-backed logger into ClickHouse clients Add a thin ClickhouseClient subclass (src/clickhouse.ts) that adapts the @clickhouse/client-common Logger interface to the API's pino logger and injects it by default, and switch every API construction site to import it. SQL query logging now flows through pino at debug level and follows HYPERDX_LOG_LEVEL, instead of being silent (or, before this PR, raw console.debug spam that no env var could suppress). --- packages/api/src/clickhouse.ts | 26 +++++++++++++++++++ packages/api/src/controllers/ai.ts | 2 +- packages/api/src/mcp/tools/query/denoise.ts | 2 +- .../api/src/mcp/tools/query/eventDeltas.ts | 2 +- packages/api/src/mcp/tools/query/helpers.ts | 2 +- .../src/mcp/tools/query/runEventPatterns.ts | 2 +- .../src/mcp/tools/sources/describeMetric.ts | 2 +- .../src/mcp/tools/sources/describeSource.ts | 2 +- .../api/src/mcp/tools/sources/listMetrics.ts | 2 +- packages/api/src/mcp/tools/trace/breakdown.ts | 2 +- packages/api/src/mcp/tools/trace/waterfall.ts | 2 +- packages/api/src/routers/api/prometheus.ts | 2 +- .../api/src/routers/external-api/v2/charts.ts | 2 +- .../routers/external-api/v2/utils/search.ts | 2 +- .../tasks/checkAlerts/providers/default.ts | 2 +- packages/api/src/tasks/usageStats.ts | 2 +- 16 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 packages/api/src/clickhouse.ts diff --git a/packages/api/src/clickhouse.ts b/packages/api/src/clickhouse.ts new file mode 100644 index 0000000000..5c55f87a57 --- /dev/null +++ b/packages/api/src/clickhouse.ts @@ -0,0 +1,26 @@ +import type { + ClickhouseClientOptions, + Logger as ClickhouseLogger, +} from '@hyperdx/common-utils/dist/clickhouse'; +import { ClickhouseClient as BaseClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; + +import logger from '@/utils/logger'; + +const customLogger: ClickhouseLogger = { + trace: ({ module, message, args }) => + logger.trace({ ...args, module }, message), + debug: ({ module, message, args }) => + logger.debug({ ...args, module }, message), + info: ({ module, message, args }) => + logger.info({ ...args, module }, message), + warn: ({ module, message, args, err }) => + logger.warn({ ...args, module, err }, message), + error: ({ module, message, args, err }) => + logger.error({ ...args, module, err }, message), +}; + +export class ClickhouseClient extends BaseClickhouseClient { + constructor(options: ClickhouseClientOptions) { + super({ customLogger, ...options }); + } +} diff --git a/packages/api/src/controllers/ai.ts b/packages/api/src/controllers/ai.ts index 48f14b97da..52d64f10f5 100644 --- a/packages/api/src/controllers/ai.ts +++ b/packages/api/src/controllers/ai.ts @@ -1,6 +1,5 @@ import { createAnthropic } from '@ai-sdk/anthropic'; import { createOpenAI } from '@ai-sdk/openai'; -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { getMetadata, TableMetadata, @@ -17,6 +16,7 @@ import * as chrono from 'chrono-node'; import ms from 'ms'; import z from 'zod'; +import { ClickhouseClient } from '@/clickhouse'; import * as config from '@/config'; import { ISource } from '@/models/source'; import { parseJSON } from '@/utils/common'; diff --git a/packages/api/src/mcp/tools/query/denoise.ts b/packages/api/src/mcp/tools/query/denoise.ts index 00d5f1bff2..a3eeb0dba4 100644 --- a/packages/api/src/mcp/tools/query/denoise.ts +++ b/packages/api/src/mcp/tools/query/denoise.ts @@ -1,4 +1,3 @@ -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { getMetadata } from '@hyperdx/common-utils/dist/core/metadata'; import { getFirstTimestampValueExpression } from '@hyperdx/common-utils/dist/core/utils'; import { @@ -12,6 +11,7 @@ import { import type { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; import { DisplayType } from '@hyperdx/common-utils/dist/types'; +import { ClickhouseClient } from '@/clickhouse'; import { getConnectionById } from '@/controllers/connection'; import { getSource } from '@/controllers/sources'; diff --git a/packages/api/src/mcp/tools/query/eventDeltas.ts b/packages/api/src/mcp/tools/query/eventDeltas.ts index 6976ab42a4..df4b9a8ff7 100644 --- a/packages/api/src/mcp/tools/query/eventDeltas.ts +++ b/packages/api/src/mcp/tools/query/eventDeltas.ts @@ -1,4 +1,3 @@ -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { getStableSampleExpression, rankProperties, @@ -12,6 +11,7 @@ import { } from '@hyperdx/common-utils/dist/types'; import { z } from 'zod'; +import { ClickhouseClient } from '@/clickhouse'; import { getConnectionById } from '@/controllers/connection'; import { getSource } from '@/controllers/sources'; import type { ToolRegistrar } from '@/mcp/tools/types'; diff --git a/packages/api/src/mcp/tools/query/helpers.ts b/packages/api/src/mcp/tools/query/helpers.ts index 5b77103a17..bef758270e 100644 --- a/packages/api/src/mcp/tools/query/helpers.ts +++ b/packages/api/src/mcp/tools/query/helpers.ts @@ -1,5 +1,4 @@ import { ClickHouseError } from '@clickhouse/client-common'; -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { getMetadata } from '@hyperdx/common-utils/dist/core/metadata'; import { convertToCategoricalChartConfig, @@ -22,6 +21,7 @@ import { import { ObjectId } from 'mongodb'; import ms from 'ms'; +import { ClickhouseClient } from '@/clickhouse'; import { getConnectionById } from '@/controllers/connection'; import { getSource } from '@/controllers/sources'; import type { McpErrorResult } from '@/mcp/utils/errors'; diff --git a/packages/api/src/mcp/tools/query/runEventPatterns.ts b/packages/api/src/mcp/tools/query/runEventPatterns.ts index 3f22ee7736..74cbc487b2 100644 --- a/packages/api/src/mcp/tools/query/runEventPatterns.ts +++ b/packages/api/src/mcp/tools/query/runEventPatterns.ts @@ -1,4 +1,3 @@ -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { getMetadata } from '@hyperdx/common-utils/dist/core/metadata'; import { getFirstTimestampValueExpression, @@ -8,6 +7,7 @@ import { minePatterns } from '@hyperdx/common-utils/dist/drain'; import type { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; import { DisplayType } from '@hyperdx/common-utils/dist/types'; +import { ClickhouseClient } from '@/clickhouse'; import { getConnectionById } from '@/controllers/connection'; import { getSource } from '@/controllers/sources'; import { mcpServerError, mcpUserError } from '@/mcp/utils/errors'; diff --git a/packages/api/src/mcp/tools/sources/describeMetric.ts b/packages/api/src/mcp/tools/sources/describeMetric.ts index 9d63433a81..65141ff8c0 100644 --- a/packages/api/src/mcp/tools/sources/describeMetric.ts +++ b/packages/api/src/mcp/tools/sources/describeMetric.ts @@ -6,11 +6,11 @@ import { JSDataType, tableExpr, } from '@hyperdx/common-utils/dist/clickhouse'; -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { getMetadata } from '@hyperdx/common-utils/dist/core/metadata'; import { SourceKind } from '@hyperdx/common-utils/dist/types'; import { z } from 'zod'; +import { ClickhouseClient } from '@/clickhouse'; import { getConnectionById } from '@/controllers/connection'; import { getSource } from '@/controllers/sources'; import { clickHouseErrorResult } from '@/mcp/tools/query/helpers'; diff --git a/packages/api/src/mcp/tools/sources/describeSource.ts b/packages/api/src/mcp/tools/sources/describeSource.ts index 453d2b9efd..26f9b74e82 100644 --- a/packages/api/src/mcp/tools/sources/describeSource.ts +++ b/packages/api/src/mcp/tools/sources/describeSource.ts @@ -6,11 +6,11 @@ import { JSDataType, tableExpr, } from '@hyperdx/common-utils/dist/clickhouse'; -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { getMetadata } from '@hyperdx/common-utils/dist/core/metadata'; import { type MetricTable, SourceKind } from '@hyperdx/common-utils/dist/types'; import { z } from 'zod'; +import { ClickhouseClient } from '@/clickhouse'; import { getConnectionById } from '@/controllers/connection'; import { getSource } from '@/controllers/sources'; import type { ToolRegistrar } from '@/mcp/tools/types'; diff --git a/packages/api/src/mcp/tools/sources/listMetrics.ts b/packages/api/src/mcp/tools/sources/listMetrics.ts index 6b7854d844..d95a8432a8 100644 --- a/packages/api/src/mcp/tools/sources/listMetrics.ts +++ b/packages/api/src/mcp/tools/sources/listMetrics.ts @@ -3,11 +3,11 @@ import { concatChSql, tableExpr, } from '@hyperdx/common-utils/dist/clickhouse'; -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { getMetadata } from '@hyperdx/common-utils/dist/core/metadata'; import { SourceKind } from '@hyperdx/common-utils/dist/types'; import { z } from 'zod'; +import { ClickhouseClient } from '@/clickhouse'; import { getConnectionById } from '@/controllers/connection'; import { getSource } from '@/controllers/sources'; import type { ToolRegistrar } from '@/mcp/tools/types'; diff --git a/packages/api/src/mcp/tools/trace/breakdown.ts b/packages/api/src/mcp/tools/trace/breakdown.ts index ac902b7b3d..d0dc95d652 100644 --- a/packages/api/src/mcp/tools/trace/breakdown.ts +++ b/packages/api/src/mcp/tools/trace/breakdown.ts @@ -15,10 +15,10 @@ * endpoint" without dropping into raw SQL with self-JOINs. The builder * tools (table / timeseries / search) can't express the TraceId subselect. */ -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { SourceKind } from '@hyperdx/common-utils/dist/types'; import { z } from 'zod'; +import { ClickhouseClient } from '@/clickhouse'; import { getConnectionById } from '@/controllers/connection'; import { getSource } from '@/controllers/sources'; import { diff --git a/packages/api/src/mcp/tools/trace/waterfall.ts b/packages/api/src/mcp/tools/trace/waterfall.ts index dc11af473f..4c8ae00e8d 100644 --- a/packages/api/src/mcp/tools/trace/waterfall.ts +++ b/packages/api/src/mcp/tools/trace/waterfall.ts @@ -1,4 +1,3 @@ -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { getMetadata } from '@hyperdx/common-utils/dist/core/metadata'; import { type BuilderChartConfigWithDateRange, @@ -8,6 +7,7 @@ import { } from '@hyperdx/common-utils/dist/types'; import { z } from 'zod'; +import { ClickhouseClient } from '@/clickhouse'; import { getConnectionById } from '@/controllers/connection'; import { getSource } from '@/controllers/sources'; import { diff --git a/packages/api/src/routers/api/prometheus.ts b/packages/api/src/routers/api/prometheus.ts index 07f446816f..742e071b51 100644 --- a/packages/api/src/routers/api/prometheus.ts +++ b/packages/api/src/routers/api/prometheus.ts @@ -1,9 +1,9 @@ -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import express from 'express'; import { performance } from 'perf_hooks'; import { Readable } from 'stream'; import { pipeline } from 'stream/promises'; +import { ClickhouseClient } from '@/clickhouse'; import { getConnectionById } from '@/controllers/connection'; import { getNonNullUserWithTeam } from '@/middleware/auth'; import { getCounter, getHistogram } from '@/utils/instrumentation'; diff --git a/packages/api/src/routers/external-api/v2/charts.ts b/packages/api/src/routers/external-api/v2/charts.ts index 447eb424b4..3dd5051cf3 100644 --- a/packages/api/src/routers/external-api/v2/charts.ts +++ b/packages/api/src/routers/external-api/v2/charts.ts @@ -1,4 +1,3 @@ -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { getMetadata } from '@hyperdx/common-utils/dist/core/metadata'; import { Granularity } from '@hyperdx/common-utils/dist/core/utils'; import { @@ -11,6 +10,7 @@ import express from 'express'; import _ from 'lodash'; import { z } from 'zod'; +import { ClickhouseClient } from '@/clickhouse'; import { getConnectionById } from '@/controllers/connection'; import { getSource } from '@/controllers/sources'; import { getTeam } from '@/controllers/team'; diff --git a/packages/api/src/routers/external-api/v2/utils/search.ts b/packages/api/src/routers/external-api/v2/utils/search.ts index 2fc750dd3d..78c8cc21b7 100644 --- a/packages/api/src/routers/external-api/v2/utils/search.ts +++ b/packages/api/src/routers/external-api/v2/utils/search.ts @@ -1,4 +1,3 @@ -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { getMetadata } from '@hyperdx/common-utils/dist/core/metadata'; import { buildSearchChartConfig } from '@hyperdx/common-utils/dist/core/searchChartConfig'; import { @@ -15,6 +14,7 @@ import { isTraceSource, } from '@hyperdx/common-utils/dist/types'; +import { ClickhouseClient } from '@/clickhouse'; import { getConnectionById } from '@/controllers/connection'; import { getSource } from '@/controllers/sources'; import type { ExternalDashboardSearchRequestConfig } from '@/utils/zod'; diff --git a/packages/api/src/tasks/checkAlerts/providers/default.ts b/packages/api/src/tasks/checkAlerts/providers/default.ts index 079cdcc89d..4b82e89108 100644 --- a/packages/api/src/tasks/checkAlerts/providers/default.ts +++ b/packages/api/src/tasks/checkAlerts/providers/default.ts @@ -1,5 +1,4 @@ import PQueue from '@esm2cjs/p-queue'; -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { displayTypeSupportsRawSqlAlerts } from '@hyperdx/common-utils/dist/core/utils'; import { isRawSqlSavedChartConfig } from '@hyperdx/common-utils/dist/guards'; import { Tile } from '@hyperdx/common-utils/dist/types'; @@ -7,6 +6,7 @@ import mongoose from 'mongoose'; import ms from 'ms'; import { URLSearchParams } from 'url'; +import { ClickhouseClient } from '@/clickhouse'; import * as config from '@/config'; import { ALERT_HISTORY_QUERY_CONCURRENCY } from '@/controllers/alertHistory'; import { LOCAL_APP_TEAM } from '@/controllers/team'; diff --git a/packages/api/src/tasks/usageStats.ts b/packages/api/src/tasks/usageStats.ts index f2ebbd5ae3..cf9ed87f37 100644 --- a/packages/api/src/tasks/usageStats.ts +++ b/packages/api/src/tasks/usageStats.ts @@ -1,5 +1,4 @@ import { ResponseJSON } from '@hyperdx/common-utils/dist/clickhouse'; -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { MetricsDataType, SourceKind, @@ -10,6 +9,7 @@ import ms from 'ms'; import os from 'os'; import pino from 'pino'; +import { ClickhouseClient } from '@/clickhouse'; import * as config from '@/config'; import Connection from '@/models/connection'; import { Source, SourceDocument } from '@/models/source'; From 41f6cc05331afcbf8f2404203206036e0196ff15 Mon Sep 17 00:00:00 2001 From: truehazker <40111175+truehazker@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:19:03 +0400 Subject: [PATCH 07/10] fix(api): :mute: default HYPERDX_LOG_LEVEL to info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prod image (entry.prod.sh) never sets HYPERDX_LOG_LEVEL, so the API ran pino at debug and would log every SQL query by default — defeating the point of routing query logging through the logger. Default to info so production is silent unless HYPERDX_LOG_LEVEL=debug is set explicitly. Use || rather than ?? because the compose files pass the variable through even when unset, injecting an empty string that pino rejects as a level. Dev and CI env files pin their levels explicitly and are unaffected. --- packages/api/src/utils/logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/utils/logger.ts b/packages/api/src/utils/logger.ts index 26ef73dd56..c5177c849c 100644 --- a/packages/api/src/utils/logger.ts +++ b/packages/api/src/utils/logger.ts @@ -8,7 +8,7 @@ import pinoHttp from 'pino-http'; import * as config from '@/config'; -const MAX_LEVEL = config.HYPERDX_LOG_LEVEL ?? 'debug'; +const MAX_LEVEL = config.HYPERDX_LOG_LEVEL || 'info'; const hyperdxTransport = config.HYPERDX_API_KEY ? getPinoTransport(MAX_LEVEL, { From c2804b6d7edf2f6eb3a74b9b06bf8bebb7c8db19 Mon Sep 17 00:00:00 2001 From: truehazker <40111175+truehazker@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:19:17 +0400 Subject: [PATCH 08/10] fix(common-utils): :loud_sound: log browser SQL queries to devtools by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default the browser ClickhouseClient's customLogger to the @clickhouse/client DefaultLogger in its constructor, so query SQL stays inspectable in devtools in all builds — including paths that bypass the app's getClickhouseClient (onboarding auto-connect, testLocalConnection) and external consumers of the published browser client. Drop the app's per-call-site logger wiring, which is now redundant. Update the changeset to describe the final behavior. --- .changeset/gate-clickhouse-query-debug-log.md | 28 +++++++++---------- packages/app/src/clickhouse.ts | 9 +----- .../common-utils/src/clickhouse/browser.ts | 4 ++- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/.changeset/gate-clickhouse-query-debug-log.md b/.changeset/gate-clickhouse-query-debug-log.md index 50d2183654..2b02c35152 100644 --- a/.changeset/gate-clickhouse-query-debug-log.md +++ b/.changeset/gate-clickhouse-query-debug-log.md @@ -1,21 +1,21 @@ --- -"@hyperdx/common-utils": patch -"@hyperdx/api": patch -"@hyperdx/app": patch +'@hyperdx/common-utils': patch +'@hyperdx/api': patch +'@hyperdx/app': patch --- 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 (and, via the browser SDK's consoleCapture, telemetry) with query spam. +`BaseClickhouseClient` dumped raw SQL to the console on every ClickHouse query, +unconditionally and outside the pino logger, flooding API logs 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. +Query logging now goes through an optional per-client `customLogger` on +`ClickhouseClientOptions`, logged at `debug`, and is silent when no logger is +passed. The API injects a pino-backed logger, so query logging follows the +existing `HYPERDX_LOG_LEVEL` setting instead of writing to `console.debug`. The +browser client defaults to a console logger, so query SQL stays visible in +devtools in all builds. -`@hyperdx/api` is bumped without source changes: it bundles common-utils, so -its released images stop emitting the per-query dump. +The API's log level now defaults to `info` (was `debug`), so SQL logging is +silent in production unless `HYPERDX_LOG_LEVEL=debug` is set. Dev and CI env +files already pin their levels explicitly and are unaffected. diff --git a/packages/app/src/clickhouse.ts b/packages/app/src/clickhouse.ts index 2301943660..6d5def56d8 100644 --- a/packages/app/src/clickhouse.ts +++ b/packages/app/src/clickhouse.ts @@ -9,13 +9,12 @@ 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_DEV, IS_LOCAL_MODE } from '@/config'; +import { IS_LOCAL_MODE } from '@/config'; import { getLocalConnections } from '@/connection'; import api from './api'; @@ -23,15 +22,9 @@ 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/common-utils/src/clickhouse/browser.ts b/packages/common-utils/src/clickhouse/browser.ts index 27c69ac8e5..73ebafb2c7 100644 --- a/packages/common-utils/src/clickhouse/browser.ts +++ b/packages/common-utils/src/clickhouse/browser.ts @@ -9,6 +9,7 @@ import { createClient } from '@clickhouse/client-web'; import { BaseClickhouseClient, ClickhouseClientOptions, + DefaultLogger, QueryInputs, } from './index'; @@ -70,7 +71,8 @@ export const testLocalConnection = async ({ export class ClickhouseClient extends BaseClickhouseClient { constructor(options: ClickhouseClientOptions) { - super(options); + // Log queries to devtools by default so SQL stays inspectable in all builds + super({ customLogger: new DefaultLogger(), ...options }); } // This subclass always builds a web client, so narrow the base class's From b9cf2ed05d731c6ba2dc65ebc6ba38000904a924 Mon Sep 17 00:00:00 2001 From: truehazker <40111175+truehazker@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:21:19 +0400 Subject: [PATCH 09/10] fix(common-utils): :loud_sound: print browser SQL logs as readable text The browser ClickHouse client used the upstream `DefaultLogger`, which serializes log args through `JSON.stringify`. That turned multi-line SQL into a single escaped string with literal `\n` sequences, making queries hard to read in devtools. Replace it with a local `consoleLogger` that passes args to the console verbatim, so multi-line SQL renders as-is and errors are appended as real Error objects. The now-unused `DefaultLogger` re-export is dropped from the clickhouse index. Co-Authored-By: Skrrt Bot --- .changeset/gate-clickhouse-query-debug-log.md | 4 +-- .../src/clickhouse/__tests__/browser.test.ts | 33 +++++++++++++++++++ .../common-utils/src/clickhouse/browser.ts | 22 +++++++++++-- packages/common-utils/src/clickhouse/index.ts | 1 - 4 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 packages/common-utils/src/clickhouse/__tests__/browser.test.ts diff --git a/.changeset/gate-clickhouse-query-debug-log.md b/.changeset/gate-clickhouse-query-debug-log.md index 2b02c35152..1c2d3d7ef6 100644 --- a/.changeset/gate-clickhouse-query-debug-log.md +++ b/.changeset/gate-clickhouse-query-debug-log.md @@ -13,8 +13,8 @@ Query logging now goes through an optional per-client `customLogger` on `ClickhouseClientOptions`, logged at `debug`, and is silent when no logger is passed. The API injects a pino-backed logger, so query logging follows the existing `HYPERDX_LOG_LEVEL` setting instead of writing to `console.debug`. The -browser client defaults to a console logger, so query SQL stays visible in -devtools in all builds. +browser client defaults to a console logger that prints SQL as readable +multi-line text, so query SQL stays visible in devtools in all builds. The API's log level now defaults to `info` (was `debug`), so SQL logging is silent in production unless `HYPERDX_LOG_LEVEL=debug` is set. Dev and CI env diff --git a/packages/common-utils/src/clickhouse/__tests__/browser.test.ts b/packages/common-utils/src/clickhouse/__tests__/browser.test.ts new file mode 100644 index 0000000000..023016862d --- /dev/null +++ b/packages/common-utils/src/clickhouse/__tests__/browser.test.ts @@ -0,0 +1,33 @@ +import { consoleLogger } from '@/clickhouse/browser'; + +describe('consoleLogger', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('prints string args as raw text so multi-line SQL stays readable', () => { + const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}); + const sql = 'SELECT 1\nFROM system.one'; + + consoleLogger.debug({ + module: 'clickhouse', + message: 'Sending query', + args: { sql, rows: 5 }, + }); + + expect(debugSpy).toHaveBeenCalledWith('[clickhouse] Sending query', sql, 5); + }); + + it('appends the error to error logs', () => { + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const err = new Error('boom'); + + consoleLogger.error({ + module: 'clickhouse', + message: 'Query failed', + err, + }); + + expect(errorSpy).toHaveBeenCalledWith('[clickhouse] Query failed', err); + }); +}); diff --git a/packages/common-utils/src/clickhouse/browser.ts b/packages/common-utils/src/clickhouse/browser.ts index eb9c00a389..da9b3ae1e8 100644 --- a/packages/common-utils/src/clickhouse/browser.ts +++ b/packages/common-utils/src/clickhouse/browser.ts @@ -1,3 +1,4 @@ +import type { Logger, WarnLogParams } from '@clickhouse/client-common'; import type { BaseResultSet, ClickHouseClient as WebClickHouseClient, @@ -9,10 +10,27 @@ import { createClient } from '@clickhouse/client-web'; import { BaseClickhouseClient, ClickhouseClientOptions, - DefaultLogger, QueryInputs, } from './index'; +const writeLog = ( + write: (...data: unknown[]) => void, + { module, message, args, err }: WarnLogParams, +): void => + write( + `[${module}] ${message}`, + ...Object.values(args ?? {}), + ...(err ? [err] : []), + ); + +export const consoleLogger: Logger = { + trace: params => writeLog(console.debug, params), + debug: params => writeLog(console.debug, params), + info: params => writeLog(console.info, params), + warn: params => writeLog(console.warn, params), + error: params => writeLog(console.error, params), +}; + const localModeFetch: typeof fetch = ( input: RequestInfo | URL, init?: RequestInit, @@ -72,7 +90,7 @@ export const testLocalConnection = async ({ export class ClickhouseClient extends BaseClickhouseClient { constructor(options: ClickhouseClientOptions) { // Log queries to devtools by default so SQL stays inspectable in all builds - super({ customLogger: new DefaultLogger(), ...options }); + super({ customLogger: consoleLogger, ...options }); } // This subclass always builds a web client, so narrow the base class's diff --git a/packages/common-utils/src/clickhouse/index.ts b/packages/common-utils/src/clickhouse/index.ts index 21e45cea30..9420ee1ea5 100644 --- a/packages/common-utils/src/clickhouse/index.ts +++ b/packages/common-utils/src/clickhouse/index.ts @@ -38,7 +38,6 @@ export type { ResponseJSON, Row, }; -export { DefaultLogger } from '@clickhouse/client-common'; export enum JSDataType { Array = 'array', From 38bd4e592fafbfdf425db9f120601c06bd626cbc Mon Sep 17 00:00:00 2001 From: truehazker <40111175+truehazker@users.noreply.github.com> Date: Fri, 21 Aug 2026 01:04:57 +0400 Subject: [PATCH 10/10] fix: :loud_sound: pretty-print ClickHouse query logs in devtools Review feedback on #2679: the browser console printed each query as `[clickhouse] Sending query` followed by the raw SQL as a trailing argument, which devtools wrapped into a single unreadable line. The browser sink now runs the SQL through the existing ClickHouse-dialect formatter in sqlFormatter.ts and emits it as one multi-line string, since devtools only renders a block when the whole message is a single argument. Unparseable SQL falls back to the raw text, and records that carry no SQL keep the `[module] message` form. Also flip the customLogger spread order in both client constructors: a caller passing an explicit `undefined` previously overwrote the default and silently disabled query logging, because a spread copies a key even when its value is undefined. --- .changeset/gate-clickhouse-query-debug-log.md | 10 +++-- packages/api/src/clickhouse.ts | 2 +- .../src/clickhouse/__tests__/browser.test.ts | 40 +++++++++++++++++-- .../common-utils/src/clickhouse/browser.ts | 24 +++++++++-- 4 files changed, 65 insertions(+), 11 deletions(-) diff --git a/.changeset/gate-clickhouse-query-debug-log.md b/.changeset/gate-clickhouse-query-debug-log.md index 1c2d3d7ef6..a0751f17df 100644 --- a/.changeset/gate-clickhouse-query-debug-log.md +++ b/.changeset/gate-clickhouse-query-debug-log.md @@ -13,9 +13,13 @@ Query logging now goes through an optional per-client `customLogger` on `ClickhouseClientOptions`, logged at `debug`, and is silent when no logger is passed. The API injects a pino-backed logger, so query logging follows the existing `HYPERDX_LOG_LEVEL` setting instead of writing to `console.debug`. The -browser client defaults to a console logger that prints SQL as readable -multi-line text, so query SQL stays visible in devtools in all builds. +browser client defaults to a console logger that pretty-prints the SQL as a +single multi-line block, so query SQL stays visible and readable in devtools in +all builds instead of wrapping into one long line. The API's log level now defaults to `info` (was `debug`), so SQL logging is silent in production unless `HYPERDX_LOG_LEVEL=debug` is set. Dev and CI env -files already pin their levels explicitly and are unaffected. +files already pin their levels explicitly and are unaffected. The default also +now applies when `HYPERDX_LOG_LEVEL` is set but empty — which is what Compose +passes when the variable is unset in the environment, and which previously made +pino throw at startup. diff --git a/packages/api/src/clickhouse.ts b/packages/api/src/clickhouse.ts index 5c55f87a57..f9bb00910c 100644 --- a/packages/api/src/clickhouse.ts +++ b/packages/api/src/clickhouse.ts @@ -21,6 +21,6 @@ const customLogger: ClickhouseLogger = { export class ClickhouseClient extends BaseClickhouseClient { constructor(options: ClickhouseClientOptions) { - super({ customLogger, ...options }); + super({ ...options, customLogger: options.customLogger ?? customLogger }); } } diff --git a/packages/common-utils/src/clickhouse/__tests__/browser.test.ts b/packages/common-utils/src/clickhouse/__tests__/browser.test.ts index 023016862d..eab74e43e6 100644 --- a/packages/common-utils/src/clickhouse/__tests__/browser.test.ts +++ b/packages/common-utils/src/clickhouse/__tests__/browser.test.ts @@ -5,17 +5,49 @@ describe('consoleLogger', () => { jest.restoreAllMocks(); }); - it('prints string args as raw text so multi-line SQL stays readable', () => { + it('pretty-prints SQL into a single multi-line message', () => { const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}); - const sql = 'SELECT 1\nFROM system.one'; + const sql = 'SELECT a, b FROM default.otel_logs WHERE a = 1'; consoleLogger.debug({ module: 'clickhouse', message: 'Sending query', - args: { sql, rows: 5 }, + args: { sql }, }); - expect(debugSpy).toHaveBeenCalledWith('[clickhouse] Sending query', sql, 5); + // Exact layout is sqlFormatter's business; here we only pin the contract + // devtools needs: one call, one string, and SQL broken across lines. + expect(debugSpy).toHaveBeenCalledTimes(1); + expect(debugSpy.mock.calls[0]).toHaveLength(1); + + const logged = String(debugSpy.mock.calls[0][0]); + expect(logged.startsWith('Sending query:\n')).toBe(true); + expect(logged.split('\n').length).toBeGreaterThan(2); + }); + + it('falls back to the raw SQL when it cannot be formatted', () => { + const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}); + const sql = 'SELECT ((( FROM'; + + consoleLogger.debug({ + module: 'clickhouse', + message: 'Sending query', + args: { sql }, + }); + + expect(debugSpy).toHaveBeenCalledWith(`Sending query:\n${sql}`); + }); + + it('keeps the prefixed form for logs that carry no SQL', () => { + const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}); + + consoleLogger.debug({ + module: 'clickhouse', + message: 'Response received', + args: { rows: 5 }, + }); + + expect(debugSpy).toHaveBeenCalledWith('[clickhouse] Response received', 5); }); it('appends the error to error logs', () => { diff --git a/packages/common-utils/src/clickhouse/browser.ts b/packages/common-utils/src/clickhouse/browser.ts index da9b3ae1e8..fc107f0725 100644 --- a/packages/common-utils/src/clickhouse/browser.ts +++ b/packages/common-utils/src/clickhouse/browser.ts @@ -7,21 +7,39 @@ import type { } from '@clickhouse/client-web'; import { createClient } from '@clickhouse/client-web'; +import { format } from '@/sqlFormatter'; + import { BaseClickhouseClient, ClickhouseClientOptions, QueryInputs, } from './index'; +const tryFormatSql = (sql: string): string => { + try { + return format(sql); + } catch { + return sql; + } +}; + const writeLog = ( write: (...data: unknown[]) => void, { module, message, args, err }: WarnLogParams, -): void => +): void => { + const { sql, ...rest } = args ?? {}; + + if (typeof sql === 'string') { + write(`${message}:\n${tryFormatSql(sql)}`); + return; + } + write( `[${module}] ${message}`, - ...Object.values(args ?? {}), + ...Object.values(rest), ...(err ? [err] : []), ); +}; export const consoleLogger: Logger = { trace: params => writeLog(console.debug, params), @@ -90,7 +108,7 @@ export const testLocalConnection = async ({ export class ClickhouseClient extends BaseClickhouseClient { constructor(options: ClickhouseClientOptions) { // Log queries to devtools by default so SQL stays inspectable in all builds - super({ customLogger: consoleLogger, ...options }); + super({ ...options, customLogger: options.customLogger ?? consoleLogger }); } // This subclass always builds a web client, so narrow the base class's