Design document for improving CKB's logging system in v8.0.
- Custom logging package - We have
internal/logginginstead of using Go's standardlog/slog(available since Go 1.21) - No global verbosity control - No
--verboseor--debugflags; must useCKB_LOG_LEVEL=debug - Inconsistent log destinations - Some commands log to stdout (mixing with output), others to stderr
- No log rotation - Daemon logs grow unbounded
- Limited contextual info - No request IDs, correlation IDs, or trace context
- No CLI-friendly quiet mode - Can't suppress all logs for scripting
| Scope | Destination | Persisted | Format |
|---|---|---|---|
| CLI commands | stdout | No | human |
| Daemon | ~/.ckb/daemon/daemon.log |
Yes | json |
| MCP | stderr | No | json |
| HTTP serve | stdout | No | human |
Replace custom internal/logging with Go's standard library log/slog:
// Before (custom)
logger := logging.NewLogger(logging.Config{
Level: logging.InfoLevel,
Format: logging.HumanFormat,
Output: os.Stdout,
})
logger.Info("message", map[string]interface{}{"key": "value"})
// After (slog)
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
logger.Info("message", "key", "value")Benefits:
- Standard library, no custom code to maintain
- Built-in JSON and text handlers
- Better performance (zero-allocation for common cases)
- LogValuer interface for custom types
- Context integration (
slog.InfoContext)
Add global flags to root command:
ckb status # Default: errors only (quiet)
ckb status -v # Verbose: info level
ckb status -vv # Very verbose: debug level
ckb status --quiet # Quiet: suppress all logs
ckb status --debug # Alias for -vvImplementation:
var (
verbosity int
quiet bool
)
func init() {
rootCmd.PersistentFlags().CountVarP(&verbosity, "verbose", "v", "Increase verbosity (-v=info, -vv=debug)")
rootCmd.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "Suppress all log output")
}
func getLogLevel() slog.Level {
if quiet {
return slog.Level(100) // Above all levels
}
switch verbosity {
case 0:
return slog.LevelWarn
case 1:
return slog.LevelInfo
default:
return slog.LevelDebug
}
}Rule: Logs go to stderr, output goes to stdout.
| Command Type | stdout | stderr |
|---|---|---|
| Query commands | JSON/human output | Logs |
| Status/info | Formatted output | Logs |
| MCP | JSON-RPC protocol | Logs |
| Daemon | (backgrounded) | Logs → file |
// All commands should use stderr for logs
logger := slog.New(slog.NewTextHandler(os.Stderr, opts))Add log rotation to prevent unbounded growth:
// Option A: Use lumberjack
import "gopkg.in/natefinch/lumberjack.v2"
logWriter := &lumberjack.Logger{
Filename: logPath,
MaxSize: 10, // MB
MaxBackups: 3,
MaxAge: 30, // days
Compress: true,
}
// Option B: Simple rotation on startup
func rotateLogIfNeeded(path string, maxSize int64) error {
info, err := os.Stat(path)
if err != nil || info.Size() < maxSize {
return nil
}
rotated := path + ".1"
os.Remove(rotated)
return os.Rename(path, rotated)
}Configuration in .ckb/config.json:
{
"daemon": {
"logFile": "~/.ckb/daemon/daemon.log",
"logMaxSize": "10MB",
"logMaxBackups": 3
}
}Add request/operation IDs for tracing:
// Generate operation ID for CLI commands
opID := fmt.Sprintf("op_%s", uuid.New().String()[:8])
ctx := context.WithValue(ctx, "operation_id", opID)
// Use slog with context
logger.InfoContext(ctx, "Starting operation",
"operation_id", opID,
"command", "status",
"repo", repoRoot,
)For MCP, use JSON-RPC request ID:
logger.Info("Handling tool call",
"request_id", req.ID,
"tool", req.Method,
"repo", repoRoot,
)Consistent error logging with stack traces in debug mode:
type LoggableError struct {
Err error
Stack string
}
func (e LoggableError) LogValue() slog.Value {
attrs := []slog.Attr{
slog.String("message", e.Err.Error()),
}
if e.Stack != "" {
attrs = append(attrs, slog.String("stack", e.Stack))
}
return slog.GroupValue(attrs...)
}Simplify to fewer, well-documented env vars:
| Variable | Values | Description |
|---|---|---|
CKB_LOG_LEVEL |
debug, info, warn, error | Log level |
CKB_LOG_FORMAT |
text, json | Output format |
CKB_LOG_FILE |
path | Write logs to file |
CKB_DEBUG |
1 | Shorthand for LOG_LEVEL=debug |
Prepare for observability integration:
// Bridge slog to OpenTelemetry
import "go.opentelemetry.io/contrib/bridges/otelslog"
handler := otelslog.NewHandler("ckb", otelslog.WithLoggerProvider(provider))
logger := slog.New(handler)
// Logs automatically include trace context
logger.InfoContext(ctx, "Processing request") // Includes trace_id, span_id- Add
--verbose,--quiet,--debugglobal flags - Route all logs to stderr
- Suppress logs in
statuscommand (done) - Add
ckb logcommand (done)
- Create
internal/slogutilwith helper functions - Migrate
internal/loggingto wrap slog - Update all command files to use new logger
- Add log rotation for daemon
- Add operation IDs for CLI commands
- Add request ID logging for MCP
- Add
--log-fileflag for persisting CLI logs - OpenTelemetry bridge for slog
- Correlation with existing telemetry
- Structured error logging with stack traces
Global Flags:
-v, --verbose count Increase verbosity (-v=info, -vv=debug)
-q, --quiet Suppress all log output
--debug Enable debug logging (same as -vv)
--log-file path Write logs to file
ckb log # View daemon logs (already implemented)
ckb log -f # Follow daemon logs
ckb log --clear # Clear daemon logs{
"logging": {
"level": "warn",
"format": "text",
"file": "",
"daemon": {
"maxSize": "10MB",
"maxBackups": 3,
"maxAge": 30
}
}
}