Module: Browserctl::Commands::Trace
- Defined in:
- lib/browserctl/commands/trace.rb
Overview
‘browserctl trace [<session>] [–no-redact]` — pretty timeline of structured log events across cli.log + daemon.log. Defaults to most recent session.
Loose categorisation by inspecting common keys (event/snapshot/request/ error). No schema is enforced — this command is tolerant of any JSONL produced by Browserctl::JsonlFormatter.
Redaction: ON by default. Secret values are sourced from current ENV patterns (‘*_TOKEN`, `*_KEY`, `*_SECRET`, `*_PASSWORD`) and any values captured by `SecretResolverRegistry` during this process. Pass `–no-redact` to disable (local debugging only). Note: when replaying historical traces from a previous process, registry-captured values are gone — only current ENV patterns apply.
Constant Summary collapse
- USAGE =
"Usage: browserctl trace [<session>] [--no-redact]"- NO_REDACT_WARNING =
"[browserctl] traces include unredacted secret values; " \ "do not paste this output publicly."
- LEVEL_COLORS =
{ "DEBUG" => "\e[2;37m", # dim grey "INFO" => "\e[36m", # cyan "WARN" => "\e[33m", # yellow "ERROR" => "\e[31m" # red }.freeze
- RESET =
red
"\e[0m"- CATEGORY_ICONS =
{ error: "!", snapshot: "S", network: "N", event: "." }.freeze
- OMIT_KEYS =
%w[ts level component event msg].freeze
Class Method Summary collapse
- .build_redactor ⇒ Object
- .categorise(record) ⇒ Object
- .colourise(line, level) ⇒ Object
-
.context_snippet(record) ⇒ Object
Compact “k=v k=v” snippet of remaining structured keys, capped to keep the timeline scannable.
- .event_label(record) ⇒ Object
-
.filter_session(records, session_filter) ⇒ Object
Session resolution.
- .format_line(record, tty:, redactor: nil) ⇒ Object
- .format_ts(timestamp) ⇒ Object
- .format_value(value) ⇒ Object
- .load_records(log_dir) ⇒ Object
- .parse_line(line) ⇒ Object
- .render(records, out:, redactor: nil) ⇒ Object
- .run(args, log_dir: Browserctl.log_dir, out: $stdout, err: $stderr) ⇒ Object
- .warn_no_redact(err) ⇒ Object
Class Method Details
.build_redactor ⇒ Object
75 76 77 78 79 80 81 82 83 84 |
# File 'lib/browserctl/commands/trace.rb', line 75 def self.build_redactor extra = if defined?(Browserctl::SecretResolverRegistry) Browserctl::SecretResolverRegistry.resolved_values else [] end Browserctl::Redactor.from_env(extra: extra) rescue StandardError Browserctl::Redactor.new(secrets: []) end |
.categorise(record) ⇒ Object
150 151 152 153 154 155 156 |
# File 'lib/browserctl/commands/trace.rb', line 150 def self.categorise(record) return :error if record["level"] == "ERROR" || record["error"] return :snapshot if record["snapshot"] return :network if record["request"] || record["response"] || record["url"] :event end |
.colourise(line, level) ⇒ Object
181 182 183 184 |
# File 'lib/browserctl/commands/trace.rb', line 181 def self.colourise(line, level) colour = LEVEL_COLORS[level] || "" "#{colour}#{line}#{RESET}" end |
.context_snippet(record) ⇒ Object
Compact “k=v k=v” snippet of remaining structured keys, capped to keep the timeline scannable. Skips fields already shown in fixed columns.
165 166 167 168 169 170 |
# File 'lib/browserctl/commands/trace.rb', line 165 def self.context_snippet(record) pairs = record.except(*OMIT_KEYS) return "" if pairs.empty? pairs.map { |k, v| "#{k}=#{format_value(v)}" }.join(" ").slice(0, 120) end |
.event_label(record) ⇒ Object
158 159 160 161 |
# File 'lib/browserctl/commands/trace.rb', line 158 def self.event_label(record) (record["event"] || record["snapshot"] || record["request"] || record["msg"] || "-").to_s.slice(0, 22) end |
.filter_session(records, session_filter) ⇒ Object
Session resolution. When session_id is stamped on records (future PR), filter/select by it. Otherwise, treat the entire merged stream as one session — caller can scope by tailing/rotating logs. TODO: stamp session_id on every log line so this scopes correctly.
111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/browserctl/commands/trace.rb', line 111 def self.filter_session(records, session_filter) if session_filter records.select { |r| r["session_id"].to_s == session_filter } else ids = records.map { |r| r["session_id"] }.compact.uniq if ids.empty? records else recent = ids.last records.select { |r| r["session_id"] == recent } end end end |
.format_line(record, tty:, redactor: nil) ⇒ Object
130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/browserctl/commands/trace.rb', line 130 def self.format_line(record, tty:, redactor: nil) level = (record["level"] || "INFO").to_s line = format("%-12<ts>s %<icon>s %-5<level>s %-7<comp>s %-22<label>s %<ctx>s", ts: format_ts(record["ts"]), icon: CATEGORY_ICONS.fetch(categorise(record), "."), level: level, comp: (record["component"] || "?").to_s, label: event_label(record), ctx: context_snippet(record)).rstrip line = redactor.redact(line) if redactor tty ? colourise(line, level) : line end |
.format_ts(timestamp) ⇒ Object
144 145 146 147 148 |
# File 'lib/browserctl/commands/trace.rb', line 144 def self.format_ts() Time.iso8601(.to_s).strftime("%H:%M:%S.%L") rescue ArgumentError, TypeError "??:??:??.???" end |
.format_value(value) ⇒ Object
172 173 174 175 176 177 178 179 |
# File 'lib/browserctl/commands/trace.rb', line 172 def self.format_value(value) case value when String then value.length > 40 ? "#{value[0, 37]}..." : value when Array then "[#{value.length}]" when Hash then "{#{value.keys.length}}" else value.to_s end end |
.load_records(log_dir) ⇒ Object
90 91 92 93 94 95 96 |
# File 'lib/browserctl/commands/trace.rb', line 90 def self.load_records(log_dir) paths = Dir.glob(File.join(log_dir, "{cli,daemon}.log")) records = paths.flat_map do |path| File.foreach(path).filter_map { |line| parse_line(line) } end records.sort_by { |r| r["ts"].to_s } end |
.parse_line(line) ⇒ Object
98 99 100 101 102 103 104 105 |
# File 'lib/browserctl/commands/trace.rb', line 98 def self.parse_line(line) line = line.strip return nil if line.empty? JSON.parse(line) rescue JSON::ParserError nil end |
.render(records, out:, redactor: nil) ⇒ Object
125 126 127 128 |
# File 'lib/browserctl/commands/trace.rb', line 125 def self.render(records, out:, redactor: nil) tty = out.respond_to?(:tty?) && out.tty? records.each { |r| out.puts(format_line(r, tty: tty, redactor: redactor)) } end |
.run(args, log_dir: Browserctl.log_dir, out: $stdout, err: $stderr) ⇒ Object
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/browserctl/commands/trace.rb', line 47 def self.run(args, log_dir: Browserctl.log_dir, out: $stdout, err: $stderr) abort USAGE if args.include?("-h") || args.include?("--help") args = args.dup redact = !args.delete("--no-redact") session_filter = args.shift if redact redactor = build_redactor else redactor = nil warn_no_redact(err) end records = load_records(log_dir) if records.empty? out.puts "No log entries found in #{log_dir}" return end records = filter_session(records, session_filter) if records.empty? out.puts "No entries match session=#{session_filter}" return end render(records, out: out, redactor: redactor) end |
.warn_no_redact(err) ⇒ Object
86 87 88 |
# File 'lib/browserctl/commands/trace.rb', line 86 def self.warn_no_redact(err) err&.puts NO_REDACT_WARNING end |