Skip to content

Commit 08f4c5e

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 08f4c5e

File tree

4 files changed

+134
-115
lines changed

4 files changed

+134
-115
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: 52 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,100 @@
11
#!/bin/bash
2+
# Development Environment Startup Script
3+
4+
set -e
25

36
# =============================================================================
4-
# Development Environment Startup Script
7+
# Configuration
58
# =============================================================================
69

710
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
811
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
9-
export PATH="$PROJECT_ROOT/.bin:$PATH"
12+
LOG_DIR="$PROJECT_ROOT/debug/console"
13+
BUN="$PROJECT_ROOT/.bin/bun"
1014

11-
LOG_DIR="$PROJECT_ROOT/debug/proc"
15+
export PATH="$PROJECT_ROOT/.bin:$PATH"
1216
mkdir -p "$LOG_DIR"
1317

1418
# =============================================================================
1519
# UI Helpers
1620
# =============================================================================
1721

18-
SPINNER_FRAMES=('' '' '' '' '' '' '' '' '' '')
19-
20-
# Print a success line
21-
ok() {
22-
printf " \033[32m✓\033[0m %-10s %s\033[K\n" "$1" "$2"
23-
}
24-
25-
# Wait for a pattern in a log file, with spinner animation
26-
wait_for_log() {
27-
local name="$1"
28-
local pattern="$2"
29-
local timeout="${3:-60}"
30-
local frame=0
31-
local elapsed=0
32-
33-
printf "\033[?25l" # Hide cursor
34-
printf " %s %-10s starting...\n" "${SPINNER_FRAMES[0]}" "$name"
35-
36-
while ! grep -q "$pattern" "$LOG_DIR/$name.log" 2>/dev/null; do
37-
printf "\033[1A" # Move up one line
38-
printf " %s %-10s starting...\033[K\n" "${SPINNER_FRAMES[$frame]}" "$name"
39-
40-
frame=$(( (frame + 1) % ${#SPINNER_FRAMES[@]} ))
41-
sleep 0.1
42-
elapsed=$(( elapsed + 1 ))
43-
44-
if (( elapsed > timeout * 10 )); then
45-
printf "\033[?25h" # Show cursor
46-
echo "Timeout waiting for $name" >&2
22+
SPINNER=('' '' '' '' '' '' '' '' '' '')
23+
24+
ok() { printf " \033[32m✓\033[0m %-10s %s\033[K\n" "$1" "$2"; }
25+
fail() { printf " \033[31m✗\033[0m %-10s %s\033[K\n" "$1" "$2"; }
26+
27+
# Wait for a condition with spinner animation
28+
# Usage: wait_for <name> <check_command> [timeout_seconds]
29+
wait_for() {
30+
local name="$1" check="$2" timeout="${3:-60}"
31+
local frame=0 elapsed=0
32+
33+
printf "\033[?25l" # hide cursor
34+
while ! eval "$check" >/dev/null 2>&1; do
35+
printf "\r %s %-10s starting..." "${SPINNER[$frame]}" "$name"
36+
frame=$(( (frame + 1) % ${#SPINNER[@]} ))
37+
sleep 0.5
38+
elapsed=$((elapsed + 1))
39+
if ((elapsed > timeout * 2)); then
40+
printf "\033[?25h\n"
41+
fail "$name" "timeout"
4742
return 1
4843
fi
4944
done
50-
51-
printf "\033[1A" # Move up one line
45+
printf "\r"
5246
ok "$name" "ready!"
53-
printf "\033[?25h" # Show cursor
47+
printf "\033[?25h" # show cursor
5448
}
5549

5650
# =============================================================================
5751
# Cleanup
5852
# =============================================================================
5953

60-
kill_proc() {
61-
local name="$1"
62-
local pattern="$2"
63-
if pkill -f "$pattern" 2>/dev/null; then
64-
echo "[$(date '+%H:%M:%S')] Killed $name" >> "$LOG_DIR/$name.log"
65-
return 0
66-
fi
67-
return 1
68-
}
69-
7054
cleanup() {
71-
printf "\033[?25h" # Restore cursor
55+
printf "\033[?25h" # restore cursor
7256
echo ""
7357
echo "Shutting down..."
7458
echo ""
75-
76-
kill_proc "web" 'bun.*--cwd web dev' || kill_proc "web" 'next-server'
77-
ok "web" "stopped"
78-
79-
kill_proc "studio" 'drizzle-kit studio' && ok "studio" "stopped"
80-
kill_proc "sdk" 'bun.*--cwd sdk' && ok "sdk" "stopped"
81-
59+
tmux kill-session -t codebuff-web 2>/dev/null && ok "web" "stopped"
60+
pkill -f 'drizzle-kit studio' 2>/dev/null && ok "studio" "stopped"
61+
pkill -f 'bun.*--cwd sdk' 2>/dev/null && ok "sdk" "stopped"
8262
echo ""
8363
}
8464
trap cleanup EXIT INT TERM
8565

8666
# =============================================================================
87-
# Start Processes
67+
# Start Services
8868
# =============================================================================
8969

9070
echo "Starting development environment..."
9171
echo ""
9272

93-
# 1. Database (blocking)
94-
printf " %s %-10s starting...\r" "${SPINNER_FRAMES[0]}" "db"
73+
# 1. Database (blocking - must complete before other services)
74+
printf " %s %-10s starting...\r" "${SPINNER[0]}" "db"
9575
bun --cwd packages/internal db:start > "$LOG_DIR/db.log" 2>&1
9676
ok "db" "ready!"
9777

98-
# 2. Background processes (fire and forget)
99-
bun --cwd sdk run build > "$LOG_DIR/sdk.log" 2>&1 &
100-
ok "sdk" "(background)"
101-
78+
# 2. Background services (non-blocking)
79+
bun run --cwd sdk build > "$LOG_DIR/sdk.log" 2>&1 &
10280
bun --cwd packages/internal db:studio > "$LOG_DIR/studio.log" 2>&1 &
10381
ok "studio" "(background)"
10482

105-
# 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
83+
# 3. Web server (wait for health check)
84+
tmux kill-session -t codebuff-web 2>/dev/null || true
85+
pkill -f 'next-server' 2>/dev/null || true
86+
87+
# Use unbuffer for real-time log output (creates pseudo-TTY to prevent buffering)
88+
# Strip ANSI escape codes with sed -l for line-buffered output
89+
if command -v unbuffer &>/dev/null; then
90+
tmux new-session -d -s codebuff-web "cd $PROJECT_ROOT && unbuffer $BUN --cwd web dev 2>&1 | sed -l 's/\x1b\[[0-9;]*m//g' | tee $LOG_DIR/web.log"
91+
else
92+
tmux new-session -d -s codebuff-web "cd $PROJECT_ROOT && $BUN --cwd web dev 2>&1 | sed -l 's/\x1b\[[0-9;]*m//g' | tee $LOG_DIR/web.log"
93+
fi
94+
95+
wait_for "web" "curl -sf ${NEXT_PUBLIC_APP_URL}/api/healthz"
10896

109-
# 4. CLI
97+
# 4. CLI (foreground - user interaction)
11098
echo ""
11199
echo "Starting CLI..."
112100
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)