logviewer 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fac69fee4a6d38227b055f96c78477b9baa3a40cceef07a291b59b184dce88b6
4
- data.tar.gz: 9c4dac6e68e9dad2753d9300a601e7582e4827f17fb35aeccbf231c1c235aa2b
3
+ metadata.gz: 30beebf819f5a18b9bb45348834b44355863b4ed2bb79919bf6f4a41d53197ad
4
+ data.tar.gz: 6bb17ee176a9073103ff445b41d1bc5eb9ee692454d8f0352bfa1244749935d0
5
5
  SHA512:
6
- metadata.gz: bbb6b8f5cbbf1ec4bb61a33dc342e71eb4833c0f9a058d1f5bedc63a7f4d1c7cdf298967dc98b96054d69347af7307a92d957a7167c8dad1ca57fcd7cbe859ee
7
- data.tar.gz: 8ac8b3b96a0aaf5bd2a418b014275cd1405402656891cfc1d4f74aa816356d6974e89bc6a381d10077a115b44c5d26b6af15368e62f7f5d6be53a804882aa9ba
6
+ metadata.gz: 83a9e3b2764aeb230c253f57fde5ea4c9d9f1175f7fdb3cadd8d8bd11a3d79ff412a7eecf0033ba4cccd77c877247034711a0229abe480cf1cbfbe94589bc7d9
7
+ data.tar.gz: f1eb44fce63eca302764c1066e4c17f27d26483e8fd8190aa80e13eb5f44025550140a1976485a15b029ed88d181948eecc9eacab7c18598f1154ead441fac29
data/README.md CHANGED
@@ -11,7 +11,7 @@ A Ruby gem that converts NDJSON log files into a readable HTML format for easy v
11
11
  - Simplified file paths (shows only filename, not full path)
12
12
  - Color-coded log levels for easy identification
13
13
  - Large, readable fonts throughout the interface (18px base size)
14
- - Interactive dynamic filtering by log level in the browser
14
+ - Interactive dynamic filtering by log level and tags in the browser
15
15
  - Dark mode interface with optimized colors for comfortable viewing
16
16
  - Responsive design that works well in any browser
17
17
  - Automatically opens the generated HTML file in your default browser
@@ -107,6 +107,7 @@ The generated HTML file will be saved in `/tmp/` with a timestamp and automatica
107
107
 
108
108
  - A wide, responsive table layout (1800px max width) with columns in order: date, level, tag, file, function, text
109
109
  - Interactive log level filtering dropdown for dynamic filtering in the browser
110
+ - Multi-select tag filter to show only specific subsystem/category combinations
110
111
  - Dark mode theme with comfortable dark backgrounds and light text
111
112
  - Human-readable timestamps (MM/DD HH:MM:SS format)
112
113
  - Color-coded log levels optimized for dark backgrounds
@@ -121,12 +122,23 @@ The generated HTML file will be saved in `/tmp/` with a timestamp and automatica
121
122
  ## Interactive Features
122
123
 
123
124
  Once the HTML file opens in your browser, you can:
124
- - Use the dropdown in the header to dynamically filter log entries by minimum level
125
+
126
+ ### Log Level Filtering
127
+ - Use the level dropdown in the header to dynamically filter log entries by minimum level
125
128
  - Filter changes are applied instantly without page reload
126
- - Entry counts update automatically to show how many entries match the current filter
127
129
  - Command line level controls what entries are included in the HTML file
128
130
  - Browser initially shows debug+ level by default, regardless of command line level
129
- - Browser filtering works within the entries included from the command line
131
+
132
+ ### Tag Filtering
133
+ - Use the multi-select tag filter to choose which subsystem/category combinations to display
134
+ - All tags are selected by default (showing all entries)
135
+ - Use "Select All" and "Clear All" buttons for quick selection changes
136
+ - Tags are built from subsystem/category fields (e.g., "Play/avPlayer", "Database/queue")
137
+
138
+ ### Combined Filtering
139
+ - Log level and tag filters work together - entries must match both criteria to be visible
140
+ - Entry counts update automatically to show how many entries match the current filters
141
+ - Header shows selected filter status including number of tags when filtered
130
142
 
131
143
  ## Development
132
144
 
@@ -1,3 +1,3 @@
1
1
  module LogViewer
2
- VERSION = "2.1.0"
3
- end
2
+ VERSION = "2.2.0"
3
+ end
data/lib/logviewer.rb CHANGED
@@ -2,6 +2,7 @@ require 'json'
2
2
  require 'optparse'
3
3
  require 'fileutils'
4
4
  require 'time'
5
+ require 'set'
5
6
  require_relative 'logviewer/version'
6
7
 
7
8
  module LogViewer
@@ -25,7 +26,7 @@ module LogViewer
25
26
  def parse_options
26
27
  OptionParser.new do |opts|
27
28
  opts.banner = "Usage: logviewer [options] [ndjson_file]"
28
-
29
+
29
30
  opts.on('-l', '--level LEVEL', 'Minimum log level (trace, debug, info, notice, warning, error, critical)') do |level|
30
31
  level = level.downcase
31
32
  if LOG_LEVELS.key?(level)
@@ -36,18 +37,18 @@ module LogViewer
36
37
  exit 1
37
38
  end
38
39
  end
39
-
40
+
40
41
  opts.on('-v', '--version', 'Show version') do
41
42
  puts "logviewer #{LogViewer::VERSION}"
42
43
  exit
43
44
  end
44
-
45
+
45
46
  opts.on('-h', '--help', 'Show this help message') do
46
47
  puts opts
47
48
  exit
48
49
  end
49
50
  end.parse!(@args)
50
-
51
+
51
52
  if @args.empty?
52
53
  @input_file = find_most_recent_ndjson_file
53
54
  if @input_file.nil?
@@ -59,7 +60,7 @@ module LogViewer
59
60
  else
60
61
  @input_file = @args[0]
61
62
  end
62
-
63
+
63
64
  unless File.exist?(@input_file)
64
65
  puts "Error: File not found: #{@input_file}"
65
66
  exit 1
@@ -69,7 +70,7 @@ module LogViewer
69
70
  def find_most_recent_ndjson_file
70
71
  ndjson_files = Dir.glob('*.ndjson')
71
72
  return nil if ndjson_files.empty?
72
-
73
+
73
74
  # Sort by modification time (most recent first) and return the first one
74
75
  ndjson_files.max_by { |file| File.mtime(file) }
75
76
  end
@@ -81,18 +82,20 @@ module LogViewer
81
82
 
82
83
  def parse_logs
83
84
  logs = []
84
-
85
+ @all_tags = Set.new
86
+
85
87
  File.foreach(@input_file) do |line|
86
88
  begin
87
89
  log_entry = JSON.parse(line.strip)
88
-
90
+
89
91
  if should_include_log?(log_entry['levelName'])
90
92
  # Build tag from subsystem/category
91
93
  tag = []
92
94
  tag << log_entry['subsystem'] if log_entry['subsystem']
93
95
  tag << log_entry['category'] if log_entry['category']
94
96
  tag_string = tag.join('/')
95
-
97
+ @all_tags.add(tag_string) unless tag_string.empty?
98
+
96
99
  logs << {
97
100
  timestamp: log_entry['timestamp'] || '',
98
101
  level: log_entry['levelName'] || 'unknown',
@@ -106,7 +109,7 @@ module LogViewer
106
109
  puts "Warning: Skipping invalid JSON line: #{e.message}"
107
110
  end
108
111
  end
109
-
112
+
110
113
  logs
111
114
  end
112
115
 
@@ -133,7 +136,7 @@ module LogViewer
133
136
 
134
137
  def format_timestamp(timestamp)
135
138
  return '' if timestamp.nil? || timestamp == ''
136
-
139
+
137
140
  begin
138
141
  # Convert milliseconds to seconds for Time.at
139
142
  time = Time.at(timestamp / 1000.0)
@@ -284,6 +287,17 @@ module LogViewer
284
287
 
285
288
  html += <<~HTML
286
289
  </select>
290
+
291
+ <div style="margin-top: 10px;">
292
+ <label for="tagFilter" style="color: white; margin-right: 10px;">Filter by tags:</label>
293
+ <select id="tagFilter" multiple style="padding: 5px; font-size: 14px; border-radius: 4px; border: none; background-color: #3a3a3a; color: #f0f0f0; min-height: 100px; width: 300px;">
294
+ #{@all_tags.sort.map { |tag| " <option value=\"#{tag}\" selected>#{tag}</option>" }.join("\n")}
295
+ </select>
296
+ <div style="margin-top: 5px; font-size: 12px; color: #ccc;">
297
+ <button id="selectAllTags" style="padding: 3px 8px; margin-right: 5px; background: #5a5a5a; color: white; border: none; border-radius: 3px; cursor: pointer;">Select All</button>
298
+ <button id="clearAllTags" style="padding: 3px 8px; background: #5a5a5a; color: white; border: none; border-radius: 3px; cursor: pointer;">Clear All</button>
299
+ </div>
300
+ </div>
287
301
  </div>
288
302
  </div>
289
303
  <div class="table-container">
@@ -310,9 +324,9 @@ module LogViewer
310
324
  filename = extract_filename(log[:file])
311
325
  file_content = filename.empty? ? '<span class="empty">-</span>' : filename
312
326
  method_content = log[:method].empty? ? '<span class="empty">-</span>' : log[:method]
313
-
327
+
314
328
  html += <<~HTML
315
- <tr data-level="#{log[:level].downcase}" data-level-num="#{LOG_LEVELS[log[:level].downcase] || 0}">
329
+ <tr data-level="#{log[:level].downcase}" data-level-num="#{LOG_LEVELS[log[:level].downcase] || 0}" data-tag="#{log[:tag]}">
316
330
  <td class="timestamp">#{timestamp_content}</td>
317
331
  <td class="level" style="#{level_style}">#{log[:level]}</td>
318
332
  <td class="tag">#{tag_content}</td>
@@ -328,7 +342,7 @@ module LogViewer
328
342
  </table>
329
343
  </div>
330
344
  </div>
331
-
345
+
332
346
  <script>
333
347
  const LOG_LEVELS = {
334
348
  'trace': 0,
@@ -339,40 +353,75 @@ module LogViewer
339
353
  'error': 5,
340
354
  'critical': 6
341
355
  };
342
-
356
+
343
357
  const levelFilter = document.getElementById('levelFilter');
358
+ const tagFilter = document.getElementById('tagFilter');
359
+ const selectAllTagsBtn = document.getElementById('selectAllTags');
360
+ const clearAllTagsBtn = document.getElementById('clearAllTags');
344
361
  const tableRows = document.querySelectorAll('tbody tr');
345
-
362
+
346
363
  // Set initial filter to debug (default UI filter)
347
364
  levelFilter.value = 'debug';
348
-
349
- function filterByLevel() {
365
+
366
+ function getSelectedTags() {
367
+ const selected = [];
368
+ for (let option of tagFilter.selectedOptions) {
369
+ selected.push(option.value);
370
+ }
371
+ return selected;
372
+ }
373
+
374
+ function applyFilters() {
350
375
  const selectedLevel = levelFilter.value;
351
376
  const selectedLevelNum = LOG_LEVELS[selectedLevel];
377
+ const selectedTags = getSelectedTags();
352
378
  let visibleCount = 0;
353
-
379
+
354
380
  tableRows.forEach(row => {
355
381
  const rowLevelNum = parseInt(row.dataset.levelNum);
356
- if (rowLevelNum >= selectedLevelNum) {
382
+ const rowTag = row.dataset.tag;
383
+
384
+ const levelMatch = rowLevelNum >= selectedLevelNum;
385
+ const tagMatch = selectedTags.length === 0 || selectedTags.includes(rowTag) || rowTag === '';
386
+
387
+ if (levelMatch && tagMatch) {
357
388
  row.style.display = '';
358
389
  visibleCount++;
359
390
  } else {
360
391
  row.style.display = 'none';
361
392
  }
362
393
  });
363
-
394
+
364
395
  // Update the header count
365
396
  const header = document.querySelector('.header p');
366
397
  const originalText = header.textContent.split(' • ');
367
398
  originalText[1] = visibleCount + ' entries';
368
399
  originalText[2] = 'Level: ' + selectedLevel.toUpperCase() + '+';
400
+ if (selectedTags.length > 0 && selectedTags.length < tagFilter.options.length) {
401
+ originalText[2] += ' • Tags: ' + selectedTags.length + ' selected';
402
+ }
369
403
  header.textContent = originalText.join(' • ');
370
404
  }
371
-
372
- levelFilter.addEventListener('change', filterByLevel);
373
-
405
+
406
+ selectAllTagsBtn.addEventListener('click', function() {
407
+ for (let option of tagFilter.options) {
408
+ option.selected = true;
409
+ }
410
+ applyFilters();
411
+ });
412
+
413
+ clearAllTagsBtn.addEventListener('click', function() {
414
+ for (let option of tagFilter.options) {
415
+ option.selected = false;
416
+ }
417
+ applyFilters();
418
+ });
419
+
420
+ levelFilter.addEventListener('change', applyFilters);
421
+ tagFilter.addEventListener('change', applyFilters);
422
+
374
423
  // Apply initial filter
375
- filterByLevel();
424
+ applyFilters();
376
425
  </script>
377
426
  </body>
378
427
  </html>
@@ -383,35 +432,35 @@ module LogViewer
383
432
 
384
433
  def run
385
434
  parse_options
386
-
435
+
387
436
  puts "Parsing log file: #{@input_file}"
388
437
  puts "Minimum log level: #{@min_level}"
389
-
438
+
390
439
  logs = parse_logs
391
440
  puts "Found #{logs.length} log entries matching criteria"
392
-
441
+
393
442
  if logs.empty?
394
443
  puts "No log entries found matching the specified criteria."
395
444
  exit 0
396
445
  end
397
-
446
+
398
447
  html_content = generate_html(logs)
399
-
448
+
400
449
  # Use /tmp directory for HTML files
401
450
  tmp_dir = '/tmp'
402
-
451
+
403
452
  # Generate output filename
404
453
  base_name = File.basename(@input_file, '.*')
405
454
  timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
406
455
  output_file = File.join(tmp_dir, "#{base_name}_#{timestamp}.html")
407
-
456
+
408
457
  # Write HTML file
409
458
  File.write(output_file, html_content)
410
459
  puts "HTML file created: #{output_file}"
411
-
460
+
412
461
  # Open in browser
413
462
  system('open', output_file)
414
463
  puts "Opening in browser..."
415
464
  end
416
465
  end
417
- end
466
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bishop