Skip to content

Commit 4599549

Browse files
committed
[SEA-NodeJS] Retarget kernel log level on runtime logger.setLevel()
Follow-up to the unified-logging wiring: a runtime `logger.setLevel(...)` now retargets the kernel-side bridge too, not just the driver's own transports — keeping kernel verbosity in lock-step with the driver's at runtime, not only at connect. - `IDBSQLLogger` gains an optional `onLevelChange(listener) => unsubscribe`; `DBSQLLogger` implements it (fires subscribers from `setLevel`, swallowing a throwing listener so it can't break level setting). - `installKernelLogBridge` now returns an unsubscribe handle and, when both the logger can notify (`onLevelChange`) and the binding can retarget (`setKernelLogLevel`), subscribes to forward level changes to the kernel. - `SeaBackend` stores the handle and drops it on `close()` (no stale listener; the process-global kernel sink follows the existing last-writer-wins model). Backend-agnostic: `DBSQLLogger` never references the SEA binding — the SEA layer subscribes via the interface. Loggers without `onLevelChange` keep the connect-time level; older bindings without `setKernelLogLevel` simply don't subscribe. Verified: tsc / eslint / prettier clean; unit suite 1183 passing incl. new `DBSQLLogger` level-subscription tests + `SeaLogging` runtime-retarget tests (retarget, unsubscribe, no-notify logger, no-retarget binding). Live: with the logger started at `warn`, a healthy SELECT logged 0 kernel lines; after `logger.setLevel('debug')` mid-session the next SELECT logged 12 `[kernel databricks::sql::kernel]` lines — proving the kernel was retargeted. Co-authored-by: Isaac Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
1 parent 26acafe commit 4599549

7 files changed

Lines changed: 220 additions & 9 deletions

File tree

lib/DBSQLLogger.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export default class DBSQLLogger implements IDBSQLLogger {
99
file?: winston.transports.FileTransportInstance;
1010
};
1111

12+
// Subscribers notified on `setLevel(...)` — used by the SEA/kernel backend to
13+
// keep the kernel-side log bridge's verbosity in lock-step with this logger.
14+
private levelListeners: Array<(level: LogLevel) => void> = [];
15+
1216
constructor({ level = LogLevel.info, filepath }: LoggerOptions = {}) {
1317
this.transports = {
1418
console: new winston.transports.Console({ handleExceptions: true, level }),
@@ -30,10 +34,28 @@ export default class DBSQLLogger implements IDBSQLLogger {
3034
return (this.transports.console.level as LogLevel) ?? LogLevel.info;
3135
}
3236

37+
onLevelChange(listener: (level: LogLevel) => void): () => void {
38+
this.levelListeners.push(listener);
39+
return () => {
40+
const index = this.levelListeners.indexOf(listener);
41+
if (index >= 0) {
42+
this.levelListeners.splice(index, 1);
43+
}
44+
};
45+
}
46+
3347
setLevel(level: LogLevel) {
3448
this.transports.console.level = level;
3549
if (this.transports.file) {
3650
this.transports.file.level = level;
3751
}
52+
for (const listener of this.levelListeners) {
53+
// A subscriber must never break level setting for the rest.
54+
try {
55+
listener(level);
56+
} catch {
57+
// swallow — level-change notification is advisory
58+
}
59+
}
3860
}
3961
}

lib/contracts/IDBSQLLogger.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ export default interface IDBSQLLogger {
1414
* bridge at its `info` default.
1515
*/
1616
getLevel?(): LogLevel;
17+
18+
/**
19+
* Optional: subscribe to runtime level changes. When implemented, the
20+
* SEA/kernel backend subscribes so a runtime `setLevel(...)` retargets the
21+
* kernel-side log bridge too (not just the driver's own transports) — keeping
22+
* kernel verbosity in lock-step with the driver's. Returns an unsubscribe
23+
* function. Loggers that don't implement it still get the connect-time level;
24+
* only *runtime* retargeting of the kernel is unavailable.
25+
*/
26+
onLevelChange?(listener: (level: LogLevel) => void): () => void;
1727
}
1828

1929
export enum LogLevel {

lib/sea/SeaBackend.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ export default class SeaBackend implements IBackend {
7171

7272
private nativeOptions?: SeaNativeConnectionOptions;
7373

74+
// Drops the kernel-log level-change listener on close. No-op until connect()
75+
// installs the bridge (and a no-op closure if the logger/binding can't
76+
// retarget at runtime).
77+
private kernelLogUnsubscribe: () => void = () => {};
78+
7479
constructor(options: SeaBackendOptions) {
7580
this.context = options.context;
7681
this.binding = options.nativeBinding ?? getSeaNative();
@@ -90,7 +95,7 @@ export default class SeaBackend implements IBackend {
9095
// that predates the bridge (logging is advisory).
9196
const logger = this.context.getLogger();
9297
const kernelLogLevel = logger.getLevel?.() ?? LogLevel.info;
93-
installKernelLogBridge(this.binding, logger, kernelLogLevel);
98+
this.kernelLogUnsubscribe = installKernelLogBridge(this.binding, logger, kernelLogLevel);
9499

95100
// Warn on the insecure combo: a `customCaCert` paired with
96101
// `checkServerCertificate: false` is almost always a mistake — verification
@@ -160,5 +165,11 @@ export default class SeaBackend implements IBackend {
160165
// No backend-level resources to release — each `SeaSessionBackend`
161166
// owns its own napi `Connection` lifecycle.
162167
this.nativeOptions = undefined;
168+
169+
// Stop retargeting the (process-global) kernel log level from this backend's
170+
// logger; the kernel sink itself is process-global and is replaced by the
171+
// next connect, matching the bridge's last-writer-wins model.
172+
this.kernelLogUnsubscribe();
173+
this.kernelLogUnsubscribe = () => {};
163174
}
164175
}

lib/sea/SeaLogging.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,24 @@ export function formatKernelLine(record: SeaNativeLogRecord): string {
8686
* process the most recently connected client's logger receives kernel logs —
8787
* mirroring the Python connector's `pyo3_log` model. Single-client apps, the
8888
* common case, are unaffected.
89+
* - **Runtime retargeting:** if `logger` exposes `onLevelChange` (as
90+
* `DBSQLLogger` does), the bridge subscribes so a later `logger.setLevel(...)`
91+
* also retargets the kernel-side filter via `setKernelLogLevel` — keeping
92+
* kernel verbosity in lock-step with the driver's at runtime, not just at
93+
* connect. Loggers without it still get the connect-time level.
8994
* - **Graceful on older bindings:** if the loaded `.node` predates
9095
* `initKernelLogging`, this is a no-op (kernel logs simply stay unbridged)
9196
* rather than a hard failure — logging is advisory.
97+
*
98+
* Returns an **unsubscribe** function the caller must invoke on teardown
99+
* (`SeaBackend.close()`) to drop the level-change listener; it is a safe no-op
100+
* when nothing was subscribed.
92101
*/
93-
export function installKernelLogBridge(binding: SeaNativeBinding, logger: IDBSQLLogger, level: LogLevel): void {
102+
export function installKernelLogBridge(binding: SeaNativeBinding, logger: IDBSQLLogger, level: LogLevel): () => void {
94103
// Defensive: a stale/older binding without the bridge export must not break
95104
// connect() — logging is non-critical.
96105
if (typeof binding.initKernelLogging !== 'function') {
97-
return;
106+
return () => {};
98107
}
99108

100109
const callback = (err: Error | null, records: Array<SeaNativeLogRecord>): void => {
@@ -107,4 +116,16 @@ export function installKernelLogBridge(binding: SeaNativeBinding, logger: IDBSQL
107116
};
108117

109118
binding.initKernelLogging(callback, logLevelToKernelLevel(level));
119+
120+
// Keep the kernel's level in lock-step with runtime `logger.setLevel(...)`
121+
// calls. Requires both a logger that notifies (`onLevelChange`) and a binding
122+
// that can retarget (`setKernelLogLevel`); otherwise the connect-time level
123+
// stands and there is nothing to unsubscribe.
124+
if (typeof logger.onLevelChange === 'function' && typeof binding.setKernelLogLevel === 'function') {
125+
return logger.onLevelChange((newLevel) => {
126+
binding.setKernelLogLevel(logLevelToKernelLevel(newLevel));
127+
});
128+
}
129+
130+
return () => {};
110131
}

tests/e2e/sea/logging-e2e.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ import { LogLevel } from '../../../lib/contracts/IDBSQLLogger';
2828
import config from '../utils/config';
2929
import { InternalConnectionOptions } from '../../../lib/contracts/InternalConnectionOptions';
3030

31-
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
31+
const delay = (ms: number) =>
32+
new Promise<void>((resolve) => {
33+
setTimeout(resolve, ms);
34+
});
3235

3336
describe('SEA — unified kernel + driver logging', function unifiedLogging() {
3437
// Live-warehouse round-trip plus async log flush.

tests/unit/DBSQLLogger.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) 2026 Databricks, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { expect } from 'chai';
16+
import DBSQLLogger from '../../lib/DBSQLLogger';
17+
import { LogLevel } from '../../lib/contracts/IDBSQLLogger';
18+
19+
describe('DBSQLLogger level subscription', () => {
20+
it('getLevel reflects the configured level', () => {
21+
const logger = new DBSQLLogger({ level: LogLevel.warn });
22+
expect(logger.getLevel()).to.equal(LogLevel.warn);
23+
});
24+
25+
it('onLevelChange fires subscribers with the new level on setLevel', () => {
26+
const logger = new DBSQLLogger({ level: LogLevel.info });
27+
const seen: LogLevel[] = [];
28+
logger.onLevelChange((level) => seen.push(level));
29+
30+
logger.setLevel(LogLevel.debug);
31+
logger.setLevel(LogLevel.error);
32+
33+
expect(seen).to.deep.equal([LogLevel.debug, LogLevel.error]);
34+
// setLevel still updates the logger's own level.
35+
expect(logger.getLevel()).to.equal(LogLevel.error);
36+
});
37+
38+
it('the returned unsubscribe stops further notifications', () => {
39+
const logger = new DBSQLLogger({ level: LogLevel.info });
40+
const seen: LogLevel[] = [];
41+
const unsubscribe = logger.onLevelChange((level) => seen.push(level));
42+
43+
logger.setLevel(LogLevel.debug);
44+
unsubscribe();
45+
logger.setLevel(LogLevel.warn);
46+
47+
expect(seen).to.deep.equal([LogLevel.debug]);
48+
});
49+
50+
it('a throwing subscriber does not break setLevel or other subscribers', () => {
51+
const logger = new DBSQLLogger({ level: LogLevel.info });
52+
const seen: LogLevel[] = [];
53+
logger.onLevelChange(() => {
54+
throw new Error('boom');
55+
});
56+
logger.onLevelChange((level) => seen.push(level));
57+
58+
expect(() => logger.setLevel(LogLevel.debug)).to.not.throw();
59+
expect(seen).to.deep.equal([LogLevel.debug]);
60+
expect(logger.getLevel()).to.equal(LogLevel.debug);
61+
});
62+
});

tests/unit/sea/logging.test.ts

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,55 @@ import {
2222
import { LogLevel } from '../../../lib/contracts/IDBSQLLogger';
2323
import { SeaNativeBinding, SeaNativeLogRecord } from '../../../lib/sea/SeaNativeLoader';
2424

25-
// Minimal recording logger.
26-
function recordingLogger() {
25+
// Minimal recording logger. With `withLevelChange` (default) it exposes
26+
// `onLevelChange` + an `emitLevelChange` test helper to simulate a runtime
27+
// `setLevel(...)`; pass `false` to model a logger that can't notify.
28+
function recordingLogger(opts: { withLevelChange?: boolean } = {}) {
2729
const lines: Array<{ level: LogLevel; message: string }> = [];
28-
return {
30+
const listeners: Array<(level: LogLevel) => void> = [];
31+
const logger = {
2932
lines,
3033
log(level: LogLevel, message: string) {
3134
lines.push({ level, message });
3235
},
36+
// test-only: simulate the driver's `setLevel(...)` firing its subscribers
37+
emitLevelChange(level: LogLevel) {
38+
listeners.forEach((l) => l(level));
39+
},
40+
} as {
41+
lines: Array<{ level: LogLevel; message: string }>;
42+
log(level: LogLevel, message: string): void;
43+
emitLevelChange(level: LogLevel): void;
44+
onLevelChange?(listener: (level: LogLevel) => void): () => void;
3345
};
46+
if (opts.withLevelChange !== false) {
47+
logger.onLevelChange = (listener: (level: LogLevel) => void) => {
48+
listeners.push(listener);
49+
return () => {
50+
const i = listeners.indexOf(listener);
51+
if (i >= 0) listeners.splice(i, 1);
52+
};
53+
};
54+
}
55+
return logger;
3456
}
3557

36-
// A binding stub that captures the registered bridge callback + level.
58+
// A binding stub that captures the registered bridge callback + initial level,
59+
// and records each `setKernelLogLevel` retarget.
3760
function captureBinding(overrides: Partial<Record<keyof SeaNativeBinding, unknown>> = {}) {
38-
const captured: { cb?: (err: Error | null, records: Array<SeaNativeLogRecord>) => void; level?: string } = {};
61+
const captured: {
62+
cb?: (err: Error | null, records: Array<SeaNativeLogRecord>) => void;
63+
level?: string;
64+
levelChanges: string[];
65+
} = { levelChanges: [] };
3966
const binding = {
4067
initKernelLogging: (cb: (err: Error | null, records: Array<SeaNativeLogRecord>) => void, level: string) => {
4168
captured.cb = cb;
4269
captured.level = level;
4370
},
71+
setKernelLogLevel: (level: string) => {
72+
captured.levelChanges.push(level);
73+
},
4474
...overrides,
4575
} as unknown as SeaNativeBinding;
4676
return { binding, captured };
@@ -119,4 +149,56 @@ describe('SeaLogging', () => {
119149
expect(() => installKernelLogBridge(binding, recordingLogger(), LogLevel.info)).to.not.throw();
120150
});
121151
});
152+
153+
describe('runtime level retargeting', () => {
154+
it('retargets the kernel level when the logger level changes at runtime', () => {
155+
const { binding, captured } = captureBinding();
156+
const logger = recordingLogger();
157+
installKernelLogBridge(binding, logger, LogLevel.warn);
158+
expect(captured.level).to.equal('warn'); // connect-time level
159+
160+
logger.emitLevelChange(LogLevel.debug);
161+
logger.emitLevelChange(LogLevel.error);
162+
expect(captured.levelChanges).to.deep.equal(['debug', 'error']);
163+
});
164+
165+
it('stops retargeting once the returned unsubscribe is called', () => {
166+
const { binding, captured } = captureBinding();
167+
const logger = recordingLogger();
168+
const unsubscribe = installKernelLogBridge(binding, logger, LogLevel.info);
169+
170+
logger.emitLevelChange(LogLevel.debug);
171+
unsubscribe();
172+
logger.emitLevelChange(LogLevel.error); // after unsubscribe → ignored
173+
174+
expect(captured.levelChanges).to.deep.equal(['debug']);
175+
});
176+
177+
it('does not subscribe when the logger cannot notify (no onLevelChange)', () => {
178+
const { binding, captured } = captureBinding();
179+
const logger = recordingLogger({ withLevelChange: false });
180+
const unsubscribe = installKernelLogBridge(binding, logger, LogLevel.info);
181+
logger.emitLevelChange(LogLevel.debug);
182+
expect(captured.levelChanges).to.have.length(0);
183+
expect(unsubscribe).to.be.a('function'); // safe no-op
184+
expect(() => unsubscribe()).to.not.throw();
185+
});
186+
187+
it('does not subscribe when the binding cannot retarget (no setKernelLogLevel)', () => {
188+
// initKernelLogging present, setKernelLogLevel absent (partial/older binding).
189+
const captured: { installed: boolean } = { installed: false };
190+
const binding = {
191+
initKernelLogging: () => {
192+
captured.installed = true;
193+
},
194+
} as unknown as SeaNativeBinding;
195+
const logger = recordingLogger();
196+
const unsubscribe = installKernelLogBridge(binding, logger, LogLevel.info);
197+
expect(captured.installed).to.equal(true);
198+
expect(() => {
199+
logger.emitLevelChange(LogLevel.debug);
200+
unsubscribe();
201+
}).to.not.throw();
202+
});
203+
});
122204
});

0 commit comments

Comments
 (0)