diff --git a/packages/dbt-tools/src/adapter.ts b/packages/dbt-tools/src/adapter.ts index c9d262b10b..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,17 +57,18 @@ function configuration(cfg: Config): DBTConfiguration { } } + 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/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 3c0cd68788..261af88f16 100644 --- a/packages/drivers/src/databricks.ts +++ b/packages/drivers/src/databricks.ts @@ -21,7 +21,12 @@ 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). + // 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, path: config.http_path, diff --git a/packages/drivers/src/snowflake.ts b/packages/drivers/src/snowflake.ts index aa9b381bff..e0e269906b 100644 --- a/packages/drivers/src/snowflake.ts +++ b/packages/drivers/src/snowflake.ts @@ -16,6 +16,16 @@ 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). + if (typeof snowflake.configure === "function") { + try { + snowflake.configure({ logLevel: "OFF" }) + } catch { + // Older SDK versions may not support this option; 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) })