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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -53,4 +53,5 @@ HTTP_REST_MCP_DEMO.md
|
||||
MCP_HTTP_REST_IMPLEMENTATION_PLAN.md
|
||||
.roo
|
||||
.kilocode
|
||||
.continue
|
||||
.continue
|
||||
tmux_logger
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user