Files
cell/log.ce
2026-02-25 16:07:39 -06:00

506 lines
14 KiB
Plaintext

// cell log - Manage log sink configuration
//
// Usage:
// cell log list Show sinks and channel routing
// cell log channels List channels with status
// cell log enable <channel> Enable a channel on terminal
// cell log disable <channel> Disable a channel on terminal
// cell log add <name> console [opts] Add a console sink
// cell log add <name> file <path> [opts] Add a file sink
// cell log remove <name> Remove a sink
// cell log route <channel> <sink> Route a channel to a sink
// cell log unroute <channel> <sink> Remove a channel from a sink
// cell log stack <channel> Enable stack traces on a channel
// cell log unstack <channel> Disable stack traces on a channel
var toml = use('toml')
var fd = use('fd')
var json = use('json')
var log_path = shop_path + '/log.toml'
function load_config() {
if (fd.is_file(log_path))
return toml.decode(text(fd.slurp(log_path)))
return null
}
function ensure_dir(path) {
if (fd.is_dir(path)) return
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue
current = current + parts[i] + '/'
if (!fd.is_dir(current)) fd.mkdir(current)
}
}
function save_config(config) {
ensure_dir(shop_path)
fd.slurpwrite(log_path, stone(blob(toml.encode(config))))
}
function print_help() {
log.console("Usage: cell log <command> [options]")
log.console("")
log.console("Commands:")
log.console(" list Show sinks and channel routing")
log.console(" channels List channels with status")
log.console(" enable <channel> Enable a channel on terminal")
log.console(" disable <channel> Disable a channel on terminal")
log.console(" add <name> console [opts] Add a console sink")
log.console(" add <name> file <path> [opts] Add a file sink")
log.console(" remove <name> Remove a sink")
log.console(" route <channel> <sink> Route a channel to a sink")
log.console(" unroute <channel> <sink> Remove a channel from a sink")
log.console(" stack <channel> Enable stack traces on a channel")
log.console(" unstack <channel> Disable stack traces on a channel")
log.console("")
log.console("Options for add:")
log.console(" --format=pretty|bare|json Output format (default: pretty for console, json for file)")
log.console(" --channels=ch1,ch2 Channels to subscribe (default: *)")
log.console(" --exclude=ch1,ch2 Channels to exclude")
log.console(" --mode=append|overwrite File write mode (default: append)")
log.console(" --max_size=N Max file size in bytes before truncation")
}
function parse_opt(arg, prefix) {
var full = '--' + prefix + '='
if (starts_with(arg, full))
return text(arg, length(full), length(arg))
return null
}
// Collect all stack channels across all sinks
function collect_stack_channels(config) {
var stack_chs = {}
var names = array(config.sink)
arrfor(names, function(n) {
var s = config.sink[n]
if (is_array(s.stack)) {
arrfor(s.stack, function(ch) { stack_chs[ch] = true })
}
})
return stack_chs
}
// Find which sinks a stack channel is declared on (for modification)
function find_stack_sink(config, channel) {
var names = array(config.sink)
var found = null
arrfor(names, function(n) {
if (found) return
var s = config.sink[n]
if (is_array(s.stack)) {
arrfor(s.stack, function(ch) {
if (ch == channel) found = n
})
}
})
return found
}
function do_list() {
var config = load_config()
var names = null
var channel_routing = {}
var stack_chs = null
names = (config && config.sink) ? array(config.sink) : []
if (length(names) == 0) {
log.console("No log sinks configured.")
return
}
// Show sinks
log.console("Sinks:")
arrfor(names, function(n) {
var s = config.sink[n]
var fmt = s.format || (s.type == 'file' ? 'json' : 'pretty')
var mode = s.mode ? " mode=" + s.mode : ""
var maxsz = s.max_size ? " max_size=" + text(s.max_size) : ""
var ex = is_array(s.exclude) ? " exclude=" + text(s.exclude, ',') : ""
if (s.type == 'file')
log.console(" " + n + ": file -> " + s.path + " format=" + fmt + mode + maxsz)
else
log.console(" " + n + ": console format=" + fmt + ex)
})
// Build channel -> sinks map
arrfor(names, function(n) {
var s = config.sink[n]
var chs = is_array(s.channels) ? s.channels : []
arrfor(chs, function(ch) {
if (!channel_routing[ch]) channel_routing[ch] = []
channel_routing[ch][] = n
})
})
// Show routing
log.console("")
log.console("Routing:")
var channels = array(channel_routing)
arrfor(channels, function(ch) {
log.console(" " + ch + " -> " + text(channel_routing[ch], ', '))
})
// Show stack traces
stack_chs = collect_stack_channels(config)
var stack_list = array(stack_chs)
if (length(stack_list) > 0) {
log.console("")
log.console("Stack traces on: " + text(stack_list, ', '))
}
}
function do_add() {
var name = null
var sink_type = null
var path = null
var format = null
var channels = ["*"]
var exclude = null
var mode = null
var max_size = null
var config = null
var val = null
var i = 0
if (length(args) < 3) {
log.console("Usage: cell log add <name> console|file [path] [options]")
return
}
name = args[1]
sink_type = args[2]
if (sink_type == 'file') {
if (length(args) < 4) {
log.console("Usage: cell log add <name> file <path> [options]")
return
}
path = args[3]
format = "json"
i = 4
} else if (sink_type == 'console') {
format = "pretty"
i = 3
} else {
log.console("Unknown sink type: " + sink_type + " (use 'console' or 'file')")
return
}
for (i = i; i < length(args); i++) {
val = parse_opt(args[i], 'format')
if (val) { format = val; continue }
val = parse_opt(args[i], 'channels')
if (val) { channels = array(val, ','); continue }
val = parse_opt(args[i], 'exclude')
if (val) { exclude = array(val, ','); continue }
val = parse_opt(args[i], 'mode')
if (val) { mode = val; continue }
val = parse_opt(args[i], 'max_size')
if (val) { max_size = number(val); continue }
}
config = load_config()
if (!config) config = {}
if (!config.sink) config.sink = {}
config.sink[name] = {type: sink_type, format: format, channels: channels}
if (path) config.sink[name].path = path
if (exclude) config.sink[name].exclude = exclude
if (mode) config.sink[name].mode = mode
if (max_size) config.sink[name].max_size = max_size
save_config(config)
log.console("Added sink: " + name)
}
function do_remove() {
var name = null
var config = null
if (length(args) < 2) {
log.console("Usage: cell log remove <name>")
return
}
name = args[1]
config = load_config()
if (!config || !config.sink || !config.sink[name]) {
log.console("Sink not found: " + name)
return
}
delete config.sink[name]
save_config(config)
log.console("Removed sink: " + name)
}
function do_route() {
var channel = null
var sink_name = null
var config = null
var sink = null
var already = false
if (length(args) < 3) {
log.console("Usage: cell log route <channel> <sink>")
return
}
channel = args[1]
sink_name = args[2]
config = load_config()
if (!config || !config.sink || !config.sink[sink_name]) {
log.console("Sink not found: " + sink_name)
return
}
sink = config.sink[sink_name]
if (!is_array(sink.channels)) sink.channels = []
arrfor(sink.channels, function(ch) {
if (ch == channel) already = true
})
if (already) {
log.console(channel + " already routed to " + sink_name)
return
}
sink.channels[] = channel
save_config(config)
log.console(channel + " -> " + sink_name)
}
function do_unroute() {
var channel = null
var sink_name = null
var config = null
var sink = null
var found = false
if (length(args) < 3) {
log.console("Usage: cell log unroute <channel> <sink>")
return
}
channel = args[1]
sink_name = args[2]
config = load_config()
if (!config || !config.sink || !config.sink[sink_name]) {
log.console("Sink not found: " + sink_name)
return
}
sink = config.sink[sink_name]
if (!is_array(sink.channels)) sink.channels = []
sink.channels = filter(sink.channels, function(ch) { return ch != channel })
save_config(config)
log.console(channel + " removed from " + sink_name)
}
function do_stack() {
var channel = null
var config = null
var names = null
var added = false
if (length(args) < 2) {
log.console("Usage: cell log stack <channel>")
return
}
channel = args[1]
config = load_config()
if (!config || !config.sink) {
log.console("No sinks configured")
return
}
// Add to first sink that already has a stack array, or first sink overall
names = array(config.sink)
arrfor(names, function(n) {
var s = config.sink[n]
var already = false
if (added) return
if (is_array(s.stack)) {
arrfor(s.stack, function(ch) { if (ch == channel) already = true })
if (!already) s.stack[] = channel
added = true
}
})
if (!added && length(names) > 0) {
config.sink[names[0]].stack = [channel]
added = true
}
if (added) {
save_config(config)
log.console("Stack traces enabled on: " + channel)
}
}
function do_unstack() {
var channel = null
var config = null
var names = null
if (length(args) < 2) {
log.console("Usage: cell log unstack <channel>")
return
}
channel = args[1]
config = load_config()
if (!config || !config.sink) {
log.console("No sinks configured")
return
}
names = array(config.sink)
arrfor(names, function(n) {
var s = config.sink[n]
if (is_array(s.stack))
s.stack = filter(s.stack, function(ch) { return ch != channel })
})
save_config(config)
log.console("Stack traces disabled on: " + channel)
}
var known_channels = ["console", "error", "warn", "system", "build", "shop", "compile", "test"]
function find_terminal_sink(config) {
var names = null
var found = null
if (!config || !config.sink) return null
names = array(config.sink)
if (config.sink.terminal) return config.sink.terminal
arrfor(names, function(n) {
if (!found && config.sink[n].type == "console")
found = config.sink[n]
})
return found
}
function do_enable() {
var channel = null
var config = null
var sink = null
var i = 0
var already = false
if (length(args) < 2) {
log.error("Usage: cell log enable <channel>")
return
}
channel = args[1]
config = load_config()
if (!config) config = {sink: {}}
if (!config.sink) config.sink = {}
sink = find_terminal_sink(config)
if (!sink) {
config.sink.terminal = {type: "console", format: "clean", channels: ["console", "error", channel], stack: ["error"]}
save_config(config)
log.console("Enabled channel: " + channel)
return
}
if (is_array(sink.channels) && length(sink.channels) == 1 && sink.channels[0] == "*") {
if (is_array(sink.exclude)) {
var new_exclude = []
arrfor(sink.exclude, function(ex) {
if (ex != channel) push(new_exclude, ex)
})
sink.exclude = new_exclude
}
} else {
if (!is_array(sink.channels)) sink.channels = ["console", "error"]
arrfor(sink.channels, function(ch) {
if (ch == channel) already = true
})
if (!already) sink.channels[] = channel
}
save_config(config)
log.console("Enabled channel: " + channel)
}
function do_disable() {
var channel = null
var config = null
var sink = null
var i = 0
var new_channels = []
if (length(args) < 2) {
log.error("Usage: cell log disable <channel>")
return
}
channel = args[1]
config = load_config()
if (!config || !config.sink) {
log.error("No log configuration found")
return
}
sink = find_terminal_sink(config)
if (!sink) {
log.error("No terminal sink found")
return
}
if (is_array(sink.channels) && length(sink.channels) == 1 && sink.channels[0] == "*") {
if (!is_array(sink.exclude)) sink.exclude = []
var already_excluded = false
arrfor(sink.exclude, function(ex) {
if (ex == channel) already_excluded = true
})
if (!already_excluded) sink.exclude[] = channel
} else {
if (is_array(sink.channels)) {
arrfor(sink.channels, function(ch) {
if (ch != channel) push(new_channels, ch)
})
sink.channels = new_channels
}
}
save_config(config)
log.console("Disabled channel: " + channel)
}
function do_channels() {
var config = load_config()
var sink = null
var is_wildcard = false
var active = {}
if (config) sink = find_terminal_sink(config)
if (sink) {
if (is_array(sink.channels) && length(sink.channels) == 1 && sink.channels[0] == "*") {
is_wildcard = true
arrfor(known_channels, function(ch) { active[ch] = true })
if (is_array(sink.exclude)) {
arrfor(sink.exclude, function(ex) { active[ex] = false })
}
} else if (is_array(sink.channels)) {
arrfor(sink.channels, function(ch) { active[ch] = true })
}
} else {
active.console = true
active.error = true
}
log.console("Channels:")
arrfor(known_channels, function(ch) {
var status = active[ch] ? "enabled" : "disabled"
log.console(" " + ch + ": " + status)
})
}
// Main dispatch
if (length(args) == 0) {
print_help()
} else if (args[0] == 'help' || args[0] == '-h' || args[0] == '--help') {
print_help()
} else if (args[0] == 'list') {
do_list()
} else if (args[0] == 'channels') {
do_channels()
} else if (args[0] == 'enable') {
do_enable()
} else if (args[0] == 'disable') {
do_disable()
} else if (args[0] == 'add') {
do_add()
} else if (args[0] == 'remove') {
do_remove()
} else if (args[0] == 'route') {
do_route()
} else if (args[0] == 'unroute') {
do_unroute()
} else if (args[0] == 'stack') {
do_stack()
} else if (args[0] == 'unstack') {
do_unstack()
} else {
log.console("Unknown command: " + args[0])
print_help()
}
$stop()