diff --git a/internal/engine.cm b/internal/engine.cm index ce0d25f3..e577fa85 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -900,7 +900,11 @@ function build_sink_routing() { if (!is_array(sink.exclude)) sink.exclude = [] if (is_text(sink.stack)) sink.stack = [sink.stack] if (!is_array(sink.stack)) sink.stack = [] - if (sink.type == "file" && sink.path) ensure_log_dir(sink.path) + if (sink.type == "file" && sink.path) { + ensure_log_dir(sink.path) + if (sink.mode == "overwrite") + fd.slurpwrite(sink.path, stone(_make_blob(""))) + } arrfor(sink.stack, function(ch) { stack_channels[ch] = true }) @@ -936,6 +940,12 @@ function load_log_config() { } } build_sink_routing() + var names = array(log_config.sink) + arrfor(names, function(name) { + var sink = log_config.sink[name] + if (sink.type == "file") + os.print("[log] " + name + " -> " + sink.path + "\n") + }) } function pretty_format(rec) { @@ -1001,6 +1011,7 @@ function sink_excluded(sink, channel) { function dispatch_to_sink(sink, rec) { var line = null + var st = null if (sink_excluded(sink, rec.channel)) return if (sink.type == "console") { if (sink.format == "json") @@ -1013,7 +1024,12 @@ function dispatch_to_sink(sink, rec) { os.print(pretty_format(rec)) } else if (sink.type == "file") { line = json.encode(rec, false) + "\n" - fd.slurpappend(sink.path, stone(blob(line))) + if (sink.max_size) { + st = fd.stat(sink.path) + if (st && st.size > sink.max_size) + fd.slurpwrite(sink.path, stone(_make_blob(""))) + } + fd.slurpappend(sink.path, stone(_make_blob(line))) } } diff --git a/log.ce b/log.ce index 9e06ecc2..f36d9a02 100644 --- a/log.ce +++ b/log.ce @@ -1,18 +1,17 @@ -// cell log - Manage and read log sinks +// cell log - Manage log sink configuration // // Usage: -// cell log list List configured sinks +// cell log list Show sinks and channel routing // cell log channels List channels with status // cell log enable Enable a channel on terminal // cell log disable Disable a channel on terminal // cell log add console [opts] Add a console sink // cell log add file [opts] Add a file sink // cell log remove Remove a sink -// cell log read [opts] Read from a file sink -// cell log tail [--lines=N] Follow a file sink -// -// The --stack option controls which channels capture a stack trace. -// Default: --stack=error (errors always show a stack trace). +// cell log route Route a channel to a sink +// cell log unroute Remove a channel from a sink +// cell log stack Enable stack traces on a channel +// cell log unstack Disable stack traces on a channel var toml = use('toml') var fd = use('fd') @@ -21,9 +20,8 @@ var json = use('json') var log_path = shop_path + '/log.toml' function load_config() { - if (fd.is_file(log_path)) { + if (fd.is_file(log_path)) return toml.decode(text(fd.slurp(log_path))) - } return null } @@ -48,26 +46,24 @@ function print_help() { log.console("Usage: cell log [options]") log.console("") log.console("Commands:") - log.console(" list List configured sinks") + log.console(" list Show sinks and channel routing") log.console(" channels List channels with status") log.console(" enable Enable a channel on terminal") log.console(" disable Disable a channel on terminal") log.console(" add console [opts] Add a console sink") log.console(" add file [opts] Add a file sink") log.console(" remove Remove a sink") - log.console(" read [opts] Read from a file sink") - log.console(" tail [--lines=N] Follow a file sink") + log.console(" route Route a channel to a sink") + log.console(" unroute Remove a channel from a sink") + log.console(" stack Enable stack traces on a channel") + log.console(" unstack 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: console,error,system)") - log.console(" --exclude=ch1,ch2 Channels to exclude (for wildcard sinks)") - log.console(" --stack=ch1,ch2 Channels that capture a stack trace (default: error)") - log.console("") - log.console("Options for read:") - log.console(" --lines=N Show last N lines (default: all)") - log.console(" --channel=X Filter by channel") - log.console(" --since=timestamp Only show entries after timestamp") + 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) { @@ -77,36 +73,85 @@ function parse_opt(arg, prefix) { return null } -function format_entry(entry) { - var aid = text(entry.actor_id, 0, 5) - var src = "" - var ev = null - if (entry.source && entry.source.file) - src = entry.source.file + ":" + text(entry.source.line) - ev = is_text(entry.event) ? entry.event : json.encode(entry.event) - return "[" + aid + "] [" + entry.channel + "] " + src + " " + ev +// 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.") - log.console("Default: console pretty for console/error/system (stack traces on error)") return } + + // Show sinks + log.console("Sinks:") arrfor(names, function(n) { var s = config.sink[n] - var ch = is_array(s.channels) ? text(s.channels, ', ') : '(none)' - var ex = is_array(s.exclude) ? " exclude=" + text(s.exclude, ',') : "" - var stk = is_array(s.stack) ? " stack=" + text(s.stack, ',') : "" 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 + ": " + s.type + " -> " + s.path + " [" + ch + "] format=" + fmt + ex + stk) + log.console(" " + n + ": file -> " + s.path + " format=" + fmt + mode + maxsz) else - log.console(" " + n + ": " + s.type + " [" + ch + "] format=" + fmt + ex + stk) + 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() { @@ -114,14 +159,15 @@ function do_add() { var sink_type = null var path = null var format = null - var channels = ["console", "error", "system"] + var channels = ["*"] var exclude = null - var stack_chs = ["error"] + var mode = null + var max_size = null var config = null var val = null var i = 0 if (length(args) < 3) { - log.error("Usage: cell log add console|file [path] [options]") + log.console("Usage: cell log add console|file [path] [options]") return } name = args[1] @@ -129,7 +175,7 @@ function do_add() { if (sink_type == 'file') { if (length(args) < 4) { - log.error("Usage: cell log add file [options]") + log.console("Usage: cell log add file [options]") return } path = args[3] @@ -139,7 +185,7 @@ function do_add() { format = "pretty" i = 3 } else { - log.error("Unknown sink type: " + sink_type + " (use 'console' or 'file')") + log.console("Unknown sink type: " + sink_type + " (use 'console' or 'file')") return } @@ -150,17 +196,21 @@ function do_add() { if (val) { channels = array(val, ','); continue } val = parse_opt(args[i], 'exclude') if (val) { exclude = array(val, ','); continue } - val = parse_opt(args[i], 'stack') - if (val) { stack_chs = 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, stack: stack_chs} + 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) @@ -170,13 +220,13 @@ function do_remove() { var name = null var config = null if (length(args) < 2) { - log.error("Usage: cell log remove ") + log.console("Usage: cell log remove ") return } name = args[1] config = load_config() if (!config || !config.sink || !config.sink[name]) { - log.error("Sink not found: " + name) + log.console("Sink not found: " + name) return } delete config.sink[name] @@ -184,154 +234,120 @@ function do_remove() { log.console("Removed sink: " + name) } -function do_read() { - var name = null - var max_lines = 0 - var filter_channel = null - var since = 0 +function do_route() { + var channel = null + var sink_name = null var config = null var sink = null - var content = null - var lines = null - var entries = [] - var entry = null - var val = null - var i = 0 - - if (length(args) < 2) { - log.error("Usage: cell log read [options]") + var already = false + if (length(args) < 3) { + log.console("Usage: cell log route ") return } - name = args[1] - - for (i = 2; i < length(args); i++) { - val = parse_opt(args[i], 'lines') - if (val) { max_lines = number(val); continue } - val = parse_opt(args[i], 'channel') - if (val) { filter_channel = val; continue } - val = parse_opt(args[i], 'since') - if (val) { since = number(val); continue } - } - + channel = args[1] + sink_name = args[2] config = load_config() - if (!config || !config.sink || !config.sink[name]) { - log.error("Sink not found: " + name) + if (!config || !config.sink || !config.sink[sink_name]) { + log.console("Sink not found: " + sink_name) return } - sink = config.sink[name] - if (sink.type != 'file') { - log.error("Can only read from file sinks") - return - } - if (!fd.is_file(sink.path)) { - log.console("Log file does not exist yet: " + sink.path) - return - } - - content = text(fd.slurp(sink.path)) - lines = array(content, '\n') - - arrfor(lines, function(line) { - var parse_fn = null - if (length(line) == 0) return - parse_fn = function() { - entry = json.decode(line) - } disruption { - entry = null - } - parse_fn() - if (!entry) return - if (filter_channel && entry.channel != filter_channel) return - if (since > 0 && entry.timestamp < since) return - entries[] = entry - }) - - if (max_lines > 0 && length(entries) > max_lines) - entries = array(entries, length(entries) - max_lines, length(entries)) - - arrfor(entries, function(e) { - log.console(format_entry(e)) + 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_tail() { - var name = null - var tail_lines = 10 +function do_unroute() { + var channel = null + var sink_name = null var config = null var sink = null - var last_size = 0 - var val = null - var i = 0 - - if (length(args) < 2) { - log.error("Usage: cell log tail [--lines=N]") + var found = false + if (length(args) < 3) { + log.console("Usage: cell log unroute ") return } - name = args[1] - - for (i = 2; i < length(args); i++) { - val = parse_opt(args[i], 'lines') - if (val) { tail_lines = number(val); continue } - } - + channel = args[1] + sink_name = args[2] config = load_config() - if (!config || !config.sink || !config.sink[name]) { - log.error("Sink not found: " + name) + if (!config || !config.sink || !config.sink[sink_name]) { + log.console("Sink not found: " + sink_name) return } - sink = config.sink[name] - if (sink.type != 'file') { - log.error("Can only tail file sinks") + 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 ") return } - if (!fd.is_file(sink.path)) - log.console("Waiting for log file: " + sink.path) - - function poll() { - var st = null - var poll_content = null - var poll_lines = null - var start = 0 - var poll_entry = null - var old_line_count = 0 - var idx = 0 - var parse_fn = null - if (!fd.is_file(sink.path)) { - $delay(poll, 1) - return - } - st = fd.stat(sink.path) - if (st.size == last_size) { - $delay(poll, 1) - return - } - - poll_content = text(fd.slurp(sink.path)) - poll_lines = array(poll_content, '\n') - - if (last_size == 0 && length(poll_lines) > tail_lines) { - start = length(poll_lines) - tail_lines - } else if (last_size > 0) { - old_line_count = length(array(text(poll_content, 0, last_size), '\n')) - start = old_line_count - } - - last_size = st.size - for (idx = start; idx < length(poll_lines); idx++) { - if (length(poll_lines[idx]) == 0) continue - parse_fn = function() { - poll_entry = json.decode(poll_lines[idx]) - } disruption { - poll_entry = null - } - parse_fn() - if (!poll_entry) continue - os.print(format_entry(poll_entry) + "\n") - } - $delay(poll, 1) + 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) + } +} - poll() +function do_unstack() { + var channel = null + var config = null + var names = null + if (length(args) < 2) { + log.console("Usage: cell log unstack ") + 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"] @@ -473,12 +489,16 @@ if (length(args) == 0) { do_add() } else if (args[0] == 'remove') { do_remove() -} else if (args[0] == 'read') { - do_read() -} else if (args[0] == 'tail') { - do_tail() +} 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.error("Unknown command: " + args[0]) + log.console("Unknown command: " + args[0]) print_help() }