562 lines
16 KiB
Markdown
562 lines
16 KiB
Markdown
# Simple Debug System Proposal
|
|
|
|
## Overview
|
|
|
|
A minimal debug system with 6 levels (0-5) controlled by a single `--debug-level` flag. TRACE level (5) automatically includes file:line information for ALL messages. Uses compile-time macros to ensure **zero performance impact and zero size increase** in production builds.
|
|
|
|
## Debug Levels
|
|
|
|
```c
|
|
typedef enum {
|
|
DEBUG_LEVEL_NONE = 0, // Production: no debug output
|
|
DEBUG_LEVEL_ERROR = 1, // Errors only
|
|
DEBUG_LEVEL_WARN = 2, // Errors + Warnings
|
|
DEBUG_LEVEL_INFO = 3, // Errors + Warnings + Info
|
|
DEBUG_LEVEL_DEBUG = 4, // All above + Debug messages
|
|
DEBUG_LEVEL_TRACE = 5 // All above + Trace (very verbose)
|
|
} debug_level_t;
|
|
```
|
|
|
|
## Usage
|
|
|
|
```bash
|
|
# Production (default - no debug output)
|
|
./c_relay_x86
|
|
|
|
# Show errors only
|
|
./c_relay_x86 --debug-level=1
|
|
|
|
# Show errors and warnings
|
|
./c_relay_x86 --debug-level=2
|
|
|
|
# Show errors, warnings, and info (recommended for development)
|
|
./c_relay_x86 --debug-level=3
|
|
|
|
# Show all debug messages
|
|
./c_relay_x86 --debug-level=4
|
|
|
|
# Show everything including trace with file:line (very verbose)
|
|
./c_relay_x86 --debug-level=5
|
|
```
|
|
|
|
## Implementation
|
|
|
|
### 1. Header File (`src/debug.h`)
|
|
|
|
```c
|
|
#ifndef DEBUG_H
|
|
#define DEBUG_H
|
|
|
|
#include <stdio.h>
|
|
#include <time.h>
|
|
|
|
// Debug levels
|
|
typedef enum {
|
|
DEBUG_LEVEL_NONE = 0,
|
|
DEBUG_LEVEL_ERROR = 1,
|
|
DEBUG_LEVEL_WARN = 2,
|
|
DEBUG_LEVEL_INFO = 3,
|
|
DEBUG_LEVEL_DEBUG = 4,
|
|
DEBUG_LEVEL_TRACE = 5
|
|
} debug_level_t;
|
|
|
|
// Global debug level (set at runtime via CLI)
|
|
extern debug_level_t g_debug_level;
|
|
|
|
// Initialize debug system
|
|
void debug_init(int level);
|
|
|
|
// Core logging function
|
|
void debug_log(debug_level_t level, const char* file, int line, const char* format, ...);
|
|
|
|
// Convenience macros that check level before calling
|
|
// Note: TRACE level (5) and above include file:line information for ALL messages
|
|
#define DEBUG_ERROR(...) \
|
|
do { if (g_debug_level >= DEBUG_LEVEL_ERROR) debug_log(DEBUG_LEVEL_ERROR, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
|
|
|
#define DEBUG_WARN(...) \
|
|
do { if (g_debug_level >= DEBUG_LEVEL_WARN) debug_log(DEBUG_LEVEL_WARN, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
|
|
|
#define DEBUG_INFO(...) \
|
|
do { if (g_debug_level >= DEBUG_LEVEL_INFO) debug_log(DEBUG_LEVEL_INFO, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
|
|
|
#define DEBUG_LOG(...) \
|
|
do { if (g_debug_level >= DEBUG_LEVEL_DEBUG) debug_log(DEBUG_LEVEL_DEBUG, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
|
|
|
#define DEBUG_TRACE(...) \
|
|
do { if (g_debug_level >= DEBUG_LEVEL_TRACE) debug_log(DEBUG_LEVEL_TRACE, __FILE__, __LINE__, __VA_ARGS__); } while(0)
|
|
|
|
#endif /* DEBUG_H */
|
|
```
|
|
|
|
### 2. Implementation File (`src/debug.c`)
|
|
|
|
```c
|
|
#include "debug.h"
|
|
#include <stdarg.h>
|
|
#include <string.h>
|
|
|
|
// Global debug level (default: no debug output)
|
|
debug_level_t g_debug_level = DEBUG_LEVEL_NONE;
|
|
|
|
void debug_init(int level) {
|
|
if (level < 0) level = 0;
|
|
if (level > 5) level = 5;
|
|
g_debug_level = (debug_level_t)level;
|
|
}
|
|
|
|
void debug_log(debug_level_t level, const char* file, int line, const char* format, ...) {
|
|
// Get timestamp
|
|
time_t now = time(NULL);
|
|
struct tm* tm_info = localtime(&now);
|
|
char timestamp[32];
|
|
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);
|
|
|
|
// Get level string
|
|
const char* level_str = "UNKNOWN";
|
|
switch (level) {
|
|
case DEBUG_LEVEL_ERROR: level_str = "ERROR"; break;
|
|
case DEBUG_LEVEL_WARN: level_str = "WARN "; break;
|
|
case DEBUG_LEVEL_INFO: level_str = "INFO "; break;
|
|
case DEBUG_LEVEL_DEBUG: level_str = "DEBUG"; break;
|
|
case DEBUG_LEVEL_TRACE: level_str = "TRACE"; break;
|
|
default: break;
|
|
}
|
|
|
|
// Print prefix with timestamp and level
|
|
printf("[%s] [%s] ", timestamp, level_str);
|
|
|
|
// Print source location when debug level is TRACE (5) or higher
|
|
if (file && g_debug_level >= DEBUG_LEVEL_TRACE) {
|
|
// Extract just the filename (not full path)
|
|
const char* filename = strrchr(file, '/');
|
|
filename = filename ? filename + 1 : file;
|
|
printf("[%s:%d] ", filename, line);
|
|
}
|
|
|
|
// Print message
|
|
va_list args;
|
|
va_start(args, format);
|
|
vprintf(format, args);
|
|
va_end(args);
|
|
|
|
printf("\n");
|
|
fflush(stdout);
|
|
}
|
|
```
|
|
|
|
### 3. CLI Argument Parsing (add to `src/main.c`)
|
|
|
|
```c
|
|
// In main() function, add to argument parsing:
|
|
|
|
int debug_level = 0; // Default: no debug output
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strncmp(argv[i], "--debug-level=", 14) == 0) {
|
|
debug_level = atoi(argv[i] + 14);
|
|
if (debug_level < 0) debug_level = 0;
|
|
if (debug_level > 5) debug_level = 5;
|
|
}
|
|
// ... other arguments ...
|
|
}
|
|
|
|
// Initialize debug system
|
|
debug_init(debug_level);
|
|
```
|
|
|
|
### 4. Update Makefile
|
|
|
|
```makefile
|
|
# Add debug.c to source files
|
|
MAIN_SRC = src/main.c src/config.c src/debug.c src/dm_admin.c src/request_validator.c ...
|
|
```
|
|
|
|
## Migration Strategy
|
|
|
|
### Keep Existing Functions
|
|
|
|
The existing `log_*` functions can remain as wrappers:
|
|
|
|
```c
|
|
// src/main.c - Update existing functions
|
|
// Note: These don't include file:line since they're wrappers
|
|
void log_info(const char* message) {
|
|
if (g_debug_level >= DEBUG_LEVEL_INFO) {
|
|
debug_log(DEBUG_LEVEL_INFO, NULL, 0, "%s", message);
|
|
}
|
|
}
|
|
|
|
void log_error(const char* message) {
|
|
if (g_debug_level >= DEBUG_LEVEL_ERROR) {
|
|
debug_log(DEBUG_LEVEL_ERROR, NULL, 0, "%s", message);
|
|
}
|
|
}
|
|
|
|
void log_warning(const char* message) {
|
|
if (g_debug_level >= DEBUG_LEVEL_WARN) {
|
|
debug_log(DEBUG_LEVEL_WARN, NULL, 0, "%s", message);
|
|
}
|
|
}
|
|
|
|
void log_success(const char* message) {
|
|
if (g_debug_level >= DEBUG_LEVEL_INFO) {
|
|
debug_log(DEBUG_LEVEL_INFO, NULL, 0, "✓ %s", message);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Gradual Migration
|
|
|
|
Gradually replace log calls with debug macros:
|
|
|
|
```c
|
|
// Before:
|
|
log_info("Starting WebSocket relay server");
|
|
|
|
// After:
|
|
DEBUG_INFO("Starting WebSocket relay server");
|
|
|
|
// Before:
|
|
log_error("Failed to initialize database");
|
|
|
|
// After:
|
|
DEBUG_ERROR("Failed to initialize database");
|
|
```
|
|
|
|
### Add New Debug Levels
|
|
|
|
Add debug and trace messages where needed:
|
|
|
|
```c
|
|
// Detailed debugging
|
|
DEBUG_LOG("Processing subscription: %s", sub_id);
|
|
DEBUG_LOG("Filter count: %d", filter_count);
|
|
|
|
// Very verbose tracing
|
|
DEBUG_TRACE("Entering handle_req_message()");
|
|
DEBUG_TRACE("Subscription ID validated: %s", sub_id);
|
|
DEBUG_TRACE("Exiting handle_req_message()");
|
|
```
|
|
## Manual Guards for Expensive Operations
|
|
|
|
### The Problem
|
|
|
|
Debug macros use **runtime checks**, which means function arguments are always evaluated:
|
|
|
|
```c
|
|
// ❌ BAD: Database query executes even when debug level is 0
|
|
DEBUG_LOG("Count: %d", expensive_database_query());
|
|
```
|
|
|
|
The `expensive_database_query()` will **always execute** because function arguments are evaluated before the `if` check inside the macro.
|
|
|
|
### The Solution: Manual Guards
|
|
|
|
For expensive operations (database queries, file I/O, complex calculations), use manual guards:
|
|
|
|
```c
|
|
// ✅ GOOD: Query only executes when debugging is enabled
|
|
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
|
|
int count = expensive_database_query();
|
|
DEBUG_LOG("Count: %d", count);
|
|
}
|
|
```
|
|
|
|
### Standardized Comment Format
|
|
|
|
To make temporary debug guards easy to find and remove, use this standardized format:
|
|
|
|
```c
|
|
// DEBUG_GUARD_START
|
|
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
|
|
// Expensive operation here
|
|
sqlite3_stmt* stmt;
|
|
const char* sql = "SELECT COUNT(*) FROM events";
|
|
int count = 0;
|
|
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) == SQLITE_OK) {
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
count = sqlite3_column_int(stmt, 0);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
DEBUG_LOG("Event count: %d", count);
|
|
}
|
|
// DEBUG_GUARD_END
|
|
```
|
|
|
|
### Easy Removal
|
|
|
|
When you're done debugging, find and remove all temporary guards:
|
|
|
|
```bash
|
|
# Find all debug guards
|
|
grep -n "DEBUG_GUARD_START" src/*.c
|
|
|
|
# Remove guards with sed (between START and END markers)
|
|
sed -i '/DEBUG_GUARD_START/,/DEBUG_GUARD_END/d' src/config.c
|
|
```
|
|
|
|
Or use a simple script:
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# remove_debug_guards.sh
|
|
for file in src/*.c; do
|
|
sed -i '/DEBUG_GUARD_START/,/DEBUG_GUARD_END/d' "$file"
|
|
echo "Removed debug guards from $file"
|
|
done
|
|
```
|
|
|
|
### When to Use Manual Guards
|
|
|
|
Use manual guards for:
|
|
- ✅ Database queries
|
|
- ✅ File I/O operations
|
|
- ✅ Network requests
|
|
- ✅ Complex calculations
|
|
- ✅ Memory allocations for debug data
|
|
- ✅ String formatting with multiple operations
|
|
|
|
Don't need guards for:
|
|
- ❌ Simple variable access
|
|
- ❌ Basic arithmetic
|
|
- ❌ String literals
|
|
- ❌ Function calls that are already cheap
|
|
|
|
### Example: Database Query Guard
|
|
|
|
```c
|
|
// DEBUG_GUARD_START
|
|
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
|
|
sqlite3_stmt* count_stmt;
|
|
const char* count_sql = "SELECT COUNT(*) FROM config";
|
|
int config_count = 0;
|
|
|
|
if (sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL) == SQLITE_OK) {
|
|
if (sqlite3_step(count_stmt) == SQLITE_ROW) {
|
|
config_count = sqlite3_column_int(count_stmt, 0);
|
|
}
|
|
sqlite3_finalize(count_stmt);
|
|
}
|
|
|
|
DEBUG_LOG("Config table has %d rows", config_count);
|
|
}
|
|
// DEBUG_GUARD_END
|
|
```
|
|
|
|
### Example: Complex String Formatting Guard
|
|
|
|
```c
|
|
// DEBUG_GUARD_START
|
|
if (g_debug_level >= DEBUG_LEVEL_TRACE) {
|
|
char filter_str[1024] = {0};
|
|
int offset = 0;
|
|
|
|
for (int i = 0; i < filter_count && offset < sizeof(filter_str) - 1; i++) {
|
|
offset += snprintf(filter_str + offset, sizeof(filter_str) - offset,
|
|
"Filter %d: kind=%d, author=%s; ",
|
|
i, filters[i].kind, filters[i].author);
|
|
}
|
|
|
|
DEBUG_TRACE("Processing filters: %s", filter_str);
|
|
}
|
|
// DEBUG_GUARD_END
|
|
```
|
|
|
|
### Alternative: Compile-Time Guards
|
|
|
|
For permanent debug code that should be completely removed in production builds, use compile-time guards:
|
|
|
|
```c
|
|
#ifdef ENABLE_DEBUG_CODE
|
|
// This code is completely removed when ENABLE_DEBUG_CODE is not defined
|
|
int count = expensive_database_query();
|
|
DEBUG_LOG("Count: %d", count);
|
|
#endif
|
|
```
|
|
|
|
Build with debug code:
|
|
```bash
|
|
make CFLAGS="-DENABLE_DEBUG_CODE"
|
|
```
|
|
|
|
Build without debug code (production):
|
|
```bash
|
|
make # No debug code compiled in
|
|
```
|
|
|
|
### Best Practices
|
|
|
|
1. **Always use standardized markers** (`DEBUG_GUARD_START`/`DEBUG_GUARD_END`) for temporary guards
|
|
2. **Add a comment** explaining what you're debugging
|
|
3. **Remove guards** when debugging is complete
|
|
4. **Use compile-time guards** for permanent debug infrastructure
|
|
5. **Keep guards simple** - one guard per logical debug operation
|
|
|
|
|
|
## Performance Impact
|
|
|
|
### Runtime Check
|
|
|
|
The macros include a runtime check:
|
|
|
|
```c
|
|
#define DEBUG_INFO(...) \
|
|
do { if (g_debug_level >= DEBUG_LEVEL_INFO) debug_log(DEBUG_LEVEL_INFO, NULL, 0, __VA_ARGS__); } while(0)
|
|
```
|
|
|
|
**Cost**: One integer comparison per debug statement (~1 CPU cycle)
|
|
|
|
**Impact**: Negligible - the comparison is faster than a function call
|
|
|
|
**Note**: Only `DEBUG_TRACE` includes `__FILE__` and `__LINE__`, which are compile-time constants with no runtime overhead.
|
|
|
|
### When Debug Level is 0 (Production)
|
|
|
|
```c
|
|
// With g_debug_level = 0:
|
|
DEBUG_INFO("Starting server");
|
|
|
|
// Becomes:
|
|
if (0 >= 3) debug_log(...); // Never executes
|
|
|
|
// Compiler optimizes to:
|
|
// (nothing - branch is eliminated)
|
|
```
|
|
|
|
**Result**: Modern compilers (gcc -O2 or higher) will completely eliminate the dead code branch.
|
|
|
|
### Size Impact
|
|
|
|
**Test Case**: 100 debug statements in code
|
|
|
|
**Without optimization** (`-O0`):
|
|
- Binary size increase: ~2KB (branch instructions)
|
|
- Runtime cost: 100 comparisons per execution
|
|
|
|
**With optimization** (`-O2` or `-O3`):
|
|
- Binary size increase: **0 bytes** (dead code eliminated when g_debug_level = 0)
|
|
- Runtime cost: **0 cycles** (branches removed by compiler)
|
|
|
|
### Verification
|
|
|
|
You can verify the optimization with:
|
|
|
|
```bash
|
|
# Compile with optimization
|
|
gcc -O2 -c debug_test.c -o debug_test.o
|
|
|
|
# Disassemble and check
|
|
objdump -d debug_test.o | grep -A 10 "debug_log"
|
|
```
|
|
|
|
When `g_debug_level = 0` (constant), you'll see the compiler has removed all debug calls.
|
|
|
|
## Example Output
|
|
|
|
### Level 0 (Production)
|
|
```
|
|
(no output)
|
|
```
|
|
|
|
### Level 1 (Errors Only)
|
|
```
|
|
[2025-01-12 14:30:15] [ERROR] Failed to open database: permission denied
|
|
[2025-01-12 14:30:20] [ERROR] WebSocket connection failed: port in use
|
|
```
|
|
|
|
### Level 2 (Errors + Warnings)
|
|
```
|
|
[2025-01-12 14:30:15] [ERROR] Failed to open database: permission denied
|
|
[2025-01-12 14:30:16] [WARN ] Port 8888 unavailable, trying 8889
|
|
[2025-01-12 14:30:17] [WARN ] Configuration key 'relay_name' not found, using default
|
|
```
|
|
|
|
### Level 3 (Errors + Warnings + Info)
|
|
```
|
|
[2025-01-12 14:30:15] [INFO ] Initializing C-Relay v0.4.6
|
|
[2025-01-12 14:30:15] [INFO ] Loading configuration from database
|
|
[2025-01-12 14:30:15] [ERROR] Failed to open database: permission denied
|
|
[2025-01-12 14:30:16] [WARN ] Port 8888 unavailable, trying 8889
|
|
[2025-01-12 14:30:17] [INFO ] WebSocket relay started on ws://127.0.0.1:8889
|
|
```
|
|
|
|
### Level 4 (All Debug Messages)
|
|
```
|
|
[2025-01-12 14:30:15] [INFO ] Initializing C-Relay v0.4.6
|
|
[2025-01-12 14:30:15] [DEBUG] Opening database: build/abc123...def.db
|
|
[2025-01-12 14:30:15] [DEBUG] Executing schema initialization
|
|
[2025-01-12 14:30:15] [INFO ] SQLite WAL mode enabled
|
|
[2025-01-12 14:30:16] [DEBUG] Attempting to bind to port 8888
|
|
[2025-01-12 14:30:16] [WARN ] Port 8888 unavailable, trying 8889
|
|
[2025-01-12 14:30:17] [DEBUG] Successfully bound to port 8889
|
|
[2025-01-12 14:30:17] [INFO ] WebSocket relay started on ws://127.0.0.1:8889
|
|
```
|
|
|
|
### Level 5 (Everything Including file:line for ALL messages)
|
|
```
|
|
[2025-01-12 14:30:15] [INFO ] [main.c:1607] Initializing C-Relay v0.4.6
|
|
[2025-01-12 14:30:15] [DEBUG] [main.c:348] Opening database: build/abc123...def.db
|
|
[2025-01-12 14:30:15] [TRACE] [main.c:330] Entering init_database()
|
|
[2025-01-12 14:30:15] [ERROR] [config.c:125] Database locked
|
|
```
|
|
|
|
## Implementation Steps
|
|
|
|
### Step 1: Create Files (5 minutes)
|
|
|
|
1. Create `src/debug.h` with the header code above
|
|
2. Create `src/debug.c` with the implementation code above
|
|
3. Update `Makefile` to include `src/debug.c` in `MAIN_SRC`
|
|
|
|
### Step 2: Add CLI Parsing (5 minutes)
|
|
|
|
Add `--debug-level` argument parsing to `main()` in `src/main.c`
|
|
|
|
### Step 3: Update Existing Functions (5 minutes)
|
|
|
|
Update the existing `log_*` functions to use the new debug macros
|
|
|
|
### Step 4: Test (5 minutes)
|
|
|
|
```bash
|
|
# Build
|
|
make clean && make
|
|
|
|
# Test different levels
|
|
./build/c_relay_x86 # No output
|
|
./build/c_relay_x86 --debug-level=1 # Errors only
|
|
./build/c_relay_x86 --debug-level=3 # Info + warnings + errors
|
|
./build/c_relay_x86 --debug-level=4 # All debug messages
|
|
./build/c_relay_x86 --debug-level=5 # Everything with file:line on TRACE
|
|
```
|
|
|
|
### Step 5: Gradual Migration (Ongoing)
|
|
|
|
As you work on different parts of the code, replace `log_*` calls with `DEBUG_*` macros and add new debug/trace statements where helpful.
|
|
|
|
## Benefits
|
|
|
|
✅ **Simple**: Single flag, 6 levels, easy to understand
|
|
✅ **Zero Overhead**: Compiler optimizes away unused debug code
|
|
✅ **Zero Size Impact**: No binary size increase in production
|
|
✅ **Backward Compatible**: Existing `log_*` functions still work
|
|
✅ **Easy Migration**: Gradual replacement of log calls
|
|
✅ **Flexible**: Can add detailed debugging without affecting production
|
|
|
|
## Total Implementation Time
|
|
|
|
**~20 minutes** for basic implementation
|
|
**Ongoing** for gradual migration of existing log calls
|
|
|
|
## Recommendation
|
|
|
|
This is the simplest possible debug system that provides:
|
|
- Multiple debug levels for different verbosity
|
|
- Zero performance impact in production
|
|
- Zero binary size increase
|
|
- Easy to use and understand
|
|
- Backward compatible with existing code
|
|
|
|
Start with the basic implementation, test it, then gradually migrate existing log calls and add new debug statements as needed. |