logviewer 2.1.0 → 2.2.1

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: 37b1f60d724296d87e4a39a682f4403d066e62903d2b4cb6e4e96a1403b452b1
4
+ data.tar.gz: 2bde00db4ffbb7f8db5e27501e9d1d29d56a54d89da61139106d22c0ff600b72
5
5
  SHA512:
6
- metadata.gz: bbb6b8f5cbbf1ec4bb61a33dc342e71eb4833c0f9a058d1f5bedc63a7f4d1c7cdf298967dc98b96054d69347af7307a92d957a7167c8dad1ca57fcd7cbe859ee
7
- data.tar.gz: 8ac8b3b96a0aaf5bd2a418b014275cd1405402656891cfc1d4f74aa816356d6974e89bc6a381d10077a115b44c5d26b6af15368e62f7f5d6be53a804882aa9ba
6
+ metadata.gz: 3d719a9c56cf246d6ab8c67f8e26f307582f2c223710f3b5ca7a6c9154d2d22d23a0b1ee047f88e174ce8a9dbfaa796ef1e7e74d3d87de6db6e5d3169cf00bbb
7
+ data.tar.gz: 9cb4fc5aae64d63a48cfe9f3781ca21d92095310fcc2a1bde77fab7e9675c41eb93e2a9cee4f29e0882f71c8837380bd3e4ca87dcf87db92c4d0e02f178f63f1
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.1"
3
+ end
data/lib/logviewer.rb CHANGED
@@ -1,7 +1,8 @@
1
- require 'json'
1
+ 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,80 @@ 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
- originalText[1] = visibleCount + ' entries';
368
- originalText[2] = 'Level: ' + selectedLevel.toUpperCase() + '+';
369
- header.textContent = originalText.join(' ');
398
+ const headerParts = [
399
+ originalText[0], // filename
400
+ visibleCount + ' entries',
401
+ 'Level: ' + selectedLevel.toUpperCase() + '+'
402
+ ];
403
+
404
+ if (selectedTags.length > 0 && selectedTags.length < tagFilter.options.length) {
405
+ headerParts.push('Tags: ' + selectedTags.length + ' selected');
406
+ }
407
+
408
+ header.textContent = headerParts.join(' • ');
370
409
  }
371
-
372
- levelFilter.addEventListener('change', filterByLevel);
373
-
410
+
411
+ selectAllTagsBtn.addEventListener('click', function() {
412
+ for (let option of tagFilter.options) {
413
+ option.selected = true;
414
+ }
415
+ applyFilters();
416
+ });
417
+
418
+ clearAllTagsBtn.addEventListener('click', function() {
419
+ for (let option of tagFilter.options) {
420
+ option.selected = false;
421
+ }
422
+ applyFilters();
423
+ });
424
+
425
+ levelFilter.addEventListener('change', applyFilters);
426
+ tagFilter.addEventListener('change', applyFilters);
427
+
374
428
  // Apply initial filter
375
- filterByLevel();
429
+ applyFilters();
376
430
  </script>
377
431
  </body>
378
432
  </html>
@@ -383,35 +437,35 @@ module LogViewer
383
437
 
384
438
  def run
385
439
  parse_options
386
-
440
+
387
441
  puts "Parsing log file: #{@input_file}"
388
442
  puts "Minimum log level: #{@min_level}"
389
-
443
+
390
444
  logs = parse_logs
391
445
  puts "Found #{logs.length} log entries matching criteria"
392
-
446
+
393
447
  if logs.empty?
394
448
  puts "No log entries found matching the specified criteria."
395
449
  exit 0
396
450
  end
397
-
451
+
398
452
  html_content = generate_html(logs)
399
-
453
+
400
454
  # Use /tmp directory for HTML files
401
455
  tmp_dir = '/tmp'
402
-
456
+
403
457
  # Generate output filename
404
458
  base_name = File.basename(@input_file, '.*')
405
459
  timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
406
460
  output_file = File.join(tmp_dir, "#{base_name}_#{timestamp}.html")
407
-
461
+
408
462
  # Write HTML file
409
463
  File.write(output_file, html_content)
410
464
  puts "HTML file created: #{output_file}"
411
-
465
+
412
466
  # Open in browser
413
467
  system('open', output_file)
414
468
  puts "Opening in browser..."
415
469
  end
416
470
  end
417
- end
471
+ 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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bishop