feat: improve tmux_logger with flexible argument parsing

- Add structured argument parsing to `tmux_logger` utility
- Introduce `--no-log` and `--logreset` command-line options
- Enable dynamic log path resolution and pane-specific directories
- Simplify tmux pane logging integration, remove buffer script
- Standardize log category output padding in `categorize_output`
This commit is contained in:
Mahmoud-Emad
2025-09-02 13:47:35 +03:00
parent 40455a8c2e
commit cf8e69041d
5 changed files with 185 additions and 71 deletions

3
.gitignore vendored
View File

@@ -53,4 +53,5 @@ HTTP_REST_MCP_DEMO.md
MCP_HTTP_REST_IMPLEMENTATION_PLAN.md
.roo
.kilocode
.continue
.continue
tmux_logger

View File

@@ -3,21 +3,53 @@ module main
import os
import io
import freeflowuniverse.herolib.core.logger
import freeflowuniverse.herolib.core.texttools
struct Args {
mut:
logpath string
pane_id string
log bool = true
logreset bool
}
fn main() {
if os.args.len < 2 {
eprintln('Usage: tmux_logger <log_path> [pane_id]')
args := parse_args() or {
eprintln('Error: ${err}')
print_usage()
exit(1)
}
log_path := os.args[1]
if !args.log {
// If logging is disabled, just consume stdin and exit
mut reader := io.new_buffered_reader(reader: os.stdin())
for {
reader.read_line() or { break }
}
return
}
mut l := logger.new(path: log_path) or {
// Determine the actual log directory path
log_dir_path := determine_log_path(args) or {
eprintln('Error determining log path: ${err}')
exit(1)
}
// Handle log reset if requested
if args.logreset {
reset_logs(log_dir_path) or {
eprintln('Error resetting logs: ${err}')
exit(1)
}
}
// Create logger - the logger factory expects a directory path
mut l := logger.new(path: log_dir_path) or {
eprintln('Failed to create logger: ${err}')
exit(1)
}
// Read from stdin line by line and log with categorization
// Read from stdin line by line and log immediately with real-time flushing
mut reader := io.new_buffered_reader(reader: os.stdin())
for {
line := reader.read_line() or { break }
@@ -28,6 +60,7 @@ fn main() {
// Detect output type and set appropriate category
category, logtype := categorize_output(line)
// Log immediately - the logger handles its own file operations
l.log(
cat: category
log: line
@@ -36,6 +69,89 @@ fn main() {
eprintln('Failed to log line: ${err}')
continue
}
// Force flush by ensuring the logger writes immediately
// The logger.log() method already handles immediate writing and flushing
}
}
fn parse_args() !Args {
if os.args.len < 2 {
return error('Missing required argument: logpath')
}
mut args := Args{
logpath: os.args[1]
}
// Parse optional pane_id (second positional argument)
if os.args.len >= 3 {
args.pane_id = os.args[2]
}
// Parse optional flags
for i in 3 .. os.args.len {
arg := os.args[i]
if arg == '--no-log' || arg == '--log=false' {
args.log = false
} else if arg == '--logreset' || arg == '--logreset=true' {
args.logreset = true
} else if arg.starts_with('--log=') {
val := arg.all_after('=').to_lower()
args.log = val == 'true' || val == '1' || val == 'yes'
} else if arg.starts_with('--logreset=') {
val := arg.all_after('=').to_lower()
args.logreset = val == 'true' || val == '1' || val == 'yes'
}
}
return args
}
fn determine_log_path(args Args) !string {
mut log_path := args.logpath
// Check if logpath is a directory or file
if os.exists(log_path) && os.is_dir(log_path) {
// It's an existing directory
if args.pane_id == '' {
return error('When logpath is a directory, pane_id must be provided')
}
// Create a subdirectory for this pane
pane_dir := os.join_path(log_path, args.pane_id)
return pane_dir
} else if log_path.contains('.') && !log_path.ends_with('/') {
// It looks like a file path, use parent directory
parent_dir := os.dir(log_path)
return parent_dir
} else {
// It's a directory path (may not exist yet)
if args.pane_id == '' {
return log_path
}
// Create a subdirectory for this pane
pane_dir := os.join_path(log_path, args.pane_id)
return pane_dir
}
}
fn reset_logs(logpath string) ! {
if !os.exists(logpath) {
return
}
if os.is_dir(logpath) {
// Remove all .log files in the directory
files := os.ls(logpath) or { return }
for file in files {
if file.ends_with('.log') {
full_path := os.join_path(logpath, file)
os.rm(full_path) or { eprintln('Warning: Failed to remove ${full_path}: ${err}') }
}
}
} else {
// Remove the specific log file
os.rm(logpath) or { return error('Failed to remove log file ${logpath}: ${err}') }
}
}
@@ -47,21 +163,41 @@ fn categorize_output(line string) (string, logger.LogType) {
|| line_lower.contains('exception') || line_lower.contains('panic')
|| line_lower.starts_with('e ') || line_lower.contains('fatal')
|| line_lower.contains('critical') {
return 'error', logger.LogType.error
return texttools.expand('error', 10, ' '), logger.LogType.error
}
// Warning patterns - use .stdout logtype but warning category
if line_lower.contains('warning') || line_lower.contains('warn:')
|| line_lower.contains('deprecated') {
return 'warning', logger.LogType.stdout
return texttools.expand('warning', 10, ' '), logger.LogType.stdout
}
// Info/debug patterns - use .stdout logtype
if line_lower.contains('info:') || line_lower.contains('debug:')
|| line_lower.starts_with('info ') || line_lower.starts_with('debug ') {
return 'info', logger.LogType.stdout
return texttools.expand('info', 10, ' '), logger.LogType.stdout
}
// Default to stdout category and logtype
return 'stdout', logger.LogType.stdout
return texttools.expand('stdout', 10, ' '), logger.LogType.stdout
}
fn print_usage() {
eprintln('Usage: tmux_logger <logpath> [pane_id] [options]')
eprintln('')
eprintln('Arguments:')
eprintln(' logpath Directory or file path where logs will be stored')
eprintln(' pane_id Optional pane identifier (required if logpath is a directory)')
eprintln('')
eprintln('Options:')
eprintln(' --log=true|false Enable/disable logging (default: true)')
eprintln(' --no-log Disable logging (same as --log=false)')
eprintln(' --logreset=true|false Reset existing logs before starting (default: false)')
eprintln(' --logreset Reset existing logs (same as --logreset=true)')
eprintln('')
eprintln('Examples:')
eprintln(' tmux_logger /tmp/logs pane1')
eprintln(' tmux_logger /tmp/logs/session.log')
eprintln(' tmux_logger /tmp/logs pane1 --logreset')
eprintln(' tmux_logger /tmp/logs pane1 --no-log')
}

View File

@@ -725,6 +725,26 @@ fn play_pane_ensure(mut plbook PlayBook, mut tmux_instance Tmux) ! {
}
}
// Handle logging parameters - enable logging if requested
log_enabled := p.get_default_false('log')
if log_enabled {
logpath := p.get_default('logpath', '')!
logreset := p.get_default_false('logreset')
// Find the target pane for logging
if pane_number > 0 && pane_number <= window.panes.len {
mut target_pane := window.panes[pane_number - 1] // Convert to 0-based index
// Enable logging with automation (binary compilation, directory creation, etc.)
target_pane.logging_enable(
logpath: logpath
logreset: logreset
) or {
console.print_debug('Warning: Failed to enable logging for pane ${name}: ${err}')
}
}
}
action.done = true
}
}

View File

@@ -367,62 +367,23 @@ pub fn (mut p Pane) logging_enable(args PaneLoggingEnableArgs) ! {
}
}
// Use a completely different approach: direct tmux pipe-pane with a buffer-based logger
// This ensures ALL output is captured in real-time without missing anything
buffer_logger_script := "#!/bin/bash
PANE_TARGET=\"${p.window.session.name}:@${p.window.id}.%${p.id}\"
LOG_PATH=\"${log_path}\"
LOGGER_BINARY=\"${logger_binary}\"
BUFFER_FILE=\"/tmp/tmux_pane_${p.id}_buffer.txt\"
// Use the simple and reliable tmux pipe-pane approach with tmux_logger binary
// This is the proven approach that works perfectly
# Create a named pipe for real-time logging
PIPE_FILE=\"/tmp/tmux_pane_${p.id}_pipe\"
mkfifo \"\$PIPE_FILE\" 2>/dev/null || true
// Determine the pane identifier for logging
pane_log_id := 'pane${p.id}'
# Start the logger process that reads from the pipe
\"\$LOGGER_BINARY\" \"\$LOG_PATH\" \"${p.id}\" < \"\$PIPE_FILE\" &
LOGGER_PID=\$!
// Set up tmux pipe-pane to send all output directly to tmux_logger
pipe_cmd := 'tmux pipe-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} -o "${logger_binary} ${log_path} ${pane_log_id}"'
# Function to cleanup on exit
cleanup() {
kill \$LOGGER_PID 2>/dev/null || true
rm -f \"\$PIPE_FILE\" \"\$BUFFER_FILE\"
exit 0
}
trap cleanup EXIT INT TERM
console.print_debug('Starting real-time logging: ${pipe_cmd}')
# Start tmux pipe-pane to send all output to our pipe
tmux pipe-pane -t \"\$PANE_TARGET\" \"cat >> \"\$PIPE_FILE\"\"
# Keep the script running and monitor the pane
while true; do
# Check if pane still exists
if ! tmux list-panes -t \"\$PANE_TARGET\" >/dev/null 2>&1; then
break
fi
sleep 1
done
cleanup
" // Write the buffer logger script
script_path := '/tmp/tmux_buffer_logger_${p.id}.sh'
os.write_file(script_path, buffer_logger_script) or {
return error("Can't create buffer logger script: ${err}")
osal.exec(cmd: pipe_cmd, stdout: false, name: 'tmux_start_pipe_logging') or {
return error("Can't start pipe logging for pane %${p.id}: ${err}")
}
// Make script executable
osal.exec(cmd: 'chmod +x "${script_path}"', stdout: false, name: 'make_script_executable') or {
return error("Can't make script executable: ${err}")
}
// Start the buffer logger script in background
start_cmd := 'nohup "${script_path}" > /dev/null 2>&1 &'
console.print_debug('Starting pane logging with buffer logger: ${start_cmd}')
osal.exec(cmd: start_cmd, stdout: false, name: 'tmux_start_buffer_logger') or {
return error("Can't start buffer logger for pane %${p.id}: ${err}")
}
// Wait a moment for the process to start
time.sleep(500 * time.millisecond)
// Update pane state
p.log_enabled = true
@@ -442,14 +403,12 @@ pub fn (mut p Pane) logging_disable() ! {
cmd := 'tmux pipe-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id}'
osal.exec(cmd: cmd, stdout: false, name: 'tmux_stop_logging', ignore_error: true) or {}
// Kill the buffer logger script process
script_path := '/tmp/tmux_buffer_logger_${p.id}.sh'
kill_cmd := 'pkill -f "${script_path}"'
osal.exec(cmd: kill_cmd, stdout: false, name: 'kill_buffer_logger_script', ignore_error: true) or {}
// Kill the tmux_logger process for this pane
pane_log_id := 'pane${p.id}'
kill_cmd := 'pkill -f "tmux_logger.*${pane_log_id}"'
osal.exec(cmd: kill_cmd, stdout: false, name: 'kill_tmux_logger', ignore_error: true) or {}
// Clean up script and temp files
cleanup_cmd := 'rm -f "${script_path}" "/tmp/tmux_pane_${p.id}_buffer.txt" "/tmp/tmux_pane_${p.id}_pipe"'
osal.exec(cmd: cleanup_cmd, stdout: false, name: 'cleanup_logging_files', ignore_error: true) or {}
// No temp files to clean up with the simple pipe approach
// Update pane state
p.log_enabled = false

View File

@@ -6,16 +6,14 @@ import freeflowuniverse.herolib.schemas.jsonschema { Reference, decode_schemaref
pub fn decode_json_any(data string) !Any {
// mut o:=decode(data)!
return json2.decode[json2.Any](data)!
return json2.decode[Any](data)!
}
pub fn decode_json_string(data string) !string {
mut o := decode(data)!
return json.encode(o)!
return json.encode(o)
}
pub fn decode(data string) !OpenRPC {
// mut object := json.decode[OpenRPC](data) or { return error('Failed to decode json\n=======\n${data}\n===========\n${err}') }
mut object := json.decode(OpenRPC, data) or {