From 304b6658ad8dff23c9de2cb2012a48c0bc27507b Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 18 Mar 2026 11:23:07 -0700 Subject: [PATCH 1/2] fix: suppress third-party and internal console logging that corrupts TUI display (#249) - `snowflake-sdk`: configure Winston log level to OFF (was writing JSON to stdout) - `@databricks/sql`: create `DBSQLLogger` with error-only level (Winston console transport) - `dbt-tools` adapter: replace `console.error` calls with in-memory ring buffer - Tracing module: route `console.debug`/`console.warn` through `Log.Default` (log file only) Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/dbt-tools/src/adapter.ts | 26 ++++++++++++++++--- packages/drivers/src/databricks.ts | 14 +++++++++- packages/drivers/src/snowflake.ts | 8 ++++++ .../src/altimate/observability/tracing.ts | 5 ++-- 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/dbt-tools/src/adapter.ts b/packages/dbt-tools/src/adapter.ts index c9d262b10b..5c43df6541 100644 --- a/packages/dbt-tools/src/adapter.ts +++ b/packages/dbt-tools/src/adapter.ts @@ -55,17 +55,35 @@ function configuration(cfg: Config): DBTConfiguration { } } +// Buffer of recent dbt log messages — keeps the last N entries in memory so +// errors can be retrieved for diagnostics without writing to stdout/stderr +// which would corrupt the TUI display (see #249). +const DBT_LOG_BUFFER_SIZE = 100 +const dbtLogBuffer: string[] = [] + +function bufferLog(msg: string): void { + dbtLogBuffer.push(msg) + if (dbtLogBuffer.length > DBT_LOG_BUFFER_SIZE) { + dbtLogBuffer.shift() + } +} + +/** Retrieve recent dbt log messages (for diagnostics / error reporting). */ +export function getRecentDbtLogs(): string[] { + return [...dbtLogBuffer] +} + function terminal(): DBTTerminal { return { show: async () => {}, - log: (msg: string) => console.error("[dbt]", msg), + log: (msg: string) => bufferLog(`[dbt] ${msg}`), trace: () => {}, debug: () => {}, - info: (_name: string, msg: string) => console.error("[dbt]", msg), - warn: (_name: string, msg: string) => console.error("[dbt:warn]", msg), + info: (_name: string, msg: string) => bufferLog(`[dbt] ${msg}`), + warn: (_name: string, msg: string) => bufferLog(`[dbt:warn] ${msg}`), error: (_name: string, msg: string, e: unknown) => { const err = e instanceof Error ? e.message : String(e) - console.error("[dbt:error]", msg, err) + bufferLog(`[dbt:error] ${msg} ${err}`) }, dispose: () => {}, } diff --git a/packages/drivers/src/databricks.ts b/packages/drivers/src/databricks.ts index 3c0cd68788..6ef5503b65 100644 --- a/packages/drivers/src/databricks.ts +++ b/packages/drivers/src/databricks.ts @@ -21,7 +21,19 @@ export async function connect(config: ConnectionConfig): Promise { return { async connect() { const DBSQLClient = databricksModule.DBSQLClient ?? databricksModule - client = new DBSQLClient() + + // Suppress @databricks/sql Winston console logging — it writes JSON + // log lines to stdout which corrupt the TUI display (see #249). + let logger: any + try { + const DBSQLLogger = databricksModule.DBSQLLogger + if (DBSQLLogger) { + logger = new DBSQLLogger({ level: "error" }) + } + } catch { + // Older SDK versions may not export DBSQLLogger; ignore. + } + client = new DBSQLClient({ logger }) const connectionOptions: Record = { host: config.server_hostname, path: config.http_path, diff --git a/packages/drivers/src/snowflake.ts b/packages/drivers/src/snowflake.ts index aa9b381bff..29d782be94 100644 --- a/packages/drivers/src/snowflake.ts +++ b/packages/drivers/src/snowflake.ts @@ -16,6 +16,14 @@ export async function connect(config: ConnectionConfig): Promise { ) } + // Suppress snowflake-sdk's Winston console logging — it writes JSON log + // lines to stdout which corrupt the TUI display (see #249). + try { + snowflake.configure({ logLevel: "OFF" }) + } catch { + // Older SDK versions may not support configure; ignore. + } + let connection: any function executeQuery(sql: string): Promise<{ columns: string[]; rows: any[][] }> { diff --git a/packages/opencode/src/altimate/observability/tracing.ts b/packages/opencode/src/altimate/observability/tracing.ts index a801721e5b..c64e74c969 100644 --- a/packages/opencode/src/altimate/observability/tracing.ts +++ b/packages/opencode/src/altimate/observability/tracing.ts @@ -21,6 +21,7 @@ import fsSync from "fs" import path from "path" import { Global } from "../../global" import { randomUUIDv7 } from "bun" +import { Log } from "../../util/log" // --------------------------------------------------------------------------- // Trace data types — v2 schema @@ -666,7 +667,7 @@ export class Tracer { .then(() => fs.writeFile(tmpPath, JSON.stringify(trace, null, 2))) .then(() => fs.rename(tmpPath, filePath)) .catch((err) => { - console.debug(`[tracing] failed to write trace snapshot: ${err}`) + Log.Default.debug(`[tracing] failed to write trace snapshot: ${err}`) fs.unlink(tmpPath).catch(() => {}) }) .finally(() => { @@ -781,7 +782,7 @@ export class Tracer { let timer: ReturnType const timeout = new Promise((resolve) => { timer = setTimeout(() => { - console.warn(`[tracing] Exporter "${name}" timed out after ${EXPORTER_TIMEOUT_MS}ms`) + Log.Default.warn(`[tracing] Exporter "${name}" timed out after ${EXPORTER_TIMEOUT_MS}ms`) resolve(undefined) }, EXPORTER_TIMEOUT_MS) }) From 2d3657130fee82ccf1f41ff664884556eaa112da Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 18 Mar 2026 11:45:29 -0700 Subject: [PATCH 2/2] fix: address code review findings for console log suppression (#249) - Extract dbt log buffer into `log-buffer.ts` for testable isolation - Wire `getRecentDbtLogs()` into `bail()` error path so buffered logs are included in diagnostic output instead of being silently lost - Add `clearDbtLogs()` for session isolation - Fix buffer order: evict before push (never exceeds `DBT_LOG_BUFFER_SIZE`) - Databricks: use no-op logger `{ log: () => {} }` instead of `DBSQLLogger({ level: "error" })` for complete stdout suppression - Snowflake: check `typeof configure === "function"` before calling - Add 7 unit tests for buffer behavior (FIFO, cap, clear, copy semantics) Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/dbt-tools/src/adapter.ts | 19 +----- packages/dbt-tools/src/index.ts | 13 +++- packages/dbt-tools/src/log-buffer.ts | 27 +++++++++ .../dbt-tools/test/adapter-buffer.test.ts | 60 +++++++++++++++++++ packages/drivers/src/databricks.ts | 11 +--- packages/drivers/src/snowflake.ts | 10 ++-- 6 files changed, 109 insertions(+), 31 deletions(-) create mode 100644 packages/dbt-tools/src/log-buffer.ts create mode 100644 packages/dbt-tools/test/adapter-buffer.test.ts diff --git a/packages/dbt-tools/src/adapter.ts b/packages/dbt-tools/src/adapter.ts index 5c43df6541..97da54f6a8 100644 --- a/packages/dbt-tools/src/adapter.ts +++ b/packages/dbt-tools/src/adapter.ts @@ -1,4 +1,6 @@ import type { Config } from "./config" +import { bufferLog } from "./log-buffer" +export { getRecentDbtLogs, clearDbtLogs } from "./log-buffer" import { DBTProjectIntegrationAdapter, DEFAULT_CONFIGURATION_VALUES, @@ -55,23 +57,6 @@ function configuration(cfg: Config): DBTConfiguration { } } -// Buffer of recent dbt log messages — keeps the last N entries in memory so -// errors can be retrieved for diagnostics without writing to stdout/stderr -// which would corrupt the TUI display (see #249). -const DBT_LOG_BUFFER_SIZE = 100 -const dbtLogBuffer: string[] = [] - -function bufferLog(msg: string): void { - dbtLogBuffer.push(msg) - if (dbtLogBuffer.length > DBT_LOG_BUFFER_SIZE) { - dbtLogBuffer.shift() - } -} - -/** Retrieve recent dbt log messages (for diagnostics / error reporting). */ -export function getRecentDbtLogs(): string[] { - return [...dbtLogBuffer] -} function terminal(): DBTTerminal { return { diff --git a/packages/dbt-tools/src/index.ts b/packages/dbt-tools/src/index.ts index 61db86a36a..24cb8a9ce5 100644 --- a/packages/dbt-tools/src/index.ts +++ b/packages/dbt-tools/src/index.ts @@ -92,7 +92,18 @@ function output(result: unknown) { } function bail(err: unknown): never { - const result = diagnose(err instanceof Error ? err : new Error(String(err))) + const result: Record = diagnose(err instanceof Error ? err : new Error(String(err))) + // Include buffered dbt logs for diagnostics (see #249 — logs are buffered + // in-memory instead of written to stderr to avoid TUI corruption). + try { + const { getRecentDbtLogs } = require("./log-buffer") as typeof import("./log-buffer") + const logs = getRecentDbtLogs() + if (logs.length > 0) { + result.logs = logs + } + } catch { + // log-buffer might not have been loaded yet + } console.log(JSON.stringify(result, null, 2)) process.exit(1) } diff --git a/packages/dbt-tools/src/log-buffer.ts b/packages/dbt-tools/src/log-buffer.ts new file mode 100644 index 0000000000..04e3223fd1 --- /dev/null +++ b/packages/dbt-tools/src/log-buffer.ts @@ -0,0 +1,27 @@ +/** + * In-memory ring buffer for dbt log messages. + * + * Captures dbt-integration library logging without writing to stdout/stderr, + * which would corrupt the TUI display (see #249). Buffered logs can be + * retrieved for diagnostics via getRecentDbtLogs(). + */ + +const DBT_LOG_BUFFER_SIZE = 100 +const dbtLogBuffer: string[] = [] + +export function bufferLog(msg: string): void { + if (dbtLogBuffer.length >= DBT_LOG_BUFFER_SIZE) { + dbtLogBuffer.shift() + } + dbtLogBuffer.push(msg) +} + +/** Retrieve recent dbt log messages (for diagnostics / error reporting). */ +export function getRecentDbtLogs(): string[] { + return [...dbtLogBuffer] +} + +/** Clear buffered logs (call on session/adapter reset). */ +export function clearDbtLogs(): void { + dbtLogBuffer.length = 0 +} diff --git a/packages/dbt-tools/test/adapter-buffer.test.ts b/packages/dbt-tools/test/adapter-buffer.test.ts new file mode 100644 index 0000000000..a2ff4de674 --- /dev/null +++ b/packages/dbt-tools/test/adapter-buffer.test.ts @@ -0,0 +1,60 @@ +import { describe, test, expect, beforeEach } from "bun:test" +import { bufferLog, getRecentDbtLogs, clearDbtLogs } from "../src/log-buffer" + +describe("dbt log buffer", () => { + beforeEach(() => { + clearDbtLogs() + }) + + test("starts empty", () => { + expect(getRecentDbtLogs()).toEqual([]) + }) + + test("buffers log messages", () => { + bufferLog("[dbt] compiling model") + bufferLog("[dbt:warn] deprecation notice") + expect(getRecentDbtLogs()).toEqual([ + "[dbt] compiling model", + "[dbt:warn] deprecation notice", + ]) + }) + + test("returns a copy, not a reference", () => { + bufferLog("test") + const logs = getRecentDbtLogs() + logs.push("injected") + expect(getRecentDbtLogs()).toEqual(["test"]) + }) + + test("caps at 100 entries (FIFO)", () => { + for (let i = 0; i < 120; i++) { + bufferLog(`msg-${i}`) + } + const logs = getRecentDbtLogs() + expect(logs.length).toBe(100) + expect(logs[0]).toBe("msg-20") + expect(logs[99]).toBe("msg-119") + }) + + test("never exceeds buffer size", () => { + for (let i = 0; i < 200; i++) { + bufferLog(`msg-${i}`) + // Buffer should never exceed 100 entries at any point + expect(getRecentDbtLogs().length).toBeLessThanOrEqual(100) + } + }) + + test("clearDbtLogs empties the buffer", () => { + bufferLog("a") + bufferLog("b") + clearDbtLogs() + expect(getRecentDbtLogs()).toEqual([]) + }) + + test("can buffer again after clearing", () => { + bufferLog("old") + clearDbtLogs() + bufferLog("new") + expect(getRecentDbtLogs()).toEqual(["new"]) + }) +}) diff --git a/packages/drivers/src/databricks.ts b/packages/drivers/src/databricks.ts index 6ef5503b65..261af88f16 100644 --- a/packages/drivers/src/databricks.ts +++ b/packages/drivers/src/databricks.ts @@ -24,15 +24,8 @@ export async function connect(config: ConnectionConfig): Promise { // Suppress @databricks/sql Winston console logging — it writes JSON // log lines to stdout which corrupt the TUI display (see #249). - let logger: any - try { - const DBSQLLogger = databricksModule.DBSQLLogger - if (DBSQLLogger) { - logger = new DBSQLLogger({ level: "error" }) - } - } catch { - // Older SDK versions may not export DBSQLLogger; ignore. - } + // Use a no-op logger that satisfies the interface but discards all output. + const logger = { log: () => {}, setLevel: () => {} } client = new DBSQLClient({ logger }) const connectionOptions: Record = { host: config.server_hostname, diff --git a/packages/drivers/src/snowflake.ts b/packages/drivers/src/snowflake.ts index 29d782be94..e0e269906b 100644 --- a/packages/drivers/src/snowflake.ts +++ b/packages/drivers/src/snowflake.ts @@ -18,10 +18,12 @@ export async function connect(config: ConnectionConfig): Promise { // Suppress snowflake-sdk's Winston console logging — it writes JSON log // lines to stdout which corrupt the TUI display (see #249). - try { - snowflake.configure({ logLevel: "OFF" }) - } catch { - // Older SDK versions may not support configure; ignore. + if (typeof snowflake.configure === "function") { + try { + snowflake.configure({ logLevel: "OFF" }) + } catch { + // Older SDK versions may not support this option; ignore. + } } let connection: any