diff --git a/.changeset/gate-clickhouse-query-debug-log.md b/.changeset/gate-clickhouse-query-debug-log.md new file mode 100644 index 0000000000..a0751f17df --- /dev/null +++ b/.changeset/gate-clickhouse-query-debug-log.md @@ -0,0 +1,25 @@ +--- +'@hyperdx/common-utils': patch +'@hyperdx/api': patch +'@hyperdx/app': patch +--- + +fix: route per-query SQL debug logging through an injectable logger (#2416) + +`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 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 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. 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 new file mode 100644 index 0000000000..f9bb00910c --- /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({ ...options, customLogger: options.customLogger ?? customLogger }); + } +} diff --git a/packages/api/src/controllers/ai.ts b/packages/api/src/controllers/ai.ts index 1d8d287234..b0c136fe41 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, @@ -16,6 +15,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 8134c9ea94..8331187354 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 12416750c1..97b0156da9 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, @@ -19,6 +18,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 a0753e708a..1da06eb6d0 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 { diff --git a/packages/api/src/mcp/tools/sources/describeMetric.ts b/packages/api/src/mcp/tools/sources/describeMetric.ts index db1b01d14b..d42bc23f68 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 9d97029424..2bbcc5d15d 100644 --- a/packages/api/src/mcp/tools/sources/describeSource.ts +++ b/packages/api/src/mcp/tools/sources/describeSource.ts @@ -6,12 +6,12 @@ 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 SqlString from 'sqlstring'; 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 bd5704e494..63714fa32c 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 fd449be1d6..a0c6fc9e57 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 cd69af011c..979a406f73 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 a0edd9c45f..e3831b4e95 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 a4c3832d87..552fdb14be 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 4075b09560..1d4e6d6c94 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 074e50c521..19704dc87c 100644 --- a/packages/api/src/tasks/usageStats.ts +++ b/packages/api/src/tasks/usageStats.ts @@ -1,11 +1,11 @@ import { ResponseJSON } from '@hyperdx/common-utils/dist/clickhouse'; -import { ClickhouseClient } from '@hyperdx/common-utils/dist/clickhouse/node'; import { MetricsDataType, SourceKind } from '@hyperdx/common-utils/dist/types'; import * as HyperDX from '@hyperdx/node-opentelemetry'; 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'; diff --git a/packages/api/src/utils/logger.ts b/packages/api/src/utils/logger.ts index b21fab54c8..162005d667 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, { diff --git a/packages/cli/src/api/client.ts b/packages/cli/src/api/client.ts index 93c7023dbb..d519d40a33 100644 --- a/packages/cli/src/api/client.ts +++ b/packages/cli/src/api/client.ts @@ -351,9 +351,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__/browser.test.ts b/packages/common-utils/src/clickhouse/__tests__/browser.test.ts new file mode 100644 index 0000000000..eab74e43e6 --- /dev/null +++ b/packages/common-utils/src/clickhouse/__tests__/browser.test.ts @@ -0,0 +1,65 @@ +import { consoleLogger } from '@/clickhouse/browser'; + +describe('consoleLogger', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('pretty-prints SQL into a single multi-line message', () => { + const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}); + const sql = 'SELECT a, b FROM default.otel_logs WHERE a = 1'; + + consoleLogger.debug({ + module: 'clickhouse', + message: 'Sending query', + args: { sql }, + }); + + // 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', () => { + 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/__tests__/index.test.ts b/packages/common-utils/src/clickhouse/__tests__/index.test.ts index 538208d327..02f5030fb8 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([ @@ -147,3 +148,60 @@ describe('convertCHDataTypeToJSType', () => { expect(convertCHDataTypeToJSType('Variant()')).toBeNull(); }); }); + +// logQuery is protected on the base class; this subclass exposes it so the +// tests can drive it without reaching through the client's public query path. +class TestClickhouseClient extends ClickhouseClient { + logQuery(query: string, query_params: Record = {}): void { + super.logQuery(query, query_params); + } +} + +describe('BaseClickhouseClient.logQuery', () => { + const makeLogger = () => ({ + trace: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('stays silent when no customLogger is configured', () => { + const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}); + const client = new TestClickhouseClient({ host: 'http://localhost' }); + client.logQuery('SELECT 1 FROM system.one'); + expect(debugSpy).not.toHaveBeenCalled(); + }); + + it('logs through the customLogger passed to the client', () => { + const customLogger = makeLogger(); + const client = new TestClickhouseClient({ + host: 'http://localhost', + customLogger, + }); + client.logQuery('SELECT 1 FROM system.one'); + expect(customLogger.debug).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 TestClickhouseClient({ + host: 'http://localhost', + customLogger, + }); + client.logQuery('SELECT {id:Int32}', { id: 5 }); + expect(customLogger.debug).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 169b994db6..fc107f0725 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, @@ -6,12 +7,48 @@ 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 => { + const { sql, ...rest } = args ?? {}; + + if (typeof sql === 'string') { + write(`${message}:\n${tryFormatSql(sql)}`); + return; + } + + write( + `[${module}] ${message}`, + ...Object.values(rest), + ...(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, @@ -70,7 +107,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({ ...options, customLogger: options.customLogger ?? consoleLogger }); } // This subclass always builds a web client, so narrow the base class's @@ -129,7 +167,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 fc8694b424..9df6177583 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, @@ -29,6 +30,7 @@ export type { BaseResultSet, ClickHouseSettings, DataFormat, + Logger, ResponseJSON, Row, }; @@ -396,6 +398,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 { @@ -412,6 +416,7 @@ export abstract class BaseClickhouseClient { */ protected maxRowReadOnly: boolean; protected requestTimeout: number = 3600000; + protected readonly customLogger?: Logger; constructor({ host, @@ -420,6 +425,7 @@ export abstract class BaseClickhouseClient { queryTimeout, application, requestTimeout, + customLogger, }: ClickhouseClientOptions) { this.host = host!; this.username = username; @@ -427,6 +433,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; } @@ -454,10 +461,12 @@ export abstract class BaseClickhouseClient { await this.client?.close(); } - protected logDebugQuery( + protected logQuery( query: string, query_params: Record = {}, ): void { + if (!this.customLogger) return; + let debugSql = ''; try { debugSql = parameterizedQueryToSql({ sql: query, params: query_params }); @@ -465,11 +474,11 @@ export abstract class BaseClickhouseClient { debugSql = query; } - console.debug('--------------------------------------------------------'); - - console.debug('Sending Query:', debugSql); - - console.debug('--------------------------------------------------------'); + this.customLogger.debug({ + 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 37c510525c..4ab917e65f 100644 --- a/packages/common-utils/src/clickhouse/node.ts +++ b/packages/common-utils/src/clickhouse/node.ts @@ -47,7 +47,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