sumologic-query 1.4.0 → 1.4.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: 5822fc268d1979e97d6993b7f233b720049f832c4d5d921a75c32ecccc16ce68
4
- data.tar.gz: f9e1cde6a138f45d70ec4ed2e06b22f87faf9500a6cb4fb7274fe8df787bcba1
3
+ metadata.gz: 17737642aa1c9244133b4442fd6dccc156cf39b5a35cc05071a4769800ad0501
4
+ data.tar.gz: 29037e867772bbc70e2cda259879e8b6d55e51b70611b7ef5167c79ab2eee00e
5
5
  SHA512:
6
- metadata.gz: 661862b1dfbc17a5729fbffc54c459e9b2e52a996346b0c028179002c606689096c0a191a26d7bb06d737c197f50324eb86e574289a093c4a392b9248a3e2647
7
- data.tar.gz: fda3e751c1dc953fa10d878288193ffe060dcd9366e9d18e8cad9d8eeaf8a194e0dcaa1f50ca245fdc5ab20a95c7f1b67809add3dbed2aa28341f68130188ffd
6
+ metadata.gz: 67bd5ba57a5a3ed5739d274a25c29e9ba4c1cd2415976a5bb7cbdfb075276c67b614917a14a312e33fa618c15a665ccb7bf82e34b20602ab800e3c00e65bed8c
7
+ data.tar.gz: 2c3fff993dcf6d5a2d14b5719a3fd23331477dece5a43cad0dc5d42e4ae7065fb94bda7741ead8e9a678839d8b5d063bb237c9c301585008fdba427737e1abd8
data/CHANGELOG.md CHANGED
@@ -6,6 +6,42 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
6
6
 
7
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
8
8
  and release notes are automatically generated from commit messages.
9
+ ## [1.4.2](https://github.com/patrick204nqh/sumologic-query/compare/v1.4.1...v1.4.2) (2026-02-11)
10
+
11
+ ### 🎉 New Features
12
+
13
+ - add source discovery guidance to skills
14
+ - add filtering options to list-sources
15
+ - add keyword and limit filters to discover-source-metadata
16
+ - add -q/--query and -l/--limit flags to list-collectors
17
+
18
+ ### 🐛 Bug Fixes
19
+
20
+ - support compound relative time expressions like -1h30m
21
+
22
+
23
+
24
+ ## [1.4.1](https://github.com/patrick204nqh/sumologic-query/compare/v1.4.0...v1.4.1) (2026-02-11)
25
+
26
+ ### 🎉 New Features
27
+
28
+ - add "Open in Sumo Logic" URL to search output
29
+ - add command recipe and CLI command specs
30
+
31
+ ### 🐛 Bug Fixes
32
+
33
+ - update source_code_uri metadata to use homepage
34
+
35
+ ### 🔧 Refactoring
36
+
37
+ - resolve ADR-006, fix README links, extract command helpers
38
+
39
+ ### 📚 Documentation
40
+
41
+ - consolidate query examples and document v1/v2 API split
42
+
43
+
44
+
9
45
  ## [1.4.0](https://github.com/patrick204nqh/sumologic-query/compare/v1.3.5...v1.4.0) (2026-02-11)
10
46
 
11
47
  ### 🎉 New Features
data/README.md CHANGED
@@ -152,9 +152,9 @@ client.list_all_sources
152
152
  ## Documentation
153
153
 
154
154
  - [Query Examples](examples/queries.md) - Query patterns and examples
155
- - [Quick Reference](docs/tldr.md) - Command cheat sheet
156
- - [Rate Limiting](docs/rate-limiting.md) - Performance tuning
157
- - [Architecture](docs/architecture/) - Design decisions
155
+ - [Quick Reference](docs/sdlc/7-maintain/tldr.md) - Command cheat sheet
156
+ - [Rate Limiting](docs/sdlc/4-develop/rate-limiting.md) - Performance tuning
157
+ - [Architecture](docs/sdlc/3-design/overview.md) - Design decisions
158
158
 
159
159
  ## Contributing
160
160
 
@@ -17,6 +17,18 @@ module Sumologic
17
17
 
18
18
  private
19
19
 
20
+ def list_resource(label:, key:)
21
+ warn "Fetching #{label}..."
22
+ items = yield
23
+ output_json(total: items.size, key => items)
24
+ end
25
+
26
+ def get_resource(label:, id:)
27
+ warn "Fetching #{label} #{id}..."
28
+ result = yield
29
+ output_json(result)
30
+ end
31
+
20
32
  def output_json(data)
21
33
  json_output = JSON.pretty_generate(data)
22
34
 
@@ -40,6 +40,7 @@ module Sumologic
40
40
  end
41
41
  warn "Time Zone: #{@parsed_timezone}"
42
42
  warn "Filter: #{options[:filter] || 'none (all sources)'}"
43
+ warn "Keyword: #{options[:keyword]}" if options[:keyword]
43
44
  warn '-' * 60
44
45
  warn 'Running aggregation query to discover sources...'
45
46
  $stderr.puts
@@ -50,7 +51,9 @@ module Sumologic
50
51
  from_time: @parsed_from,
51
52
  to_time: @parsed_to,
52
53
  time_zone: @parsed_timezone,
53
- filter: options[:filter]
54
+ filter: options[:filter],
55
+ keyword: options[:keyword],
56
+ limit: options[:limit]
54
57
  )
55
58
  end
56
59
  end
@@ -8,11 +8,9 @@ module Sumologic
8
8
  # Handles the export-content command execution
9
9
  class ExportContentCommand < BaseCommand
10
10
  def execute
11
- content_id = options[:content_id]
12
- warn "Exporting content #{content_id}..."
13
- result = client.export_content(content_id: content_id)
14
-
15
- output_json(result)
11
+ get_resource(label: 'content', id: options[:content_id]) do
12
+ client.export_content(content_id: options[:content_id])
13
+ end
16
14
  end
17
15
  end
18
16
  end
@@ -8,11 +8,9 @@ module Sumologic
8
8
  # Handles the get-content command execution
9
9
  class GetContentCommand < BaseCommand
10
10
  def execute
11
- path = options[:path]
12
- warn "Looking up content at path: #{path}..."
13
- content = client.get_content(path: path)
14
-
15
- output_json(content)
11
+ get_resource(label: 'content at path:', id: options[:path]) do
12
+ client.get_content(path: options[:path])
13
+ end
16
14
  end
17
15
  end
18
16
  end
@@ -8,11 +8,9 @@ module Sumologic
8
8
  # Handles the get-dashboard command execution
9
9
  class GetDashboardCommand < BaseCommand
10
10
  def execute
11
- dashboard_id = options[:dashboard_id]
12
- warn "Fetching dashboard #{dashboard_id}..."
13
- dashboard = client.get_dashboard(dashboard_id: dashboard_id)
14
-
15
- output_json(dashboard)
11
+ get_resource(label: 'dashboard', id: options[:dashboard_id]) do
12
+ client.get_dashboard(dashboard_id: options[:dashboard_id])
13
+ end
16
14
  end
17
15
  end
18
16
  end
@@ -8,11 +8,9 @@ module Sumologic
8
8
  # Handles the get-lookup command execution
9
9
  class GetLookupCommand < BaseCommand
10
10
  def execute
11
- lookup_id = options[:lookup_id]
12
- warn "Fetching lookup table #{lookup_id}..."
13
- lookup = client.get_lookup(lookup_id: lookup_id)
14
-
15
- output_json(lookup)
11
+ get_resource(label: 'lookup table', id: options[:lookup_id]) do
12
+ client.get_lookup(lookup_id: options[:lookup_id])
13
+ end
16
14
  end
17
15
  end
18
16
  end
@@ -8,11 +8,9 @@ module Sumologic
8
8
  # Handles the get-monitor command execution
9
9
  class GetMonitorCommand < BaseCommand
10
10
  def execute
11
- monitor_id = options[:monitor_id]
12
- warn "Fetching monitor #{monitor_id}..."
13
- monitor = client.get_monitor(monitor_id: monitor_id)
14
-
15
- output_json(monitor)
11
+ get_resource(label: 'monitor', id: options[:monitor_id]) do
12
+ client.get_monitor(monitor_id: options[:monitor_id])
13
+ end
16
14
  end
17
15
  end
18
16
  end
@@ -8,13 +8,7 @@ module Sumologic
8
8
  # Handles the list-apps command execution
9
9
  class ListAppsCommand < BaseCommand
10
10
  def execute
11
- warn 'Fetching app catalog...'
12
- apps = client.list_apps
13
-
14
- output_json(
15
- total: apps.size,
16
- apps: apps
17
- )
11
+ list_resource(label: 'app catalog', key: :apps) { client.list_apps }
18
12
  end
19
13
  end
20
14
  end
@@ -8,13 +8,12 @@ module Sumologic
8
8
  # Handles the list-collectors command execution
9
9
  class ListCollectorsCommand < BaseCommand
10
10
  def execute
11
- warn 'Fetching collectors...'
12
- collectors = client.list_collectors
13
-
14
- output_json(
15
- total: collectors.size,
16
- collectors: collectors
17
- )
11
+ list_resource(label: 'collectors', key: :collectors) do
12
+ client.list_collectors(
13
+ query: options[:query],
14
+ limit: options[:limit]
15
+ )
16
+ end
18
17
  end
19
18
  end
20
19
  end
@@ -8,13 +8,9 @@ module Sumologic
8
8
  # Handles the list-dashboards command execution
9
9
  class ListDashboardsCommand < BaseCommand
10
10
  def execute
11
- warn 'Fetching dashboards...'
12
- dashboards = client.list_dashboards(limit: options[:limit] || 100)
13
-
14
- output_json(
15
- total: dashboards.size,
16
- dashboards: dashboards
17
- )
11
+ list_resource(label: 'dashboards', key: :dashboards) do
12
+ client.list_dashboards(limit: options[:limit] || 100)
13
+ end
18
14
  end
19
15
  end
20
16
  end
@@ -8,13 +8,9 @@ module Sumologic
8
8
  # Handles the list-health-events command execution
9
9
  class ListHealthEventsCommand < BaseCommand
10
10
  def execute
11
- warn 'Fetching health events...'
12
- events = client.list_health_events(limit: options[:limit] || 100)
13
-
14
- output_json(
15
- total: events.size,
16
- healthEvents: events
17
- )
11
+ list_resource(label: 'health events', key: :healthEvents) do
12
+ client.list_health_events(limit: options[:limit] || 100)
13
+ end
18
14
  end
19
15
  end
20
16
  end
@@ -9,17 +9,13 @@ module Sumologic
9
9
  # Uses the monitors search API for flat, filterable results
10
10
  class ListMonitorsCommand < BaseCommand
11
11
  def execute
12
- warn 'Fetching monitors...'
13
- monitors = client.list_monitors(
14
- query: options[:query],
15
- status: options[:status],
16
- limit: options[:limit] || 100
17
- )
18
-
19
- output_json(
20
- total: monitors.size,
21
- monitors: monitors
22
- )
12
+ list_resource(label: 'monitors', key: :monitors) do
13
+ client.list_monitors(
14
+ query: options[:query],
15
+ status: options[:status],
16
+ limit: options[:limit] || 100
17
+ )
18
+ end
23
19
  end
24
20
  end
25
21
  end
@@ -29,10 +29,14 @@ module Sumologic
29
29
  end
30
30
 
31
31
  def list_all_sources
32
- warn 'Fetching all sources from all collectors...'
33
- warn 'This may take a minute...'
32
+ warn 'Fetching sources from collectors...'
34
33
 
35
- all_sources = client.list_all_sources
34
+ all_sources = client.list_all_sources(
35
+ collector: options[:collector],
36
+ name: options[:name],
37
+ category: options[:category],
38
+ limit: options[:limit]
39
+ )
36
40
 
37
41
  output_json(
38
42
  total_collectors: all_sources.size,
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'erb'
3
4
  require_relative 'base_command'
4
5
  require_relative '../../utils/time_parser'
5
6
 
@@ -46,6 +47,7 @@ module Sumologic
46
47
  end
47
48
  warn "Query: #{options[:query]}"
48
49
  warn "Limit: #{options[:limit] || 'unlimited'}"
50
+ warn "Open in Sumo: #{build_search_url}"
49
51
  warn '-' * 60
50
52
  warn 'Creating search job...'
51
53
  $stderr.puts
@@ -96,6 +98,7 @@ module Sumologic
96
98
  from_original: @original_from,
97
99
  to_original: @original_to,
98
100
  time_zone: @parsed_timezone,
101
+ search_url: build_search_url,
99
102
  record_count: results.size,
100
103
  records: results
101
104
  )
@@ -107,6 +110,7 @@ module Sumologic
107
110
  from_original: @original_from,
108
111
  to_original: @original_to,
109
112
  time_zone: @parsed_timezone,
113
+ search_url: build_search_url,
110
114
  message_count: results.size,
111
115
  messages: results
112
116
  )
@@ -135,10 +139,20 @@ module Sumologic
135
139
  'from' => @parsed_from,
136
140
  'to' => @parsed_to,
137
141
  'time_zone' => @parsed_timezone,
142
+ 'search_url' => build_search_url,
138
143
  'message_count' => results.size,
139
144
  'messages' => results
140
145
  }
141
146
  end
147
+
148
+ def build_search_url
149
+ from_ms = (Time.parse("#{@parsed_from}Z").to_f * 1000).to_i
150
+ to_ms = (Time.parse("#{@parsed_to}Z").to_f * 1000).to_i
151
+ encoded_query = ERB::Util.url_encode(options[:query])
152
+ base = client.config.web_ui_base_url
153
+
154
+ "#{base}/ui/#/search/create?query=#{encoded_query}&startTime=#{from_ms}&endTime=#{to_ms}"
155
+ end
142
156
  end
143
157
  end
144
158
  end
data/lib/sumologic/cli.rb CHANGED
@@ -41,7 +41,7 @@ module Sumologic
41
41
  Time Formats:
42
42
  --from and --to support multiple formats:
43
43
  • 'now' - current time
44
- • Relative: '-30s', '-5m', '-2h', '-7d', '-1w', '-1M' (sec/min/hour/day/week/month)
44
+ • Relative: '-30s', '-5m', '-2h', '-1h30m', '-7d', '-1w', '-1M' (compound supported)
45
45
  • Unix timestamp: '1700000000' (seconds since epoch)
46
46
  • ISO 8601: '2025-11-13T14:00:00'
47
47
 
@@ -89,29 +89,36 @@ module Sumologic
89
89
  Commands::SearchCommand.new(options, create_client).execute
90
90
  end
91
91
 
92
- desc 'list-collectors', 'List all Sumo Logic collectors'
92
+ desc 'list-collectors', 'List Sumo Logic collectors with optional filters'
93
93
  long_desc <<~DESC
94
- List all collectors in your Sumo Logic account.
94
+ List collectors in your Sumo Logic account.
95
+ Supports filtering by name/category and limiting results.
95
96
 
96
- Example:
97
+ Examples:
98
+ sumo-query list-collectors -q "my-service" -l 20
97
99
  sumo-query list-collectors --output collectors.json
98
100
  DESC
101
+ option :query, type: :string, aliases: '-q', desc: 'Filter by name or category (case-insensitive)'
102
+ option :limit, type: :numeric, aliases: '-l', desc: 'Maximum collectors to return'
99
103
  def list_collectors
100
104
  Commands::ListCollectorsCommand.new(options, create_client).execute
101
105
  end
102
106
 
103
- desc 'list-sources', 'List sources from collectors'
107
+ desc 'list-sources', 'List sources from collectors with optional filters'
104
108
  long_desc <<~DESC
105
- List all sources from all collectors, or sources from a specific collector.
109
+ List sources from all collectors, or from a specific collector.
110
+ Supports filtering by collector name, source name, and category.
106
111
 
107
112
  Examples:
108
- # List all sources
109
- sumo-query list-sources
110
-
111
- # List sources for specific collector
113
+ sumo-query list-sources --collector "my-service" --name "nginx" -l 20
114
+ sumo-query list-sources --category "production"
112
115
  sumo-query list-sources --collector-id 12345
113
116
  DESC
114
117
  option :collector_id, type: :string, desc: 'Collector ID to list sources for'
118
+ option :collector, type: :string, desc: 'Filter by collector name (case-insensitive)'
119
+ option :name, type: :string, aliases: '-n', desc: 'Filter by source name (case-insensitive)'
120
+ option :category, type: :string, desc: 'Filter by source category (case-insensitive)'
121
+ option :limit, type: :numeric, aliases: '-l', desc: 'Maximum total sources to return'
115
122
  def list_sources
116
123
  Commands::ListSourcesCommand.new(options, create_client).execute
117
124
  end
@@ -131,7 +138,7 @@ module Sumologic
131
138
  Time Formats:
132
139
  --from and --to support multiple formats:
133
140
  • 'now' - current time
134
- • Relative: '-30s', '-5m', '-2h', '-7d', '-1w', '-1M' (sec/min/hour/day/week/month)
141
+ • Relative: '-30s', '-5m', '-2h', '-1h30m', '-7d', '-1w', '-1M' (compound supported)
135
142
  • Unix timestamp: '1700000000' (seconds since epoch)
136
143
  • ISO 8601: '2025-11-13T14:00:00'
137
144
 
@@ -145,8 +152,9 @@ module Sumologic
145
152
  # Filter by source category (ECS only)
146
153
  sumo-query discover-source-metadata --filter '_sourceCategory=*ecs*'
147
154
 
148
- # Discover CloudWatch sources
149
- sumo-query discover-source-metadata --filter '_sourceCategory=*cloudwatch*'
155
+ # Filter results by keyword (matches name or category)
156
+ sumo-query discover-source-metadata --keyword nginx
157
+ sumo-query discover-source-metadata --keyword nginx -l 10
150
158
 
151
159
  # Save to file
152
160
  sumo-query discover-source-metadata --output discovered-sources.json
@@ -158,6 +166,8 @@ module Sumologic
158
166
  option :time_zone, type: :string, default: 'UTC', aliases: '-z',
159
167
  desc: 'Time zone (UTC, EST, AEST, +00:00, America/New_York, Australia/Sydney)'
160
168
  option :filter, type: :string, desc: 'Optional filter query (e.g., _sourceCategory=*ecs*)'
169
+ option :keyword, type: :string, aliases: '-k', desc: 'Filter results by keyword (matches name or category)'
170
+ option :limit, type: :numeric, aliases: '-l', desc: 'Maximum number of sources to return'
161
171
  def discover_source_metadata
162
172
  Commands::DiscoverSourceMetadataCommand.new(options, create_client).execute
163
173
  end
@@ -83,11 +83,13 @@ module Sumologic
83
83
  )
84
84
  end
85
85
 
86
- # List all collectors
86
+ # List collectors with optional filtering
87
87
  #
88
+ # @param query [String, nil] Filter by name or category (case-insensitive)
89
+ # @param limit [Integer, nil] Maximum number of collectors to return
88
90
  # @return [Array<Hash>] Array of collector hashes
89
- def list_collectors
90
- @collector.list
91
+ def list_collectors(query: nil, limit: nil)
92
+ @collector.list(query: query, limit: limit)
91
93
  end
92
94
 
93
95
  # List sources for a specific collector
@@ -98,11 +100,15 @@ module Sumologic
98
100
  @source.list(collector_id: collector_id)
99
101
  end
100
102
 
101
- # List all sources from all collectors
103
+ # List all sources from all collectors with optional filtering
102
104
  #
105
+ # @param collector [String, nil] Filter collectors by name
106
+ # @param name [String, nil] Filter sources by name
107
+ # @param category [String, nil] Filter sources by category
108
+ # @param limit [Integer, nil] Maximum total sources to return
103
109
  # @return [Array<Hash>] Array of { 'collector' => Hash, 'sources' => Array<Hash> }
104
- def list_all_sources
105
- @source.list_all
110
+ def list_all_sources(collector: nil, name: nil, category: nil, limit: nil)
111
+ @source.list_all(collector: collector, name: name, category: category, limit: limit)
106
112
  end
107
113
 
108
114
  # Discover source metadata from actual log data
@@ -111,14 +117,14 @@ module Sumologic
111
117
  # @param from_time [String] Start time (ISO 8601, unix timestamp, or relative)
112
118
  # @param to_time [String] End time
113
119
  # @param time_zone [String] Time zone (default: UTC)
114
- # @param filter [String, nil] Optional filter query to scope results
120
+ # @param options [Hash] Optional filters :filter, :keyword, :limit
115
121
  # @return [Hash] Discovery results with source metadata
116
- def discover_source_metadata(from_time:, to_time:, time_zone: 'UTC', filter: nil)
122
+ def discover_source_metadata(from_time:, to_time:, time_zone: 'UTC', **options)
117
123
  @source_metadata_discovery.discover(
118
124
  from_time: from_time,
119
125
  to_time: to_time,
120
126
  time_zone: time_zone,
121
- filter: filter
127
+ **options
122
128
  )
123
129
  end
124
130
 
@@ -45,6 +45,10 @@ module Sumologic
45
45
  @base_url_v2 ||= build_base_url('v2')
46
46
  end
47
47
 
48
+ def web_ui_base_url
49
+ @web_ui_base_url ||= build_web_ui_base_url
50
+ end
51
+
48
52
  def validate!
49
53
  raise AuthenticationError, 'SUMO_ACCESS_ID not set' unless @access_id
50
54
  raise AuthenticationError, 'SUMO_ACCESS_KEY not set' unless @access_key
@@ -52,6 +56,23 @@ module Sumologic
52
56
 
53
57
  private
54
58
 
59
+ def build_web_ui_base_url
60
+ case @deployment
61
+ when /^http/
62
+ @deployment.sub('api.', 'service.').sub(%r{/api/v\d+.*}, '')
63
+ when 'us1'
64
+ 'https://service.sumologic.com'
65
+ when 'us2'
66
+ 'https://service.us2.sumologic.com'
67
+ when 'eu'
68
+ 'https://service.eu.sumologic.com'
69
+ when 'au'
70
+ 'https://service.au.sumologic.com'
71
+ else
72
+ "https://service.#{@deployment}.sumologic.com"
73
+ end
74
+ end
75
+
55
76
  def build_base_url(version = API_VERSION)
56
77
  case @deployment
57
78
  when /^http/
@@ -13,9 +13,12 @@ module Sumologic
13
13
  @http = http_client
14
14
  end
15
15
 
16
- # List all collectors
17
- # Returns array of collector objects
18
- def list
16
+ # List collectors with optional client-side filtering
17
+ #
18
+ # @param query [String, nil] Filter by name or category (case-insensitive substring match)
19
+ # @param limit [Integer, nil] Maximum number of collectors to return
20
+ # @return [Array<Hash>] Array of collector objects
21
+ def list(query: nil, limit: nil)
19
22
  data = @http.request(
20
23
  method: :get,
21
24
  path: '/collectors'
@@ -23,10 +26,25 @@ module Sumologic
23
26
 
24
27
  collectors = data['collectors'] || []
25
28
  log_info "Found #{collectors.size} collectors"
29
+
30
+ collectors = filter_by_query(collectors, query) if query
31
+ collectors = collectors.take(limit) if limit
32
+
26
33
  collectors
27
34
  rescue StandardError => e
28
35
  raise Error, "Failed to list collectors: #{e.message}"
29
36
  end
37
+
38
+ private
39
+
40
+ def filter_by_query(collectors, query)
41
+ pattern = query.downcase
42
+ collectors.select do |c|
43
+ name = (c['name'] || '').downcase
44
+ category = (c['category'] || '').downcase
45
+ name.include?(pattern) || category.include?(pattern)
46
+ end
47
+ end
30
48
  end
31
49
  end
32
50
  end
@@ -31,19 +31,28 @@ module Sumologic
31
31
  raise Error, "Failed to list sources for collector #{collector_id}: #{e.message}"
32
32
  end
33
33
 
34
- # List all sources from all collectors
34
+ # List all sources from all collectors with optional filtering
35
35
  # Returns array of hashes with collector info and their sources
36
36
  # Uses parallel fetching with thread pool for better performance
37
- def list_all
37
+ #
38
+ # @param collector [String, nil] Filter collectors by name (case-insensitive substring)
39
+ # @param name [String, nil] Filter sources by name (case-insensitive substring)
40
+ # @param category [String, nil] Filter sources by category (case-insensitive substring)
41
+ # @param limit [Integer, nil] Maximum total sources to return
42
+ def list_all(collector: nil, name: nil, category: nil, limit: nil)
38
43
  collectors = @collector_client.list
39
44
  active_collectors = collectors.select { |c| c['alive'] }
45
+ active_collectors = filter_collectors(active_collectors, collector) if collector
40
46
 
41
47
  log_info "Fetching sources for #{active_collectors.size} active collectors in parallel..."
42
48
 
43
- result = @fetcher.fetch_all(active_collectors) do |collector|
44
- fetch_collector_sources(collector)
49
+ result = @fetcher.fetch_all(active_collectors) do |c|
50
+ fetch_collector_sources(c)
45
51
  end
46
52
 
53
+ result = filter_sources(result, name: name, category: category)
54
+ result = apply_source_limit(result, limit) if limit
55
+
47
56
  log_info "Total: #{result.size} collectors with sources"
48
57
  result
49
58
  rescue StandardError => e
@@ -52,6 +61,37 @@ module Sumologic
52
61
 
53
62
  private
54
63
 
64
+ def filter_collectors(collectors, pattern)
65
+ pattern = pattern.downcase
66
+ collectors.select { |c| (c['name'] || '').downcase.include?(pattern) }
67
+ end
68
+
69
+ def filter_sources(result, name:, category:)
70
+ matcher = source_matcher(name&.downcase, category&.downcase)
71
+ result.filter_map do |entry|
72
+ filtered = entry['sources'].select(&matcher)
73
+ { 'collector' => entry['collector'], 'sources' => filtered } unless filtered.empty?
74
+ end
75
+ end
76
+
77
+ def source_matcher(name_pattern, cat_pattern)
78
+ lambda do |s|
79
+ (!name_pattern || (s['name'] || '').downcase.include?(name_pattern)) &&
80
+ (!cat_pattern || (s['category'] || '').downcase.include?(cat_pattern))
81
+ end
82
+ end
83
+
84
+ def apply_source_limit(result, limit)
85
+ remaining = limit
86
+ result.each_with_object([]) do |entry, acc|
87
+ break acc if remaining <= 0
88
+
89
+ sources = entry['sources'].take(remaining)
90
+ acc << { 'collector' => entry['collector'], 'sources' => sources }
91
+ remaining -= sources.size
92
+ end
93
+ end
94
+
55
95
  # Fetch sources for a single collector
56
96
  # @return [Hash] collector and sources data
57
97
  def fetch_collector_sources(collector)
@@ -22,8 +22,12 @@ module Sumologic
22
22
  # @param from_time [String] Start time (ISO 8601, unix timestamp, or relative)
23
23
  # @param to_time [String] End time
24
24
  # @param time_zone [String] Time zone (default: UTC)
25
- # @param filter [String, nil] Optional filter query to scope results
26
- def discover(from_time:, to_time:, time_zone: 'UTC', filter: nil)
25
+ # @param options [Hash] Optional filters :filter, :keyword, :limit
26
+ def discover(from_time:, to_time:, time_zone: 'UTC', **options)
27
+ filter = options[:filter]
28
+ keyword = options[:keyword]
29
+ limit = options[:limit]
30
+
27
31
  query = build_query(filter)
28
32
  log_info "Discovering source metadata with query: #{query}"
29
33
  log_info "Time range: #{from_time} to #{to_time} (#{time_zone})"
@@ -39,6 +43,8 @@ module Sumologic
39
43
  )
40
44
 
41
45
  source_models = parse_aggregation_results(records)
46
+ source_models = filter_by_keyword(source_models, keyword) if keyword
47
+ source_models = source_models.take(limit) if limit
42
48
 
43
49
  {
44
50
  'time_range' => {
@@ -47,6 +53,7 @@ module Sumologic
47
53
  'time_zone' => time_zone
48
54
  },
49
55
  'filter' => filter,
56
+ 'keyword' => keyword,
50
57
  'total_sources' => source_models.size,
51
58
  'sources' => source_models.map(&:to_h)
52
59
  }
@@ -56,6 +63,14 @@ module Sumologic
56
63
 
57
64
  private
58
65
 
66
+ def filter_by_keyword(source_models, keyword)
67
+ pattern = keyword.downcase
68
+ source_models.select do |s|
69
+ (s.name || '').downcase.include?(pattern) ||
70
+ (s.category || '').downcase.include?(pattern)
71
+ end
72
+ end
73
+
59
74
  # Build aggregation query to discover sources
60
75
  def build_query(filter)
61
76
  base = filter || '*'
@@ -7,7 +7,7 @@ module Sumologic
7
7
  # Parses various time formats into ISO 8601 strings for the Sumo Logic API
8
8
  # Supports:
9
9
  # - 'now' - current time
10
- # - Relative times: '-30s', '-5m', '-2h', '-7d', '-1w', '-1M'
10
+ # - Relative times: '-30s', '-5m', '-2h', '-7d', '-1w', '-1M', '-1h30m'
11
11
  # - Unix timestamps: '1700000000' or 1700000000
12
12
  # - ISO 8601: '2025-11-13T14:00:00'
13
13
  class TimeParser
@@ -21,7 +21,9 @@ module Sumologic
21
21
  'M' => 2_592_000 # months (30 days approximation)
22
22
  }.freeze
23
23
 
24
- RELATIVE_TIME_REGEX = /^([+-])(\d+)([smhdwM])$/.freeze
24
+ # Matches single or compound relative times: -30m, -1h30m, -2d3h15m
25
+ RELATIVE_TIME_REGEX = /^([+-])(\d+[smhdwM])+$/.freeze
26
+ RELATIVE_COMPONENT_REGEX = /(\d+)([smhdwM])/.freeze
25
27
 
26
28
  class ParseError < StandardError; end
27
29
 
@@ -32,10 +34,8 @@ module Sumologic
32
34
  def self.parse(time_str, _timezone: 'UTC')
33
35
  return parse_now if time_str.to_s.downcase == 'now'
34
36
 
35
- # Try relative time format (e.g., '-30m', '+1h')
36
- if time_str.is_a?(String) && (match = time_str.match(RELATIVE_TIME_REGEX))
37
- return parse_relative_time(match)
38
- end
37
+ # Try relative time format (e.g., '-30m', '+1h', '-1h30m')
38
+ return parse_relative_time(time_str) if time_str.is_a?(String) && time_str.match?(RELATIVE_TIME_REGEX)
39
39
 
40
40
  # Try Unix timestamp (integer or numeric string)
41
41
  return parse_unix_timestamp(time_str) if unix_timestamp?(time_str)
@@ -95,14 +95,14 @@ module Sumologic
95
95
  format_time(Time.now)
96
96
  end
97
97
 
98
- private_class_method def self.parse_relative_time(match)
99
- sign, amount, unit = match.captures
100
- amount = amount.to_i
101
- amount = -amount if sign == '-'
98
+ private_class_method def self.parse_relative_time(time_str)
99
+ sign = time_str[0]
100
+ components = time_str.scan(RELATIVE_COMPONENT_REGEX)
102
101
 
103
- seconds_delta = amount * UNITS[unit]
104
- target_time = Time.now + seconds_delta
102
+ seconds_delta = components.sum { |amount, unit| amount.to_i * UNITS[unit] }
103
+ seconds_delta = -seconds_delta if sign == '-'
105
104
 
105
+ target_time = Time.now + seconds_delta
106
106
  format_time(target_time)
107
107
  end
108
108
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sumologic
4
- VERSION = '1.4.0'
4
+ VERSION = '1.4.2'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sumologic-query
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - patrick204nqh
@@ -154,7 +154,6 @@ homepage: https://github.com/patrick204nqh/sumologic-query
154
154
  licenses:
155
155
  - MIT
156
156
  metadata:
157
- homepage_uri: https://github.com/patrick204nqh/sumologic-query
158
157
  source_code_uri: https://github.com/patrick204nqh/sumologic-query
159
158
  bug_tracker_uri: https://github.com/patrick204nqh/sumologic-query/issues
160
159
  changelog_uri: https://github.com/patrick204nqh/sumologic-query/blob/main/CHANGELOG.md