sumologic-query 1.3.1 → 1.3.2

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: ee3064549af4cc950fcd9ff873bd9f1bb9940b4e6078bc45d9086973fbab1162
4
- data.tar.gz: c16964c3af7afd22e6ebdb878bc00d22cb4b6ab55f6b4a6946c0db435856e128
3
+ metadata.gz: fff9b3ee00ddf6f3adfd2bb84fb8488068149fc8e7bfd94a3aa1b9854f7b74f2
4
+ data.tar.gz: 898d75c6c4ca00c9e78ef21301d1345ec4e2c3806f33a8554b485486f314214a
5
5
  SHA512:
6
- metadata.gz: 2a8c9c6b61000afc3d3ba204215b054332c7efc001bb1fc0a3a349e161b2ac9f9f36b7794ca561b0564b6b3b56245bf8ed9f630a7fe57fdba086ddba4f94f7ce
7
- data.tar.gz: 6466d1a6180686719d614022fc908db2a2a8949594192c41bdf87e1a8c4c3d6d93bf754e4ef00e4479467f15c2eee5b4f240086afc200ef78bfad1786444b427
6
+ metadata.gz: b9d264177ec993228a116d3c17f77cf1ac0b0e16c48d19a3de2b6b6502fcf9d89bfa90dc1046609f5819c18bcccb0a30eeb3c57b7ba00363a94f4e08f2705318
7
+ data.tar.gz: 252af2f57e025938cbe9607eb5ae32b23140d0e84bbc8c8ecaceb10c06110436e37a096c9c6d21480c2408cbfb3f29a5f322e71655622954c5c7fc5881ac959f
data/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@
6
6
 
7
7
 
8
8
 
9
+ ## [1.3.2](https://github.com/patrick204nqh/sumologic-query/compare/v1.3.1...v1.3.2) (2025-11-16)
10
+
11
+ <!-- Release notes generated using configuration in .github/release.yml at main -->
12
+
13
+
14
+
15
+ **Full Changelog**: https://github.com/patrick204nqh/sumologic-query/compare/v1.3.1...v1.3.2
16
+
9
17
  # [1.2.0](https://github.com/patrick204nqh/sumologic-query/compare/v1.1.2...v1.2.0) (2025-11-14)
10
18
 
11
19
 
data/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  > A lightweight Ruby CLI for querying Sumo Logic logs and metadata. Simple, fast, read-only access to your logs.
4
4
 
5
5
  [![Gem Version](https://badge.fury.io/rb/sumologic-query.svg)](https://badge.fury.io/rb/sumologic-query)
6
+ [![Downloads](https://img.shields.io/gem/dt/sumologic-query.svg)](https://rubygems.org/gems/sumologic-query)
6
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
8
 
8
9
  ## Why This Tool?
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sumologic
4
+ module Interactive
5
+ class FzfViewer
6
+ module Config
7
+ # Display configuration
8
+ TIME_WIDTH = 8
9
+ LEVEL_WIDTH = 7
10
+ SOURCE_WIDTH = 25
11
+ MESSAGE_PREVIEW_LENGTH = 80
12
+ SEARCHABLE_PADDING = 5
13
+
14
+ # Searchable field names
15
+ SEARCHABLE_FIELDS = %w[
16
+ _source
17
+ _sourcecategory
18
+ _sourcename
19
+ _collector
20
+ _sourcehost
21
+ region
22
+ _group
23
+ _tier
24
+ _view
25
+ ].freeze
26
+
27
+ # ANSI color codes
28
+ COLORS = {
29
+ red: "\e[31m",
30
+ yellow: "\e[33m",
31
+ cyan: "\e[36m",
32
+ gray: "\e[90m",
33
+ reset: "\e[0m"
34
+ }.freeze
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sumologic
4
+ module Interactive
5
+ class FzfViewer
6
+ module Formatter
7
+ module_function
8
+
9
+ def format_time(timestamp_ms)
10
+ return 'N/A' unless timestamp_ms
11
+
12
+ Time.at(timestamp_ms.to_i / 1000).strftime('%H:%M:%S')
13
+ end
14
+
15
+ def format_level(level)
16
+ level_str = level.to_s.upcase.ljust(Config::LEVEL_WIDTH)
17
+ colorize_level(level_str)
18
+ end
19
+
20
+ def colorize_level(level_str)
21
+ case level_str.strip
22
+ when 'ERROR', 'FATAL', 'CRITICAL'
23
+ "#{Config::COLORS[:red]}#{level_str}#{Config::COLORS[:reset]}"
24
+ when 'WARN', 'WARNING'
25
+ "#{Config::COLORS[:yellow]}#{level_str}#{Config::COLORS[:reset]}"
26
+ when 'INFO'
27
+ "#{Config::COLORS[:cyan]}#{level_str}#{Config::COLORS[:reset]}"
28
+ when 'DEBUG', 'TRACE'
29
+ "#{Config::COLORS[:gray]}#{level_str}#{Config::COLORS[:reset]}"
30
+ else
31
+ level_str
32
+ end
33
+ end
34
+
35
+ def sanitize(text)
36
+ text.to_s.gsub(/[\n\r\t]/, ' ').squeeze(' ')
37
+ end
38
+
39
+ def truncate(text, length)
40
+ text = text.to_s
41
+ text.length > length ? "#{text[0...(length - 3)]}..." : text
42
+ end
43
+
44
+ def pad(text, width)
45
+ text.ljust(width)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+
5
+ module Sumologic
6
+ module Interactive
7
+ class FzfViewer
8
+ module FzfConfig
9
+ module_function
10
+
11
+ def build_fzf_args(input_path, preview_path, header_text)
12
+ [
13
+ 'fzf',
14
+ *search_options,
15
+ *display_options(preview_path, header_text),
16
+ *keybinding_options(input_path, preview_path)
17
+ ]
18
+ end
19
+
20
+ def search_options
21
+ [
22
+ '--ansi',
23
+ '--multi',
24
+ '--exact', # Exact substring matching
25
+ '-i', # Case-insensitive
26
+ '--no-hscroll' # Prevent horizontal scrolling
27
+ ]
28
+ end
29
+
30
+ def display_options(preview_path, header_text)
31
+ [
32
+ "--header=#{header_text}",
33
+ "--preview=#{build_preview_command(preview_path)}",
34
+ '--preview-window=right:60%:wrap:follow',
35
+ '--height=100%'
36
+ ]
37
+ end
38
+
39
+ def keybinding_options(input_path, preview_path)
40
+ [
41
+ '--bind=enter:toggle',
42
+ "--bind=tab:execute(#{build_view_command(preview_path)})",
43
+ '--bind=ctrl-a:select-all',
44
+ '--bind=ctrl-d:deselect-all',
45
+ '--bind=ctrl-s:execute-silent(echo {+} > sumo-selected.txt)+abort',
46
+ '--bind=ctrl-y:execute-silent(echo {+} | pbcopy || ' \
47
+ 'echo {+} | xclip -selection clipboard 2>/dev/null)+abort',
48
+ '--bind=ctrl-e:execute-silent(echo {+} > sumo-export.jsonl)+abort',
49
+ '--bind=ctrl-/:toggle-preview',
50
+ "--bind=ctrl-r:reload(cat #{input_path})",
51
+ '--bind=ctrl-t:toggle-search',
52
+ '--bind=ctrl-q:abort'
53
+ ]
54
+ end
55
+
56
+ def build_view_command(preview_path)
57
+ # FZF {n} is 0-indexed, sed is 1-indexed
58
+ 'LINE=$(({n} + 1)); ' \
59
+ "sed -n \"$LINE\"p #{Shellwords.escape(preview_path)} | jq -C . | less -R"
60
+ end
61
+
62
+ def build_preview_command(preview_path)
63
+ escaped_path = Shellwords.escape(preview_path)
64
+
65
+ calc = "LINE=$(({n} + 1)); TOTAL=$(wc -l < #{escaped_path}); "
66
+ display = 'echo "Message $LINE of $TOTAL"; echo ""; '
67
+ extract = "sed -n \"$LINE\"p #{escaped_path}"
68
+
69
+ "#{calc}#{display}#{extract} | jq -C . || #{extract}"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sumologic
4
+ module Interactive
5
+ class FzfViewer
6
+ module HeaderBuilder
7
+ module_function
8
+
9
+ def build_header_text(results, messages)
10
+ [
11
+ build_column_headers,
12
+ build_info_line(results, messages),
13
+ build_search_tips,
14
+ build_keybindings_help
15
+ ].join("\n")
16
+ end
17
+
18
+ def build_column_headers
19
+ "#{Formatter.pad('TIME', Config::TIME_WIDTH)} " \
20
+ "#{Formatter.pad('LEVEL', Config::LEVEL_WIDTH)} " \
21
+ "#{Formatter.pad('SOURCE', Config::SOURCE_WIDTH)} MESSAGE"
22
+ end
23
+
24
+ def build_info_line(results, messages)
25
+ query = results['query'] || 'N/A'
26
+ count = messages.size
27
+ sources = messages.map { |m| m['map']['_source'] }.compact.uniq.size
28
+
29
+ "#{count} msgs | #{sources} sources | Query: #{Formatter.truncate(query, 40)}"
30
+ end
31
+
32
+ def build_search_tips
33
+ '💡 Simple text search (case-insensitive) - searches all JSON fields and log content'
34
+ end
35
+
36
+ def build_keybindings_help
37
+ 'Enter=select Tab=view Ctrl-T=toggle-search Ctrl-S=save Ctrl-Y=copy Ctrl-E=export Ctrl-Q=quit'
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sumologic
4
+ module Interactive
5
+ class FzfViewer
6
+ module SearchableBuilder
7
+ module_function
8
+
9
+ def build_searchable_content(map)
10
+ parts = []
11
+
12
+ add_primary_content(parts, map)
13
+ add_standard_fields(parts, map)
14
+ add_custom_fields(parts, map)
15
+
16
+ parts.compact.join(' ')
17
+ end
18
+
19
+ def add_primary_content(parts, map)
20
+ parts << Formatter.sanitize(map['_raw'] || map['message'] || '')
21
+ end
22
+
23
+ def add_standard_fields(parts, map)
24
+ Config::SEARCHABLE_FIELDS.each do |field|
25
+ parts << map[field] if map[field]
26
+ end
27
+ end
28
+
29
+ def add_custom_fields(parts, map)
30
+ map.each do |key, value|
31
+ next if key.start_with?('_')
32
+ next if value.nil? || value.to_s.empty?
33
+
34
+ parts << "#{key}:#{value}"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -4,13 +4,15 @@ require 'json'
4
4
  require 'tempfile'
5
5
  require 'time'
6
6
  require 'open3'
7
- require 'shellwords'
7
+ require_relative 'fzf_viewer/config'
8
+ require_relative 'fzf_viewer/formatter'
9
+ require_relative 'fzf_viewer/searchable_builder'
10
+ require_relative 'fzf_viewer/fzf_config'
11
+ require_relative 'fzf_viewer/header_builder'
8
12
 
9
13
  module Sumologic
10
14
  module Interactive
11
15
  class FzfViewer
12
- DELIMITER = '||'
13
-
14
16
  def initialize(results)
15
17
  @results = results
16
18
  @messages = results['messages'] || []
@@ -31,74 +33,56 @@ module Sumologic
31
33
 
32
34
  private
33
35
 
36
+ # ============================================================
37
+ # Data Preparation
38
+ # ============================================================
39
+
34
40
  def prepare_data(input_file, preview_file)
35
- # Write data lines only (no header in file - handled by FZF --header)
41
+ write_input_file(input_file)
42
+ write_preview_file(preview_file)
43
+ end
44
+
45
+ def write_input_file(input_file)
36
46
  File.open(input_file, 'w') do |f|
37
- @messages.each do |msg|
38
- f.puts format_line(msg)
39
- end
47
+ @messages.each { |msg| f.puts format_line(msg) }
40
48
  end
49
+ end
41
50
 
42
- # Write JSONL for preview (one JSON per line, line numbers match input)
51
+ def write_preview_file(preview_file)
43
52
  File.open(preview_file, 'w') do |f|
44
- @messages.each do |msg|
45
- f.puts JSON.generate(msg['map'])
46
- end
53
+ @messages.each { |msg| f.puts JSON.generate(msg['map']) }
47
54
  end
48
55
  end
49
56
 
57
+ # ============================================================
58
+ # Line Formatting
59
+ # ============================================================
60
+
50
61
  def format_line(msg)
51
62
  map = msg['map']
63
+ display = build_display_line(map)
64
+ searchable = SearchableBuilder.build_searchable_content(map)
52
65
 
53
- time = format_time(map['_messagetime'])
54
- level = format_level(map['level'] || map['severity'] || 'INFO')
55
- source = truncate(map['_sourceCategory'] || '-', 25)
56
- message = truncate(sanitize(map['_raw'] || map['message'] || ''), 80)
57
-
58
- # No index in display - use FZF line number instead
59
- "#{time} #{level} #{source.ljust(25)} #{message}"
66
+ "#{display}#{' ' * Config::SEARCHABLE_PADDING}#{searchable}"
60
67
  end
61
68
 
62
- def format_time(timestamp_ms)
63
- return 'N/A' unless timestamp_ms
69
+ def build_display_line(map)
70
+ time = Formatter.format_time(map['_messagetime'])
71
+ level = Formatter.format_level(map['level'] || map['severity'] || 'INFO')
72
+ source = Formatter.truncate(map['_source'] || map['_sourcecategory'] || '-', Config::SOURCE_WIDTH)
73
+ message = Formatter.truncate(Formatter.sanitize(map['_raw'] || map['message'] || ''), Config::MESSAGE_PREVIEW_LENGTH)
64
74
 
65
- Time.at(timestamp_ms.to_i / 1000).strftime('%H:%M:%S')
75
+ "#{time} #{level} #{source.ljust(Config::SOURCE_WIDTH)} #{message}"
66
76
  end
67
77
 
68
- def format_level(level)
69
- level_str = level.to_s.upcase.ljust(7)
70
-
71
- case level_str.strip
72
- when 'ERROR', 'FATAL', 'CRITICAL'
73
- "\e[31m#{level_str}\e[0m" # Red
74
- when 'WARN', 'WARNING'
75
- "\e[33m#{level_str}\e[0m" # Yellow
76
- when 'INFO'
77
- "\e[36m#{level_str}\e[0m" # Cyan
78
- when 'DEBUG', 'TRACE'
79
- "\e[90m#{level_str}\e[0m" # Gray
80
- else
81
- level_str
82
- end
83
- end
84
-
85
- def sanitize(text)
86
- text.to_s.gsub(/[\n\r\t]/, ' ').squeeze(' ')
87
- end
88
-
89
- def truncate(text, length)
90
- text = text.to_s
91
- text.length > length ? "#{text[0...(length - 3)]}..." : text
92
- end
93
-
94
- def colorize_json(data)
95
- JSON.pretty_generate(data)
96
- end
78
+ # ============================================================
79
+ # FZF Execution
80
+ # ============================================================
97
81
 
98
82
  def execute_fzf(input_path, preview_path)
99
- fzf_args = build_fzf_args(input_path, preview_path)
83
+ header_text = HeaderBuilder.build_header_text(@results, @messages)
84
+ fzf_args = FzfConfig.build_fzf_args(input_path, preview_path, header_text)
100
85
 
101
- # Use IO.popen with array to avoid shell escaping issues
102
86
  result = IO.popen(fzf_args, 'r+') do |io|
103
87
  File.readlines(input_path).each { |line| io.puts line }
104
88
  io.close_write
@@ -108,70 +92,11 @@ module Sumologic
108
92
  result.strip
109
93
  end
110
94
 
111
- def build_fzf_args(input_path, preview_path)
112
- preview_cmd = build_preview_command(preview_path)
113
- view_cmd = build_view_command(preview_path)
114
- header_text = build_header_text
115
-
116
- [
117
- 'fzf',
118
- '--ansi',
119
- '--multi',
120
- "--header=#{header_text}",
121
- "--preview=#{preview_cmd}",
122
- '--preview-window=right:60%:wrap:follow',
123
- '--bind=enter:toggle',
124
- "--bind=tab:execute(#{view_cmd})",
125
- '--bind=ctrl-a:select-all',
126
- '--bind=ctrl-d:deselect-all',
127
- '--bind=ctrl-s:execute-silent(echo {+} > sumo-selected.txt)+abort',
128
- '--bind=ctrl-y:execute-silent(echo {+} | pbcopy || echo {+} | xclip -selection clipboard 2>/dev/null)+abort',
129
- '--bind=ctrl-e:execute-silent(echo {+} > sumo-export.jsonl)+abort',
130
- '--bind=ctrl-/:toggle-preview',
131
- "--bind=ctrl-r:reload(cat #{input_path})",
132
- '--bind=ctrl-q:abort',
133
- '--height=100%'
134
- ]
135
- end
136
-
137
- def build_view_command(preview_path)
138
- # FZF {n} is 0-indexed! Add 1 to get sed line number (1-indexed)
139
- 'LINE=$(({n} + 1)); ' \
140
- "sed -n \"$LINE\"p #{Shellwords.escape(preview_path)} | jq -C . | less -R"
141
- end
142
-
143
- def build_preview_command(preview_path)
144
- # FZF {n} is 0-indexed! Add 1 to get JSONL line number (1-indexed)
145
- escaped_path = Shellwords.escape(preview_path)
146
- calc = "LINE=$(({n} + 1)); TOTAL=$(wc -l < #{escaped_path}); "
147
- display = 'echo "Message $LINE of $TOTAL"; echo ""; '
148
- extract = "sed -n \"$LINE\"p #{escaped_path}"
149
-
150
- calc + display + "#{extract} | jq -C . || #{extract}"
151
- end
152
-
153
- def build_header_text
154
- query = @results['query'] || 'N/A'
155
- count = @messages.size
156
- sources = @messages.map { |m| m['map']['_sourceCategory'] }.compact.uniq.size
157
-
158
- # Column headers
159
- columns = "#{pad('TIME', 8)} #{pad('LEVEL', 7)} #{pad('SOURCE', 25)} MESSAGE"
160
- # Info and keys on second line
161
- info = "#{count} msgs | #{sources} sources | Query: #{truncate(query, 40)}"
162
- keys = 'Enter=select Tab=view Ctrl-S=save Ctrl-Y=copy Ctrl-E=export Ctrl-Q=quit'
163
-
164
- "#{columns}\n#{info} | #{keys}"
165
- end
166
-
167
- def pad(text, width)
168
- text.ljust(width)
169
- end
95
+ # ============================================================
96
+ # Selection Handling
97
+ # ============================================================
170
98
 
171
99
  def handle_selection(selected)
172
- # Selected contains the actual display lines (no index field)
173
- # We don't show them since user already saw in FZF
174
- # The keybindings (Ctrl-S, Ctrl-Y, Ctrl-E) handle the export
175
100
  return if selected.empty?
176
101
 
177
102
  puts "\n#{'═' * 80}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sumologic
4
- VERSION = '1.3.1'
4
+ VERSION = '1.3.2'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sumologic-query
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - patrick204nqh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-15 00:00:00.000000000 Z
11
+ date: 2025-11-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -104,6 +104,11 @@ files:
104
104
  - lib/sumologic/http/connection_pool.rb
105
105
  - lib/sumologic/interactive.rb
106
106
  - lib/sumologic/interactive/fzf_viewer.rb
107
+ - lib/sumologic/interactive/fzf_viewer/config.rb
108
+ - lib/sumologic/interactive/fzf_viewer/formatter.rb
109
+ - lib/sumologic/interactive/fzf_viewer/fzf_config.rb
110
+ - lib/sumologic/interactive/fzf_viewer/header_builder.rb
111
+ - lib/sumologic/interactive/fzf_viewer/searchable_builder.rb
107
112
  - lib/sumologic/metadata/collector.rb
108
113
  - lib/sumologic/metadata/collector_source_fetcher.rb
109
114
  - lib/sumologic/metadata/source.rb