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"
|
name:"test|demo|1"
|
||||||
label:"first"
|
label:"first"
|
||||||
cmd:"echo First pane ready"
|
cmd:"echo First pane ready"
|
||||||
|
log:true
|
||||||
|
logpath:"/tmp/logs"
|
||||||
|
logreset:true
|
||||||
|
|
||||||
!!tmux.pane_ensure
|
!!tmux.pane_ensure
|
||||||
name:"test|demo|2"
|
name:"test|demo|2"
|
||||||
label:"second"
|
label:"second"
|
||||||
cmd:"echo Second pane ready"
|
cmd:"echo Second pane ready"
|
||||||
|
log:true
|
||||||
|
logpath:"/tmp/logs"
|
||||||
|
|
||||||
!!tmux.pane_ensure
|
!!tmux.pane_ensure
|
||||||
name:"test|demo|3"
|
name:"test|demo|3"
|
||||||
label:"third"
|
label:"third"
|
||||||
cmd:"echo Third pane ready"
|
cmd:"echo Third pane ready"
|
||||||
|
log:true
|
||||||
|
logpath:"/tmp/logs"
|
||||||
|
|
||||||
!!tmux.pane_ensure
|
!!tmux.pane_ensure
|
||||||
name:"test|demo|4"
|
name:"test|demo|4"
|
||||||
label:"fourth"
|
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 {
|
pub fn (mut session Session) logger() !logger.Logger {
|
||||||
return session.logger_ or {
|
return session.logger_ or {
|
||||||
mut l2 := logger.new('${session.path()!.path}/logs')!
|
mut l2 := logger.new(path: '${session.path()!.path}/logs')!
|
||||||
l2
|
l2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ module logger
|
|||||||
|
|
||||||
import freeflowuniverse.herolib.core.pathlib
|
import freeflowuniverse.herolib.core.pathlib
|
||||||
|
|
||||||
pub fn new(path string) !Logger {
|
// Logger Factory
|
||||||
mut p := pathlib.get_dir(path: path, create: true)!
|
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{
|
return Logger{
|
||||||
path: p
|
path: p
|
||||||
lastlog_time: 0
|
lastlog_time: 0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Logger Module
|
# 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.
|
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.playbook { PlayBook }
|
||||||
import freeflowuniverse.herolib.core.texttools
|
import freeflowuniverse.herolib.core.texttools
|
||||||
import freeflowuniverse.herolib.osal.core as osal
|
import freeflowuniverse.herolib.osal.core as osal
|
||||||
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
|
||||||
pub fn play(mut plbook PlayBook) ! {
|
pub fn play(mut plbook PlayBook) ! {
|
||||||
if !plbook.exists(filter: 'tmux.') {
|
if !plbook.exists(filter: 'tmux.') {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import freeflowuniverse.herolib.osal.core as osal
|
|||||||
import freeflowuniverse.herolib.data.ourtime
|
import freeflowuniverse.herolib.data.ourtime
|
||||||
import freeflowuniverse.herolib.ui.console
|
import freeflowuniverse.herolib.ui.console
|
||||||
import time
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
@[heap]
|
@[heap]
|
||||||
struct Pane {
|
struct Pane {
|
||||||
@@ -16,6 +17,10 @@ pub mut:
|
|||||||
env map[string]string
|
env map[string]string
|
||||||
created_at time.Time
|
created_at time.Time
|
||||||
last_output_offset int // for tracking new logs
|
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 {
|
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
|
// Kill this specific pane with comprehensive process cleanup
|
||||||
pub fn (mut p Pane) kill() ! {
|
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()!
|
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}'
|
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}') }
|
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()
|
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
|
cmd string // command to run in new pane
|
||||||
horizontal bool // true for horizontal split, false for vertical
|
horizontal bool // true for horizontal split, false for vertical
|
||||||
env map[string]string // environment variables
|
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
|
// Split the active pane horizontally or vertically
|
||||||
@@ -272,12 +276,26 @@ pub fn (mut w Window) pane_split(args PaneSplitArgs) !&Pane {
|
|||||||
env: args.env
|
env: args.env
|
||||||
created_at: time.now()
|
created_at: time.now()
|
||||||
last_output_offset: 0
|
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
|
// Add to window's panes and rescan to get current state
|
||||||
w.panes << &new_pane
|
w.panes << &new_pane
|
||||||
w.scan()!
|
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 the new pane reference
|
||||||
return &new_pane
|
return &new_pane
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user