|
| 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 | +/** |
| 16 | + * Kernel → driver log bridge. |
| 17 | + * |
| 18 | + * The Rust kernel emits its diagnostics via `tracing`. In a Node process those |
| 19 | + * events have no subscriber and are dropped — so by default the driver's |
| 20 | + * `DBSQLLogger` only ever saw JS-side lines. The napi binding's |
| 21 | + * `initKernelLogging` installs a process-global subscriber that forwards |
| 22 | + * kernel events (batched) to a JS callback; this module wires that callback |
| 23 | + * into the **same** `IDBSQLLogger` the driver logs through, so logs from all |
| 24 | + * three layers (driver, napi shim, kernel) land in one place — and one file |
| 25 | + * when the logger has a file transport. |
| 26 | + * |
| 27 | + * Verbosity follows the driver's logger level (see `installKernelLogBridge`), |
| 28 | + * filtered kernel-side so we don't pay the channel/bridge cost for events the |
| 29 | + * sink would discard anyway. |
| 30 | + */ |
| 31 | + |
| 32 | +import IDBSQLLogger, { LogLevel } from '../contracts/IDBSQLLogger'; |
| 33 | +import { SeaNativeBinding, SeaNativeLogRecord } from './SeaNativeLoader'; |
| 34 | + |
| 35 | +/** |
| 36 | + * Map a kernel level string (`error`/`warn`/`info`/`debug`/`trace`) onto the |
| 37 | + * driver's `LogLevel`. The kernel's `trace` has no `LogLevel` analogue, so it |
| 38 | + * folds into `debug` (the most verbose driver level). |
| 39 | + */ |
| 40 | +export function kernelLevelToLogLevel(level: string): LogLevel { |
| 41 | + switch (level) { |
| 42 | + case 'error': |
| 43 | + return LogLevel.error; |
| 44 | + case 'warn': |
| 45 | + return LogLevel.warn; |
| 46 | + case 'info': |
| 47 | + return LogLevel.info; |
| 48 | + case 'debug': |
| 49 | + case 'trace': |
| 50 | + return LogLevel.debug; |
| 51 | + default: |
| 52 | + // Unknown kernel level — surface it rather than drop it; debug is the |
| 53 | + // least surprising bucket for an unrecognised severity. |
| 54 | + return LogLevel.debug; |
| 55 | + } |
| 56 | +} |
| 57 | + |
| 58 | +/** |
| 59 | + * Map a driver `LogLevel` onto the kernel level string the napi bridge expects. |
| 60 | + * The `LogLevel` enum values are already the kernel-compatible lower-case |
| 61 | + * strings, so this is the identity at runtime — kept as an explicit function so |
| 62 | + * the boundary is named and a future divergence has one place to live. |
| 63 | + */ |
| 64 | +export function logLevelToKernelLevel(level: LogLevel): string { |
| 65 | + return level; |
| 66 | +} |
| 67 | + |
| 68 | +/** |
| 69 | + * Format one kernel log record into a single driver log line, tagged with its |
| 70 | + * origin so kernel lines are distinguishable from driver lines in a shared |
| 71 | + * sink/file. |
| 72 | + */ |
| 73 | +export function formatKernelLine(record: SeaNativeLogRecord): string { |
| 74 | + return `[kernel ${record.target}] ${record.message}`; |
| 75 | +} |
| 76 | + |
| 77 | +/** |
| 78 | + * Install the kernel→driver log bridge: forward kernel `tracing` events into |
| 79 | + * `logger` at `level`. |
| 80 | + * |
| 81 | + * - **Verbosity** is set kernel-side to `level` so events the sink would drop |
| 82 | + * never cross the bridge. |
| 83 | + * - **Process-global, last-writer-wins:** the napi binding holds a single |
| 84 | + * process-global subscriber + sink (a `tracing` global subscriber installs |
| 85 | + * once). Each call retargets the sink to `logger`, so in a multi-client |
| 86 | + * process the most recently connected client's logger receives kernel logs — |
| 87 | + * mirroring the Python connector's `pyo3_log` model. Single-client apps, the |
| 88 | + * 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. |
| 94 | + * - **Graceful on older bindings:** if the loaded `.node` predates |
| 95 | + * `initKernelLogging`, this is a no-op (kernel logs simply stay unbridged) |
| 96 | + * 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. |
| 101 | + */ |
| 102 | +export function installKernelLogBridge(binding: SeaNativeBinding, logger: IDBSQLLogger, level: LogLevel): () => void { |
| 103 | + // Defensive: a stale/older binding without the bridge export must not break |
| 104 | + // connect() — logging is non-critical. |
| 105 | + if (typeof binding.initKernelLogging !== 'function') { |
| 106 | + return () => {}; |
| 107 | + } |
| 108 | + |
| 109 | + const callback = (err: Error | null, records: Array<SeaNativeLogRecord>): void => { |
| 110 | + if (err || !records) { |
| 111 | + return; |
| 112 | + } |
| 113 | + for (const record of records) { |
| 114 | + logger.log(kernelLevelToLogLevel(record.level), formatKernelLine(record)); |
| 115 | + } |
| 116 | + }; |
| 117 | + |
| 118 | + 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 () => {}; |
| 131 | +} |
0 commit comments