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:
Mahmoud-Emad
2025-09-01 19:48:15 +03:00
parent 6fbca85d0c
commit 52a1d2f80d
9 changed files with 301 additions and 7 deletions

17
examples/core/logger/logger.vsh Executable file
View 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
)!

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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.

View 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
}

View File

@@ -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.') {

View File

@@ -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'
}

View File

@@ -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
}