feat: add real-time logging for tmux panes
- Introduce `tmux_logger` app for categorized output - Implement pane logging via `tmux pipe-pane` - Add `log`, `logpath`, `logreset` options to panes - Update `Pane` struct with logging state and cleanup - Refactor `logger.new` to use `LoggerFactoryArgs`
This commit is contained in:
17
examples/core/logger/logger.vsh
Executable file
17
examples/core/logger/logger.vsh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.core.logger
|
||||
|
||||
mut l := logger.new(path: '/tmp/vlogs')!
|
||||
|
||||
l.log(
|
||||
cat: 'system'
|
||||
log: 'System started successfully'
|
||||
logtype: .stdout
|
||||
)!
|
||||
|
||||
l.log(
|
||||
cat: 'system'
|
||||
log: 'Failed to connect\nRetrying in 5 seconds...'
|
||||
logtype: .error
|
||||
)!
|
||||
@@ -16,18 +16,28 @@
|
||||
name:"test|demo|1"
|
||||
label:"first"
|
||||
cmd:"echo First pane ready"
|
||||
log:true
|
||||
logpath:"/tmp/logs"
|
||||
logreset:true
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"test|demo|2"
|
||||
label:"second"
|
||||
cmd:"echo Second pane ready"
|
||||
log:true
|
||||
logpath:"/tmp/logs"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"test|demo|3"
|
||||
label:"third"
|
||||
cmd:"echo Third pane ready"
|
||||
log:true
|
||||
logpath:"/tmp/logs"
|
||||
|
||||
!!tmux.pane_ensure
|
||||
name:"test|demo|4"
|
||||
label:"fourth"
|
||||
cmd:"echo Fourth pane ready"
|
||||
cmd:"echo Fourth pane ready"
|
||||
log:true
|
||||
logpath:"/tmp/logs"
|
||||
logreset:true
|
||||
@@ -4,7 +4,7 @@ import freeflowuniverse.herolib.core.logger
|
||||
|
||||
pub fn (mut session Session) logger() !logger.Logger {
|
||||
return session.logger_ or {
|
||||
mut l2 := logger.new('${session.path()!.path}/logs')!
|
||||
mut l2 := logger.new(path: '${session.path()!.path}/logs')!
|
||||
l2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,14 @@ module logger
|
||||
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
|
||||
pub fn new(path string) !Logger {
|
||||
mut p := pathlib.get_dir(path: path, create: true)!
|
||||
// Logger Factory
|
||||
pub struct LoggerFactoryArgs {
|
||||
pub mut:
|
||||
path string
|
||||
}
|
||||
|
||||
pub fn new(args LoggerFactoryArgs) !Logger {
|
||||
mut p := pathlib.get_dir(path: args.path, create: true)!
|
||||
return Logger{
|
||||
path: p
|
||||
lastlog_time: 0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Logger Module
|
||||
|
||||
A simple logging system that provides structured logging with search capabilities.
|
||||
A simple logging system that provides structured logging with search capabilities.
|
||||
|
||||
Logs are stored in hourly files with a consistent format that makes them both human-readable and machine-parseable.
|
||||
|
||||
|
||||
67
lib/osal/tmux/bin/tmux_logger.v
Normal file
67
lib/osal/tmux/bin/tmux_logger.v
Normal file
@@ -0,0 +1,67 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
import io
|
||||
import freeflowuniverse.herolib.core.logger
|
||||
|
||||
fn main() {
|
||||
if os.args.len < 2 {
|
||||
eprintln('Usage: tmux_logger <log_path> [pane_id]')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
log_path := os.args[1]
|
||||
|
||||
mut l := logger.new(path: log_path) or {
|
||||
eprintln('Failed to create logger: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// Read from stdin line by line and log with categorization
|
||||
mut reader := io.new_buffered_reader(reader: os.stdin())
|
||||
for {
|
||||
line := reader.read_line() or { break }
|
||||
if line.len == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect output type and set appropriate category
|
||||
category, logtype := categorize_output(line)
|
||||
|
||||
l.log(
|
||||
cat: category
|
||||
log: line
|
||||
logtype: logtype
|
||||
) or {
|
||||
eprintln('Failed to log line: ${err}')
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn categorize_output(line string) (string, logger.LogType) {
|
||||
line_lower := line.to_lower().trim_space()
|
||||
|
||||
// Error patterns - use .error logtype
|
||||
if line_lower.contains('error') || line_lower.contains('err:') || line_lower.contains('failed')
|
||||
|| 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Default to stdout category and logtype
|
||||
return 'stdout', logger.LogType.stdout
|
||||
}
|
||||
@@ -3,6 +3,7 @@ module tmux
|
||||
import freeflowuniverse.herolib.core.playbook { PlayBook }
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import freeflowuniverse.herolib.osal.core as osal
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
pub fn play(mut plbook PlayBook) ! {
|
||||
if !plbook.exists(filter: 'tmux.') {
|
||||
|
||||
@@ -4,6 +4,7 @@ import freeflowuniverse.herolib.osal.core as osal
|
||||
import freeflowuniverse.herolib.data.ourtime
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import time
|
||||
import os
|
||||
|
||||
@[heap]
|
||||
struct Pane {
|
||||
@@ -16,6 +17,10 @@ pub mut:
|
||||
env map[string]string
|
||||
created_at time.Time
|
||||
last_output_offset int // for tracking new logs
|
||||
// Logging fields
|
||||
log_enabled bool // whether logging is enabled for this pane
|
||||
log_path string // path where logs are stored
|
||||
logger_pid int // process id of the logger process
|
||||
}
|
||||
|
||||
pub fn (mut p Pane) stats() !ProcessStats {
|
||||
@@ -154,10 +159,17 @@ pub fn (mut p Pane) send_keys(keys string) ! {
|
||||
|
||||
// Kill this specific pane with comprehensive process cleanup
|
||||
pub fn (mut p Pane) kill() ! {
|
||||
// First, kill all processes running in this pane
|
||||
// First, disable logging if enabled
|
||||
if p.log_enabled {
|
||||
p.logging_disable() or {
|
||||
console.print_debug('Warning: Failed to disable logging for pane %${p.id}: ${err}')
|
||||
}
|
||||
}
|
||||
|
||||
// Then, kill all processes running in this pane
|
||||
p.kill_processes()!
|
||||
|
||||
// Then kill the tmux pane itself
|
||||
// Finally, kill the tmux pane itself
|
||||
cmd := 'tmux kill-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id}'
|
||||
osal.execute_silent(cmd) or { return error('Cannot kill pane %${p.id}: ${err}') }
|
||||
}
|
||||
@@ -291,3 +303,166 @@ pub fn (p Pane) get_height() !int {
|
||||
}
|
||||
return res.output.trim_space().int()
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct PaneLoggingEnableArgs {
|
||||
pub mut:
|
||||
logpath string // custom log path, if empty uses default
|
||||
logreset bool // whether to reset/clear existing logs
|
||||
}
|
||||
|
||||
// Enable logging for this pane
|
||||
pub fn (mut p Pane) logging_enable(args PaneLoggingEnableArgs) ! {
|
||||
if p.log_enabled {
|
||||
return error('Logging is already enabled for pane %${p.id}')
|
||||
}
|
||||
|
||||
// Determine log path
|
||||
mut log_path := args.logpath
|
||||
if log_path == '' {
|
||||
// Default path: /tmp/tmux_logs/session/window/pane_id
|
||||
log_path = '/tmp/tmux_logs/${p.window.session.name}/${p.window.name}/pane_${p.id}'
|
||||
}
|
||||
|
||||
// Create log directory if it doesn't exist
|
||||
osal.exec(cmd: 'mkdir -p "${log_path}"', stdout: false, name: 'tmux_create_log_dir') or {
|
||||
return error("Can't create log directory ${log_path}: ${err}")
|
||||
}
|
||||
|
||||
// Reset logs if requested
|
||||
if args.logreset {
|
||||
osal.exec(
|
||||
cmd: 'rm -f "${log_path}"/*.log'
|
||||
stdout: false
|
||||
name: 'tmux_reset_logs'
|
||||
ignore_error: true
|
||||
) or {}
|
||||
}
|
||||
|
||||
// Get the path to tmux_logger binary - use a more reliable path resolution
|
||||
// Find the herolib root by looking for the lib directory
|
||||
mut herolib_root := os.getwd()
|
||||
for {
|
||||
if os.exists('${herolib_root}/lib/osal/tmux/bin/tmux_logger.v') {
|
||||
break
|
||||
}
|
||||
parent := os.dir(herolib_root)
|
||||
if parent == herolib_root {
|
||||
return error('Could not find herolib root directory')
|
||||
}
|
||||
herolib_root = parent
|
||||
}
|
||||
|
||||
logger_binary := '${herolib_root}/lib/osal/tmux/bin/tmux_logger'
|
||||
logger_source := '${herolib_root}/lib/osal/tmux/bin/tmux_logger.v'
|
||||
|
||||
// Check if binary exists, if not try to compile it
|
||||
if !os.exists(logger_binary) {
|
||||
console.print_debug('Compiling tmux_logger binary...')
|
||||
console.print_debug('Source: ${logger_source}')
|
||||
console.print_debug('Binary: ${logger_binary}')
|
||||
compile_cmd := 'v -enable-globals -o "${logger_binary}" "${logger_source}"'
|
||||
osal.exec(cmd: compile_cmd, stdout: false, name: 'tmux_compile_logger') or {
|
||||
return error("Can't compile tmux_logger: ${err}")
|
||||
}
|
||||
}
|
||||
|
||||
// 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\"
|
||||
|
||||
# Create a named pipe for real-time logging
|
||||
PIPE_FILE=\"/tmp/tmux_pane_${p.id}_pipe\"
|
||||
mkfifo \"\$PIPE_FILE\" 2>/dev/null || true
|
||||
|
||||
# Start the logger process that reads from the pipe
|
||||
\"\$LOGGER_BINARY\" \"\$LOG_PATH\" \"${p.id}\" < \"\$PIPE_FILE\" &
|
||||
LOGGER_PID=\$!
|
||||
|
||||
# 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
|
||||
|
||||
# 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}")
|
||||
}
|
||||
|
||||
// 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}")
|
||||
}
|
||||
|
||||
// Update pane state
|
||||
p.log_enabled = true
|
||||
p.log_path = log_path
|
||||
// Note: tmux pipe-pane doesn't return a PID, we'll track it differently if needed
|
||||
|
||||
console.print_debug('Logging enabled for pane %${p.id} -> ${log_path}')
|
||||
}
|
||||
|
||||
// Disable logging for this pane
|
||||
pub fn (mut p Pane) logging_disable() ! {
|
||||
if !p.log_enabled {
|
||||
return error('Logging is not enabled for pane %${p.id}')
|
||||
}
|
||||
|
||||
// Stop pipe-pane (in case it's running)
|
||||
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 {}
|
||||
|
||||
// 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 {}
|
||||
|
||||
// Update pane state
|
||||
p.log_enabled = false
|
||||
p.log_path = ''
|
||||
p.logger_pid = 0
|
||||
|
||||
console.print_debug('Logging disabled for pane %${p.id}')
|
||||
}
|
||||
|
||||
// Get logging status for this pane
|
||||
pub fn (p Pane) logging_status() string {
|
||||
if p.log_enabled {
|
||||
return 'enabled (${p.log_path})'
|
||||
}
|
||||
return 'disabled'
|
||||
}
|
||||
|
||||
@@ -223,6 +223,10 @@ pub mut:
|
||||
cmd string // command to run in new pane
|
||||
horizontal bool // true for horizontal split, false for vertical
|
||||
env map[string]string // environment variables
|
||||
// Logging parameters
|
||||
log bool // enable logging for this pane
|
||||
logreset bool // reset/clear existing logs when enabling
|
||||
logpath string // custom log path, if empty uses default
|
||||
}
|
||||
|
||||
// Split the active pane horizontally or vertically
|
||||
@@ -272,12 +276,26 @@ pub fn (mut w Window) pane_split(args PaneSplitArgs) !&Pane {
|
||||
env: args.env
|
||||
created_at: time.now()
|
||||
last_output_offset: 0
|
||||
// Initialize logging fields
|
||||
log_enabled: false
|
||||
log_path: ''
|
||||
logger_pid: 0
|
||||
}
|
||||
|
||||
// Add to window's panes and rescan to get current state
|
||||
w.panes << &new_pane
|
||||
w.scan()!
|
||||
|
||||
// Enable logging if requested
|
||||
if args.log {
|
||||
new_pane.logging_enable(
|
||||
logpath: args.logpath
|
||||
logreset: args.logreset
|
||||
) or {
|
||||
console.print_debug('Warning: Failed to enable logging for pane %${new_pane.id}: ${err}')
|
||||
}
|
||||
}
|
||||
|
||||
// Return the new pane reference
|
||||
return &new_pane
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user