Repository navigation
[client-v2, jdbc-v2] Remove sensitive and repetitive logging - #3300
LjungeldTheHelper wants to merge 1 commit into
Conversation
…ckHouse#3295) Logs of client-v2 and jdbc-v2 no longer carry SQL, bound values, credentials, query IDs, client metadata, endpoint addresses or exception messages at any level, and no longer repeat per retry, request stream, statement or metadata object. - client-v2: one WARN per operation that recovers after retries instead of one per attempt plus one before the throw; endpoint quarantine and the all-endpoints-down fallback logged once; transport log-and-rethrow removed; User-Agent warning reports the length only; per-stream compressor DEBUG removed; swallowed failures logged by type. - jdbc-v2: SQL TRACE and JavaCC DEBUG of the SQL and parser error removed; per-statement query ID, metadata argument and per-object logs removed or aggregated; log-and-throw duplicates removed. - Tests: canaries at TRACE for both modules, retry log volume. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| result.put(key, value); | ||
| } catch (NumberFormatException e) { | ||
| LOG.debug("Skipping summary field '" + key + "' with non-numeric value '" + valueStr + "'", e); | ||
| LOG.debug("Skipping summary field '{}' with a non-numeric value", key); |
There was a problem hiding this comment.
move to trace level. This minor problem.
|
|
| return str == null || str.trim().isEmpty(); | ||
| } | ||
|
|
||
| public static void quietClose(Closeable closeable, Logger log) { |
There was a problem hiding this comment.
make this final to indicate that closeable never change
| } | ||
| } catch (Exception e) { | ||
| LOG.error("Failed to get local host name or address", e); | ||
| LOG.warn("Failed to get local host name or address: {}", ClientUtils.rootCauseClassName(e)); |
There was a problem hiding this comment.
do not do like this. We need to have stacktrace for exception like before. Just exception class name is useless. keep log warn but also preserve exception stack in log - here is should be very short
| throw e; | ||
| } catch (Exception e) { | ||
| LOG.warn("Failed to read error message (queryId = " + queryId + ")", e); | ||
| LOG.warn("Failed to read the server error message (HTTP status {}): {}", httpCode, |
There was a problem hiding this comment.
again - we should have stacktrace here. exception here might not have any sensitive information .
| throw e; | ||
| } | ||
| logIncompleteBodyOnClose(getQueryId(), e); | ||
| logIncompleteBodyOnClose(e); |
There was a problem hiding this comment.
why did you removed queryId ? this should present - may be we need to extend logic to grab it from different places, but it should present.
When queryId is missing - we should place something like unknown
| public InputStream createDataInputStream() { | ||
| try { | ||
| InputStream stream = new ResponseBodyInputStream(delegate.getEntity().getContent(), getQueryId()); | ||
| InputStream stream = new ResponseBodyInputStream(delegate.getEntity().getContent()); |
There was a problem hiding this comment.
queryId should be present here
| private final String queryId; | ||
|
|
||
| ResponseBodyInputStream(InputStream in, String queryId) { | ||
| ResponseBodyInputStream(InputStream in) { |
There was a problem hiding this comment.
queryId should be present - do not try to drop it - it is not sensitive information
| super.close(); | ||
| } catch (ConnectionClosedException e) { | ||
| logIncompleteBodyOnClose(queryId, e); | ||
| logIncompleteBodyOnClose(e); |
There was a problem hiding this comment.
queryId should be present in log - this is whole feature
| } catch (ConnectException | NoRouteToHostException e) { | ||
| LOG.debug("Failed to connect to '{}': {}", req.getAuthority(), e.getMessage()); | ||
| throw e; | ||
| } catch (Exception e) { |
There was a problem hiding this comment.
return removed catch branches - they are for purpose.
| } | ||
| final Header exceptionCodeHeader = httpResponse.getFirstHeader(ClickHouseHttpProto.HEADER_EXCEPTION_CODE); | ||
| LOG.warn("Server returned error response: status={}, queryId='{}', authority='{}', {}={}", | ||
| LOG.warn("Server returned error response: status={}, {}={}", |
There was a problem hiding this comment.
queryId and authority should be present
| if (stmt.isInsert()) { | ||
| LOG.debug("Assuming a function in the values list of an insert into {} that could not be parsed without errors", | ||
| stmt.getTable()); | ||
| LOG.debug("Assuming a function in the values list of an insert that could not be parsed without errors"); |
There was a problem hiding this comment.
Table should be present. log should be trace
| ClickHouseDataType dt = c.getDataType(); | ||
| type = JdbcUtils.convertToSqlType(dt); | ||
| } catch (Exception e) { | ||
| log.debug("Failed to convert column data type to SQL type: " + typeName, e); |
There was a problem hiding this comment.
make it trace - do not remove.
| // Don't log the raw SQL (may carry sensitive literals); the cause is enough to diagnose. | ||
| // Only the type of the failure: the SQL and the server message (which can echo it) may carry | ||
| // sensitive literals. | ||
| LOG.warn("Failed to resolve result-set metadata for prepared statement; " |
There was a problem hiding this comment.
make it debug or even trace. But preserve exception stack trace
| try { | ||
| reader.close(); | ||
| } catch (Exception re) { | ||
| log.debug("Error closing reader", re); |
There was a problem hiding this comment.
actually, leave it - it is not frequent error but if happens we need to know enough context to fix.
| currentResultSet.close(); | ||
| } catch (Exception e) { | ||
| LOG.error("Failed to close previous result set", e); | ||
| LOG.debug("Failed to close previous result set: {}", ClientUtils.rootCauseClassName(e)); |
There was a problem hiding this comment.
preserve exception stacktrace.
| writtenRows = response.getWrittenRows(); | ||
| } catch (Exception e) { | ||
| LOG.debug("Failed to read written-rows count from response; defaulting to 0", e); | ||
| LOG.debug("Failed to read written-rows count from response; defaulting to 0: {}", |
There was a problem hiding this comment.
just ignore exception - no need to get root cause here
| reader.close(); | ||
| } catch (Exception closeEx) { | ||
| LOG.warn("Failed to close reader when schema is null", closeEx); | ||
| LOG.debug("Failed to close reader when schema is null: {}", |
There was a problem hiding this comment.
here we need to know stacktrace
| response.close(); | ||
| } catch (Exception ex) { | ||
| LOG.warn("Failed to close response after exception", ex); | ||
| LOG.debug("Failed to close response after exception: {}", ClientUtils.rootCauseClassName(ex)); |
There was a problem hiding this comment.
here we need to know stacktrace, too
| resultSet.close(); | ||
| } catch (Exception e) { | ||
| LOG.error("Failed to close result set", e); | ||
| LOG.debug("Failed to close result set: {}", ClientUtils.rootCauseClassName(e)); |
| ClientConfigProperties.ASYNC_OPERATIONS.getDefaultValue())); | ||
| } catch (Exception e) { | ||
| LOG.error("Failed to read client configuration " + ClientConfigProperties.ASYNC_OPERATIONS.getKey(), e); | ||
| LOG.warn("Failed to read client configuration {}", ClientConfigProperties.ASYNC_OPERATIONS.getKey(), e); |
| log.debug("Failed to parse SQL: '" + sql + "'", e); | ||
| // Neither the SQL nor the parser error is logged at any level: the error quotes the statement, | ||
| // which can carry credentials or other sensitive literals. | ||
| log.warn("Failed to parse SQL. If you believe the SQL is valid, please feel free to open an issue on GitHub with this warning and the statement, with any sensitive values removed."); |
| * Logs one message per metadata call, with the count only: the names of the objects are not logged, and a call | ||
| * over many objects does not log one message per object. | ||
| */ | ||
| private static void logSkipped(int skipped, String objects) { |
Summary
Part of #3295, limited to
client-v2andjdbc-v2. The logs of the client and the driver no longer carryapplication data at any level (
TRACEincluded) and no longer repeat per retry, request stream, statement, ormetadata object. The other modules (
clickhouse-data, including theNonBlockingPipedOutputStreamexample from theissue, and the V1 client/HTTP/JDBC, R2DBC, and so on) are not changed here and are left for a follow-up.
All 73 logging calls in
client-v2/src/mainandjdbc-v2/src/main(including the JavaCC grammar) were classified.The changes:
Sensitive data removed
jdbc-v2StatementImpl: bothTRACElogs of the complete SQL (the source comment already said they exposepasswords).
jdbc-v2JavaCC parser: theDEBUGlog of the SQL and of the parser exception, which quotes the statement. Thegeneric
WARNand the fallback stay; theWARNno longer says "Enable DEBUG logging for details".jdbc-v2ANTLR facade: the syntax-error message (it quotes tokens) and the table name are no longer logged; lineand position stay.
client-v2WorkloadIdentitySupport: the long User-AgentWARNreports the length, not the value (client nameand metadata).
client-v2ProcessParser: the server value and the exception are no longer logged.client-v2HttpAPIClientHelper: query IDs and the authority are no longer logged; a failure to read the servererror is logged by exception type (the message can echo the body).
jdbc-v2prepared-statement metadata fallback: logs the exception type instead of the exception, whose servermessage echoes the SQL with its literals.
jdbc-v2getTablesDEBUGof its arguments,cancel()query IDs,Query IDper statement.Duplicates and repetition removed
WARNper attempt and noWARNbefore the final exception. An operation that succeeds after retrieslogs one
WARN:Query succeeded after 2 retries; last retried failure: java.net.ConnectException. A failedoperation logs nothing extra; its exception reaches the caller. Metrics and spans still record every attempt.
ClientNodeSelector:Endpoint <host> quarantinedonly when an alive endpoint becomes quarantined, not for eachfailure that extends the quarantine;
All endpoints are non-responsiveonce per outage instead of once perrequest.
DEBUGlog-and-rethrow inexecuteRequest(it logged authority and exception message), theBinaryStreamReaderlog-and-throw, thePreparedStatementImplandDatabaseMetaDataImplERROR-and-throw, andthe
ResultSetImpl.close()logs of exceptions that it throws.DEBUGin the LZ4 stream constructors; per-endpointDEBUGat build.ShowStatementsMetaData: oneDEBUGwith the count of skipped tables/databases per call instead of one perobject; per-column type-parse
DEBUGin metadata rows removed.Severity
DEBUGwith the exception type (ClientUtils.quietCloselogs the closeable'stype, not its
toString()).ERRORonly where the failure cannot reach the caller (driver (de)registration, connection abort on networktimeout). Metrics registration, executor shutdown, local host name, and async-config read failures are
WARN.DEBUG(a read-only or write-only POJO is normal).ClientUtils.rootCauseClassName(Throwable)(internal package) names a failure without its message.Compatibility
No public API, configuration key or default, protocol, serialization, retry, timeout, parsing, result, or JDBC
behavior changes. Exceptions and their messages are unchanged. The only observable difference is fewer and safer log
messages; code that parses the removed messages (for example the per-attempt
... Retrying.WARN) will no longersee them. The no-HTTP-wire-logging behavior and its tests (#3252) are preserved and extended.
Tests
HttpAPIClientHelperTest.testNoWireOrSensitiveLoggingAtTrace: everyclient-v2logger atTRACE, wire logger atDEBUG, pooled/unpooled, HTTP/HTTPS, Basic/password header/Bearer, failover from a dead endpoint, success and aserver error that echoes the SQL. No password, Basic/Bearer token, header value, SQL, body, error body, URL
setting value, client name, client metadata, or query ID in the log; the server error still reaches the caller.
ClientFailoverUnitTest.testRetryLoggedOncePerOperation: recovered → exactly one summary naming the failure type;failed → none; one quarantine
WARN; no query ID or endpoint address.HttpAPIClientHelperTest: the User-AgentWARNhas the length, not the value; the unknown-statusWARNhasneither the query ID nor the authority.
StatementLoggingTest(WireMock, no server user created):Statementwith a password literal andPreparedStatementwith a bound value, JavaCC/ANTLR4/ANTLR4_PARAMS_PARSER, success and server error, driver andclient loggers at
TRACE: neither the SQL nor the value is logged.ClickHouseSqlParserLoggingTest: atTRACE, no SQL token or parser detail; the fallback and theWARNstay.DatabaseMetaDataTest.testMetadataCallsLogNoPatterns(was asserting thegetTablesarguments were logged),
JDBCErrorHandlingTests.testPreparedStatementMetadataFallbackLogsNoSql.ClientUtilsTest:quietCloseatDEBUGwith the type;rootCauseClassNamedata provider.The new tests fail against
main(checked by reverting the main sources: 5/5 injdbc-v2, 10 inclient-v2).Run locally (JDK 17, JDK 8 toolchain):
mvn -pl client-v2 -am test: client-v2 830 tests, 0 failuresmvn -pl jdbc-v2 test(after-am install -DskipTests): 1979 tests, 0 failures.mvn -pl jdbc-v2 -am testfailsin the reactor with
module-info.java: module not foundonmainas well, so dependencies were installed first.DatabaseMetaDataTest#testMetadataCallsLogNoPatterns+testGetColumns*,JDBCErrorHandlingTests: 13 tests, 0 failuresclient-v2main sources compile with-Dj8(release 8).Checklist (
docs/changes_checklist.md, Logging)ERRORonly where the caller cannot see the failure,WARNfor degraded states,DEBUGfor bounded diagnostics. ✔deliberate exception to "attach the throwable": the message is the leak.)
Checklist
history/latest/3295.md)docs/features.mdupdated; user documentation in the follow-up docs PR🤖 Generated with Claude Code
Note
Low Risk
Runtime behavior and thrown exceptions are unchanged; the main impact is observability—monitoring or tooling that parsed removed log formats will need updating.
Overview
client-v2 and jdbc-v2 logging is tightened so logs do not carry SQL, credentials, query IDs, full exception messages, endpoint addresses (except host on quarantine), User-Agent values, or other application data—even at
TRACE.Retry and failover behavior in logs: Per-attempt
WARNmessages with query IDs and endpoints are removed. Successful operations after retries emit a single summary (logRecoveredRetries) with retry count and root exception type via newClientUtils.rootCauseClassName. Endpoint quarantine and “all endpoints down” warnings are deduplicated inClientNodeSelector.JDBC: Statement SQL
TRACE, parser SQL/DEBUGdetail, metadata argument logging, and similar paths are removed or reduced to generic or type-only messages. Cleanup and close failures move toDEBUGwith exception types; several formerERRORlogs on paths that already throw are dropped.Docs/tests:
docs/features.md,history/latest/3295.md, and broad WireMock/TRACE logging tests document and enforce the policy. Public API, retries, metrics, and spans are unchanged aside from log output.Reviewed by Cursor Bugbot for commit d13beae. Bugbot is set up for automated code reviews on this repo. Configure here.