Skip to content

Commit 2c18ea5

Browse files
committed
refactor(dev): improve debug log organization
- Move raw console logs to debug/console/ subdirectory - Rename pino logs to .jsonl extension for clarity - Rename web.log to next.log to distinguish from pino web.jsonl - Simplify log paths (removed debug/proc/ nesting) New structure: debug/ ├── cli.jsonl (CLI app, structured JSON) ├── web.jsonl (Web app, structured JSON) └── console/ ├── next.log (Next.js framework) ├── db.log (Database startup) ├── sdk.log (SDK build) └── studio.log (Drizzle Studio)
1 parent 90a8c70 commit 2c18ea5

File tree

4 files changed

+119
-61
lines changed

4 files changed

+119
-61
lines changed

cli/src/utils/logger.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, mkdirSync, unlinkSync } from 'fs'
1+
import { appendFileSync, existsSync, mkdirSync, unlinkSync } from 'fs'
22
import path, { dirname } from 'path'
33
import { format as stringFormat } from 'util'
44

@@ -49,7 +49,7 @@ function setLogPath(p: string): void {
4949
const fileStream = pino.destination({
5050
dest: p, // absolute or relative file path
5151
mkdir: true, // create parent dirs if they don’t exist
52-
sync: false, // set true if you *must* block on every write
52+
sync: true, // set true if you *must* block on every write
5353
})
5454

5555
pinoLogger = pino(
@@ -66,7 +66,7 @@ function setLogPath(p: string): void {
6666

6767
export function clearLogFile(): void {
6868
const projectRoot = getProjectRoot()
69-
const defaultLog = path.join(projectRoot, 'debug', 'cli.log')
69+
const defaultLog = path.join(projectRoot, 'debug', 'cli.jsonl')
7070
const targets = new Set<string>()
7171

7272
if (logPath) {
@@ -102,7 +102,7 @@ function sendAnalyticsAndLog(
102102

103103
const logTarget =
104104
env.NEXT_PUBLIC_CB_ENVIRONMENT === 'dev'
105-
? path.join(projectRoot, 'debug', 'cli.log')
105+
? path.join(projectRoot, 'debug', 'cli.jsonl')
106106
: path.join(getCurrentChatDir(), 'log.jsonl')
107107

108108
setLogPath(logTarget)
@@ -143,7 +143,22 @@ function sendAnalyticsAndLog(
143143
trackEvent(analyticsEventId, toTrack)
144144
}
145145

146-
if (pinoLogger !== undefined) {
146+
// In dev mode, use appendFileSync for real-time logging (Bun has issues with pino sync)
147+
// In prod mode, use pino for better performance
148+
if (env.NEXT_PUBLIC_CB_ENVIRONMENT === 'dev' && logPath) {
149+
const logEntry = JSON.stringify({
150+
level: level.toUpperCase(),
151+
timestamp: new Date().toISOString(),
152+
...loggerContext,
153+
...(includeData ? { data: normalizedData } : {}),
154+
msg: stringFormat(normalizedMsg ?? '', ...args),
155+
})
156+
try {
157+
appendFileSync(logPath, logEntry + '\n')
158+
} catch {
159+
// Ignore write errors
160+
}
161+
} else if (pinoLogger !== undefined) {
147162
const base = { ...loggerContext }
148163
const obj = includeData ? { ...base, data: normalizedData } : base
149164
pinoLogger[level](obj, normalizedMsg as any, ...args)

scripts/dev.sh

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
88
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
99
export PATH="$PROJECT_ROOT/.bin:$PATH"
1010

11-
LOG_DIR="$PROJECT_ROOT/debug/proc"
11+
LOG_DIR="$PROJECT_ROOT/debug/console"
1212
mkdir -p "$LOG_DIR"
1313

1414
# =============================================================================
@@ -61,7 +61,7 @@ kill_proc() {
6161
local name="$1"
6262
local pattern="$2"
6363
if pkill -f "$pattern" 2>/dev/null; then
64-
echo "[$(date '+%H:%M:%S')] Killed $name" >> "$LOG_DIR/$name.log"
64+
echo "[$(date '+%H:%M:%S')] Killed $name" >> "$LOG_DIR/$name.log" 2>/dev/null
6565
return 0
6666
fi
6767
return 1
@@ -73,8 +73,9 @@ cleanup() {
7373
echo "Shutting down..."
7474
echo ""
7575

76-
kill_proc "web" 'bun.*--cwd web dev' || kill_proc "web" 'next-server'
77-
ok "web" "stopped"
76+
# Kill web server (tmux session or background process)
77+
tmux kill-session -t codebuff-web 2>/dev/null || kill_proc "next" 'bun.*--cwd web dev' || kill_proc "next" 'next-server'
78+
ok "next" "stopped"
7879

7980
kill_proc "studio" 'drizzle-kit studio' && ok "studio" "stopped"
8081
kill_proc "sdk" 'bun.*--cwd sdk' && ok "sdk" "stopped"
@@ -95,18 +96,44 @@ printf " %s %-10s starting...\r" "${SPINNER_FRAMES[0]}" "db"
9596
bun --cwd packages/internal db:start > "$LOG_DIR/db.log" 2>&1
9697
ok "db" "ready!"
9798

98-
# 2. Background processes (fire and forget)
99-
bun --cwd sdk run build > "$LOG_DIR/sdk.log" 2>&1 &
100-
ok "sdk" "(background)"
99+
# 2. Background processes
100+
bun run --cwd sdk build > "$LOG_DIR/sdk.log" 2>&1 &
101101

102102
bun --cwd packages/internal db:studio > "$LOG_DIR/studio.log" 2>&1 &
103103
ok "studio" "(background)"
104104

105105
# 3. Web server (wait for ready)
106-
bun --cwd web dev > "$LOG_DIR/web.log" 2>&1 &
107-
wait_for_log "web" "Ready in" 60
106+
tmux kill-session -t codebuff-web 2>/dev/null
107+
pkill -f 'next-server' 2>/dev/null
108+
109+
# Start web server in tmux with unbuffer for real-time logging
110+
# unbuffer creates a pseudo-TTY which prevents output buffering
111+
if command -v unbuffer &>/dev/null; then
112+
tmux new-session -d -s codebuff-web "cd $PROJECT_ROOT && unbuffer $PROJECT_ROOT/.bin/bun --cwd web dev 2>&1 | tee $LOG_DIR/next.log"
113+
else
114+
# Fallback without unbuffer (logs may be delayed)
115+
tmux new-session -d -s codebuff-web "cd $PROJECT_ROOT && $PROJECT_ROOT/.bin/bun --cwd web dev 2>&1 | tee $LOG_DIR/next.log"
116+
fi
117+
118+
# Wait for web server to be ready by polling the health endpoint
119+
printf " %s %-10s starting..." "${SPINNER_FRAMES[0]}" "next"
120+
frame=0
121+
elapsed=0
122+
while ! curl -sf http://localhost:3000/api/healthz > /dev/null 2>&1; do
123+
printf "\r %s %-10s starting..." "${SPINNER_FRAMES[$frame]}" "next"
124+
frame=$(( (frame + 1) % ${#SPINNER_FRAMES[@]} ))
125+
sleep 0.5
126+
elapsed=$(( elapsed + 1 ))
127+
if (( elapsed > 120 )); then # 60 second timeout (0.5s * 120)
128+
echo ""
129+
echo "Timeout waiting for web server" >&2
130+
exit 1
131+
fi
132+
done
133+
printf "\r"
134+
ok "next" "ready!"
108135

109-
# 4. CLI
136+
# 4. CLI (SDK builds in background)
110137
echo ""
111138
echo "Starting CLI..."
112139
bun --cwd cli dev "$@"

sdk/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
"fetch-ripgrep": "bun scripts/fetch-ripgrep.ts",
4040
"prepack": "bun run build",
4141
"typecheck": "tsc --noEmit -p .",
42-
"test": "bun test"
42+
"test": "bun test",
43+
"dev": "bun run build"
4344
},
4445
"engines": {
4546
"node": ">=18.0.0"

web/src/util/logger.ts

Lines changed: 60 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import fs from 'fs'
1+
import fs, { appendFileSync } from 'fs'
22
import path from 'path'
33
import { format } from 'util'
44

@@ -19,40 +19,32 @@ function getDebugDir(): string | null {
1919
if (debugDir !== undefined) {
2020
return debugDir
2121
}
22-
debugDir = __dirname
23-
while (true) {
24-
if (fs.existsSync(path.join(debugDir, '.git'))) {
25-
break
22+
// Walk up from cwd to find the git root (where .git exists)
23+
let dir = process.cwd()
24+
while (dir !== path.dirname(dir)) {
25+
if (fs.existsSync(path.join(dir, '.git'))) {
26+
debugDir = path.join(dir, 'debug')
27+
return debugDir
2628
}
27-
const parent = path.dirname(debugDir)
28-
if (parent === debugDir) {
29-
debugDir = null
30-
console.error('Failed to find git root directory for logger')
31-
break
32-
}
33-
debugDir = parent
34-
}
35-
36-
if (debugDir) {
37-
debugDir = path.join(debugDir, 'debug')
29+
dir = path.dirname(dir)
3830
}
39-
31+
debugDir = null
32+
console.error('Failed to find git root directory for logger')
4033
return debugDir
4134
}
4235

43-
setLocalDebugDir: if (
36+
// Initialize debug directory in dev environment
37+
if (
4438
env.NEXT_PUBLIC_CB_ENVIRONMENT === 'dev' &&
4539
process.env.CODEBUFF_GITHUB_ACTIONS !== 'true'
4640
) {
47-
const debugDir = getDebugDir()
48-
if (!debugDir) {
49-
break setLocalDebugDir
50-
}
51-
52-
try {
53-
fs.mkdirSync(debugDir, { recursive: true })
54-
} catch (err) {
55-
console.error('Failed to create debug directory:', err)
41+
const dir = getDebugDir()
42+
if (dir) {
43+
try {
44+
fs.mkdirSync(dir, { recursive: true })
45+
} catch {
46+
// Ignore errors when creating debug directory
47+
}
5648
}
5749
}
5850

@@ -67,12 +59,10 @@ const pinoLogger = pino(
6759
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
6860
},
6961
debugDir
70-
? pino.transport({
71-
target: 'pino/file',
72-
options: {
73-
destination: path.join(debugDir, 'web.log'),
74-
},
75-
level: 'debug',
62+
? pino.destination({
63+
dest: path.join(debugDir, 'web.jsonl'),
64+
mkdir: true,
65+
sync: true, // sync writes for real-time logging
7666
})
7767
: undefined,
7868
)
@@ -108,18 +98,43 @@ function splitAndLog(
10898
})
10999
}
110100

111-
export const logger: Record<LogLevel, pino.LogFn> =
112-
env.NEXT_PUBLIC_CB_ENVIRONMENT === 'dev'
113-
? pinoLogger
114-
: (Object.fromEntries(
115-
loggingLevels.map((level) => {
116-
return [
117-
level,
118-
(data: any, msg?: string, ...args: any[]) =>
119-
splitAndLog(level, data, msg, ...args),
120-
]
121-
}),
122-
) as Record<LogLevel, pino.LogFn>)
101+
// In dev mode, use appendFileSync for real-time file logging (Bun has issues with pino sync)
102+
// Also output to console via pinoLogger so logs remain visible in the terminal
103+
function logWithSync(level: LogLevel, data: any, msg?: string, ...args: any[]): void {
104+
const formattedMsg = format(msg ?? '', ...args)
105+
106+
if (env.NEXT_PUBLIC_CB_ENVIRONMENT === 'dev') {
107+
// Write to file for real-time logging
108+
if (debugDir) {
109+
const logEntry = JSON.stringify({
110+
level: level.toUpperCase(),
111+
timestamp: new Date().toISOString(),
112+
...(data && typeof data === 'object' ? data : { data }),
113+
msg: formattedMsg,
114+
})
115+
try {
116+
appendFileSync(path.join(debugDir, 'web.jsonl'), logEntry + '\n')
117+
} catch {
118+
// Ignore write errors
119+
}
120+
}
121+
// Also output to console for interactive debugging
122+
pinoLogger[level](data, msg, ...args)
123+
} else {
124+
// In prod, use pino with splitAndLog for large payloads
125+
splitAndLog(level, data, msg, ...args)
126+
}
127+
}
128+
129+
export const logger: Record<LogLevel, pino.LogFn> = Object.fromEntries(
130+
loggingLevels.map((level) => {
131+
return [
132+
level,
133+
(data: any, msg?: string, ...args: any[]) =>
134+
logWithSync(level, data, msg, ...args),
135+
]
136+
}),
137+
) as Record<LogLevel, pino.LogFn>
123138

124139
export function loggerWithContext(
125140
context: ParamsOf<LoggerWithContextFn>,

0 commit comments

Comments
 (0)