Skip to content
Merged
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
11 changes: 7 additions & 4 deletions packages/dbt-tools/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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: () => {},
Comment on lines +64 to 66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Error logs from dbt operations are now buffered but never retrieved, as the new getRecentDbtLogs() function is never called. This results in silent failures with no diagnostic output.
Severity: HIGH

Suggested Fix

The code that calls dbt operations and handles their failures (e.g., in tryExecuteViaDbt() or CLI command handlers) must be updated. In the catch blocks or after detecting a failure via stderr, these callers should invoke getRecentDbtLogs() and log the retrieved error messages to provide necessary diagnostic information.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/dbt-tools/src/adapter.ts#L79-L81

Potential issue: The modification to the dbt adapter's `terminal()` function routes all
error logs into an in-memory buffer, `dbtLogBuffer`, instead of `console.error`. While a
new function, `getRecentDbtLogs()`, was added to retrieve these buffered logs, it is
never called anywhere in the codebase. As a result, when any dbt operation fails (e.g.,
in `tryExecuteViaDbt()` or CLI commands), the error messages are silently buffered and
lost. This completely removes error visibility for debugging dbt failures, as the
diagnostic information is never retrieved or displayed to the user or developer.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 2d36571getRecentDbtLogs() is now wired into the bail() error handler in index.ts, so buffered logs are included in diagnostic JSON output on failure. The buffer was also extracted to its own log-buffer.ts module with 7 unit tests.

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: () => {},
}
Expand Down
13 changes: 12 additions & 1 deletion packages/dbt-tools/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = 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)
}
Expand Down
27 changes: 27 additions & 0 deletions packages/dbt-tools/src/log-buffer.ts
Original file line number Diff line number Diff line change
@@ -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
}
60 changes: 60 additions & 0 deletions packages/dbt-tools/test/adapter-buffer.test.ts
Original file line number Diff line number Diff line change
@@ -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"])
})
})
7 changes: 6 additions & 1 deletion packages/drivers/src/databricks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {
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<string, unknown> = {
host: config.server_hostname,
path: config.http_path,
Expand Down
10 changes: 10 additions & 0 deletions packages/drivers/src/snowflake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {
)
}

// 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[][] }> {
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/altimate/observability/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -781,7 +782,7 @@ export class Tracer {
let timer: ReturnType<typeof setTimeout>
const timeout = new Promise<undefined>((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)
})
Expand Down
Loading