// cell log - Manage and read log sinks // // Usage: // cell log list List configured sinks // 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). 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 List configured sinks") 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("") 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") } function parse_opt(arg, prefix) { var full = '--' + prefix + '=' if (starts_with(arg, full)) return text(arg, length(full), length(arg)) 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 } function do_list() { var config = load_config() var names = 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 } 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') if (s.type == 'file') log.console(" " + n + ": " + s.type + " -> " + s.path + " [" + ch + "] format=" + fmt + ex + stk) else log.console(" " + n + ": " + s.type + " [" + ch + "] format=" + fmt + ex + stk) }) } function do_add() { var name = null var sink_type = null var path = null var format = null var channels = ["console", "error", "system"] var exclude = null var stack_chs = ["error"] var config = null var val = null var i = 0 if (length(args) < 3) { log.error("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.error("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.error("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], 'stack') if (val) { stack_chs = array(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} if (path) config.sink[name].path = path if (exclude) config.sink[name].exclude = exclude save_config(config) log.console("Added sink: " + name) } function do_remove() { var name = null var config = null if (length(args) < 2) { log.error("Usage: cell log remove ") return } name = args[1] config = load_config() if (!config || !config.sink || !config.sink[name]) { log.error("Sink not found: " + name) return } delete config.sink[name] save_config(config) log.console("Removed sink: " + name) } function do_read() { var name = null var max_lines = 0 var filter_channel = null var since = 0 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]") 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 } } config = load_config() if (!config || !config.sink || !config.sink[name]) { log.error("Sink not found: " + 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)) }) } function do_tail() { var name = null var tail_lines = 10 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]") return } name = args[1] for (i = 2; i < length(args); i++) { val = parse_opt(args[i], 'lines') if (val) { tail_lines = number(val); continue } } config = load_config() if (!config || !config.sink || !config.sink[name]) { log.error("Sink not found: " + name) return } sink = config.sink[name] if (sink.type != 'file') { log.error("Can only tail file sinks") 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) } poll() } // 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] == 'add') { do_add() } else if (args[0] == 'remove') { do_remove() } else if (args[0] == 'read') { do_read() } else if (args[0] == 'tail') { do_tail() } else { log.error("Unknown command: " + args[0]) print_help() } $stop()