// 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 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 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') 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 [options]") log.console("") log.console("Commands:") 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(" 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: *)") 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 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 file [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 ") 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 ") 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 ") 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 ") 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 ") 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 ") 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 ") 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()