Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion KERNEL_REV
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8bedaabf69f5bce5a957a8775f29dbb8dbdd2e71
99d6ffc8cb5165d304cd9ab8f57649e885493438
26 changes: 26 additions & 0 deletions lib/DBSQLLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export default class DBSQLLogger implements IDBSQLLogger {
file?: winston.transports.FileTransportInstance;
};

// Subscribers notified on `setLevel(...)` — used by the SEA/kernel backend to
// keep the kernel-side log bridge's verbosity in lock-step with this logger.
private levelListeners: Array<(level: LogLevel) => void> = [];

constructor({ level = LogLevel.info, filepath }: LoggerOptions = {}) {
this.transports = {
console: new winston.transports.Console({ handleExceptions: true, level }),
Expand All @@ -26,10 +30,32 @@ export default class DBSQLLogger implements IDBSQLLogger {
this.logger.log({ level, message });
}

getLevel(): LogLevel {
return (this.transports.console.level as LogLevel) ?? LogLevel.info;
}

onLevelChange(listener: (level: LogLevel) => void): () => void {
this.levelListeners.push(listener);
return () => {
const index = this.levelListeners.indexOf(listener);
if (index >= 0) {
this.levelListeners.splice(index, 1);
}
};
}

setLevel(level: LogLevel) {
this.transports.console.level = level;
if (this.transports.file) {
this.transports.file.level = level;
}
for (const listener of this.levelListeners) {
// A subscriber must never break level setting for the rest.
try {
listener(level);
} catch {
// swallow — level-change notification is advisory
}
}
}
}
19 changes: 19 additions & 0 deletions lib/contracts/IDBSQLLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ export interface LoggerOptions {

export default interface IDBSQLLogger {
log(level: LogLevel, message: string): void;

/**
* Optional: the logger's current level. When implemented, the SEA/kernel
* backend uses it to set the verbosity of the kernel-side (Rust) log bridge,
* so kernel logs are filtered at the same level as the driver's own logs and
* land in the same sink. Loggers that don't implement it leave the kernel
* bridge at its `info` default.
*/
getLevel?(): LogLevel;

/**
* Optional: subscribe to runtime level changes. When implemented, the
* SEA/kernel backend subscribes so a runtime `setLevel(...)` retargets the
* kernel-side log bridge too (not just the driver's own transports) — keeping
* kernel verbosity in lock-step with the driver's. Returns an unsubscribe
* function. Loggers that don't implement it still get the connect-time level;
* only *runtime* retargeting of the kernel is unavailable.
*/
onLevelChange?(listener: (level: LogLevel) => void): () => void;
}

export enum LogLevel {
Expand Down
22 changes: 22 additions & 0 deletions lib/sea/SeaBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import HiveDriverError from '../errors/HiveDriverError';
import { getSeaNative, SeaNativeBinding, SeaConnection } from './SeaNativeLoader';
import { decodeNapiKernelError } from './SeaErrorMapping';
import { buildSeaConnectionOptions, SeaNativeConnectionOptions } from './SeaAuth';
import { installKernelLogBridge } from './SeaLogging';
import SeaSessionBackend from './SeaSessionBackend';

export interface SeaBackendOptions {
Expand Down Expand Up @@ -70,6 +71,11 @@ export default class SeaBackend implements IBackend {

private nativeOptions?: SeaNativeConnectionOptions;

// Drops the kernel-log level-change listener on close. No-op until connect()
// installs the bridge (and a no-op closure if the logger/binding can't
// retarget at runtime).
private kernelLogUnsubscribe: () => void = () => {};

constructor(options: SeaBackendOptions) {
this.context = options.context;
this.binding = options.nativeBinding ?? getSeaNative();
Expand All @@ -81,6 +87,16 @@ export default class SeaBackend implements IBackend {
// we ever touch the native binding.
this.nativeOptions = buildSeaConnectionOptions(options);

// Bridge the Rust kernel's `tracing` logs into the SAME `DBSQLLogger` the
// driver logs through, so logs from all three layers (driver, napi shim,
// kernel) land in one place — and one file when the logger has a file
// transport. Kernel verbosity follows the logger's own level; loggers that
// don't expose `getLevel()` leave the bridge at `info`. No-op on a binding
// that predates the bridge (logging is advisory).
const logger = this.context.getLogger();
const kernelLogLevel = logger.getLevel?.() ?? LogLevel.info;
this.kernelLogUnsubscribe = installKernelLogBridge(this.binding, logger, kernelLogLevel);

// Warn on the insecure combo: a `customCaCert` paired with
// `checkServerCertificate: false` is almost always a mistake — verification
// is fully off, so the custom trust anchor is never used. The combo is
Expand Down Expand Up @@ -149,5 +165,11 @@ export default class SeaBackend implements IBackend {
// No backend-level resources to release — each `SeaSessionBackend`
// owns its own napi `Connection` lifecycle.
this.nativeOptions = undefined;

// Stop retargeting the (process-global) kernel log level from this backend's
// logger; the kernel sink itself is process-global and is replaced by the
// next connect, matching the bridge's last-writer-wins model.
this.kernelLogUnsubscribe();
this.kernelLogUnsubscribe = () => {};
}
}
131 changes: 131 additions & 0 deletions lib/sea/SeaLogging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (c) 2026 Databricks, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* Kernel → driver log bridge.
*
* The Rust kernel emits its diagnostics via `tracing`. In a Node process those
* events have no subscriber and are dropped — so by default the driver's
* `DBSQLLogger` only ever saw JS-side lines. The napi binding's
* `initKernelLogging` installs a process-global subscriber that forwards
* kernel events (batched) to a JS callback; this module wires that callback
* into the **same** `IDBSQLLogger` the driver logs through, so logs from all
* three layers (driver, napi shim, kernel) land in one place — and one file
* when the logger has a file transport.
*
* Verbosity follows the driver's logger level (see `installKernelLogBridge`),
* filtered kernel-side so we don't pay the channel/bridge cost for events the
* sink would discard anyway.
*/

import IDBSQLLogger, { LogLevel } from '../contracts/IDBSQLLogger';
import { SeaNativeBinding, SeaNativeLogRecord } from './SeaNativeLoader';

/**
* Map a kernel level string (`error`/`warn`/`info`/`debug`/`trace`) onto the
* driver's `LogLevel`. The kernel's `trace` has no `LogLevel` analogue, so it
* folds into `debug` (the most verbose driver level).
*/
export function kernelLevelToLogLevel(level: string): LogLevel {
switch (level) {
case 'error':
return LogLevel.error;
case 'warn':
return LogLevel.warn;
case 'info':
return LogLevel.info;
case 'debug':
case 'trace':
return LogLevel.debug;
default:
// Unknown kernel level — surface it rather than drop it; debug is the
// least surprising bucket for an unrecognised severity.
return LogLevel.debug;
}
}

/**
* Map a driver `LogLevel` onto the kernel level string the napi bridge expects.
* The `LogLevel` enum values are already the kernel-compatible lower-case
* strings, so this is the identity at runtime — kept as an explicit function so
* the boundary is named and a future divergence has one place to live.
*/
export function logLevelToKernelLevel(level: LogLevel): string {
return level;
}

/**
* Format one kernel log record into a single driver log line, tagged with its
* origin so kernel lines are distinguishable from driver lines in a shared
* sink/file.
*/
export function formatKernelLine(record: SeaNativeLogRecord): string {
return `[kernel ${record.target}] ${record.message}`;
}

/**
* Install the kernel→driver log bridge: forward kernel `tracing` events into
* `logger` at `level`.
*
* - **Verbosity** is set kernel-side to `level` so events the sink would drop
* never cross the bridge.
* - **Process-global, last-writer-wins:** the napi binding holds a single
* process-global subscriber + sink (a `tracing` global subscriber installs
* once). Each call retargets the sink to `logger`, so in a multi-client
* process the most recently connected client's logger receives kernel logs —
* mirroring the Python connector's `pyo3_log` model. Single-client apps, the
* common case, are unaffected.
* - **Runtime retargeting:** if `logger` exposes `onLevelChange` (as
* `DBSQLLogger` does), the bridge subscribes so a later `logger.setLevel(...)`
* also retargets the kernel-side filter via `setKernelLogLevel` — keeping
* kernel verbosity in lock-step with the driver's at runtime, not just at
* connect. Loggers without it still get the connect-time level.
* - **Graceful on older bindings:** if the loaded `.node` predates
* `initKernelLogging`, this is a no-op (kernel logs simply stay unbridged)
* rather than a hard failure — logging is advisory.
*
* Returns an **unsubscribe** function the caller must invoke on teardown
* (`SeaBackend.close()`) to drop the level-change listener; it is a safe no-op
* when nothing was subscribed.
*/
export function installKernelLogBridge(binding: SeaNativeBinding, logger: IDBSQLLogger, level: LogLevel): () => void {
// Defensive: a stale/older binding without the bridge export must not break
// connect() — logging is non-critical.
if (typeof binding.initKernelLogging !== 'function') {
return () => {};
}

const callback = (err: Error | null, records: Array<SeaNativeLogRecord>): void => {
if (err || !records) {
return;
}
for (const record of records) {
logger.log(kernelLevelToLogLevel(record.level), formatKernelLine(record));
}
};

binding.initKernelLogging(callback, logLevelToKernelLevel(level));

// Keep the kernel's level in lock-step with runtime `logger.setLevel(...)`
// calls. Requires both a logger that notifies (`onLevelChange`) and a binding
// that can retarget (`setKernelLogLevel`); otherwise the connect-time level
// stands and there is nothing to unsubscribe.
if (typeof logger.onLevelChange === 'function' && typeof binding.setKernelLogLevel === 'function') {
return logger.onLevelChange((newLevel) => {
binding.setKernelLogLevel(logLevelToKernelLevel(newLevel));
});
}

return () => {};
}
6 changes: 6 additions & 0 deletions lib/sea/SeaNativeLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type {
AsyncStatement as NativeAsyncStatement,
AsyncResultHandle as NativeAsyncResultHandle,
CancellableExecution as NativeCancellableExecution,
LogRecord as NativeLogRecord,
} from '../../native/sea';

// SEA-prefixed re-exports. The kernel-generated `.d.ts` keeps the
Expand Down Expand Up @@ -78,6 +79,11 @@ export type SeaNativeAsyncResultHandle = NativeAsyncResultHandle;
// returns.
export type SeaNativeCancellableExecution = NativeCancellableExecution;

// One kernel log event forwarded over the napi log bridge (see SeaLogging.ts):
// `{ level, target, message }`. Re-exported so the bridge can name the shape
// without re-declaring it (stays in lock-step with the kernel contract).
export type SeaNativeLogRecord = NativeLogRecord;

/**
* The full native binding surface, derived from the generated module
* so it can never drift from the `.d.ts` contract: when the kernel
Expand Down
30 changes: 30 additions & 0 deletions native/sea/index.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion native/sea/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading