sumologic-query 1.1.0 → 1.1.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 +4 -4
- data/README.md +105 -200
- data/bin/sumo-query +5 -101
- data/lib/sumologic/cli.rb +208 -0
- data/lib/sumologic/client.rb +44 -200
- data/lib/sumologic/configuration.rb +55 -0
- data/lib/sumologic/http/authenticator.rb +20 -0
- data/lib/sumologic/http/client.rb +80 -0
- data/lib/sumologic/metadata/collector.rb +33 -0
- data/lib/sumologic/metadata/source.rb +72 -0
- data/lib/sumologic/search/job.rb +68 -0
- data/lib/sumologic/search/paginator.rb +67 -0
- data/lib/sumologic/search/poller.rb +80 -0
- data/lib/sumologic/version.rb +1 -1
- data/lib/sumologic.rb +33 -2
- metadata +25 -2
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Sumologic
|
|
7
|
+
# Thor-based CLI for Sumo Logic query tool
|
|
8
|
+
class CLI < Thor
|
|
9
|
+
class_option :debug, type: :boolean, aliases: '-d', desc: 'Enable debug output'
|
|
10
|
+
class_option :output, type: :string, aliases: '-o', desc: 'Output file (default: stdout)'
|
|
11
|
+
|
|
12
|
+
def self.exit_on_failure?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc 'search', 'Search Sumo Logic logs'
|
|
17
|
+
long_desc <<~DESC
|
|
18
|
+
Search Sumo Logic logs using a query string.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
# Error timeline with 5-minute buckets
|
|
22
|
+
sumo-query search --query 'error | timeslice 5m | count' \\
|
|
23
|
+
--from '2025-11-13T14:00:00' --to '2025-11-13T15:00:00'
|
|
24
|
+
|
|
25
|
+
# Search for specific text
|
|
26
|
+
sumo-query search --query '"connection timeout"' \\
|
|
27
|
+
--from '2025-11-13T14:00:00' --to '2025-11-13T15:00:00' \\
|
|
28
|
+
--limit 100
|
|
29
|
+
DESC
|
|
30
|
+
option :query, type: :string, required: true, aliases: '-q', desc: 'Search query'
|
|
31
|
+
option :from, type: :string, required: true, aliases: '-f', desc: 'Start time (ISO 8601)'
|
|
32
|
+
option :to, type: :string, required: true, aliases: '-t', desc: 'End time (ISO 8601)'
|
|
33
|
+
option :time_zone, type: :string, default: 'UTC', aliases: '-z', desc: 'Time zone'
|
|
34
|
+
option :limit, type: :numeric, aliases: '-l', desc: 'Limit number of messages'
|
|
35
|
+
def search
|
|
36
|
+
$DEBUG = true if options[:debug]
|
|
37
|
+
|
|
38
|
+
client = create_client
|
|
39
|
+
|
|
40
|
+
log_search_info
|
|
41
|
+
results = execute_search(client)
|
|
42
|
+
output_search_results(results)
|
|
43
|
+
|
|
44
|
+
warn "\nMessage count: #{results.size}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
desc 'list-collectors', 'List all Sumo Logic collectors'
|
|
48
|
+
long_desc <<~DESC
|
|
49
|
+
List all collectors in your Sumo Logic account.
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
sumo-query list-collectors --output collectors.json
|
|
53
|
+
DESC
|
|
54
|
+
def list_collectors
|
|
55
|
+
$DEBUG = true if options[:debug]
|
|
56
|
+
|
|
57
|
+
client = create_client
|
|
58
|
+
|
|
59
|
+
warn 'Fetching collectors...'
|
|
60
|
+
collectors = client.list_collectors
|
|
61
|
+
|
|
62
|
+
output_json(
|
|
63
|
+
total: collectors.size,
|
|
64
|
+
collectors: collectors.map { |c| format_collector(c) }
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
desc 'list-sources', 'List sources from collectors'
|
|
69
|
+
long_desc <<~DESC
|
|
70
|
+
List all sources from all collectors, or sources from a specific collector.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
# List all sources
|
|
74
|
+
sumo-query list-sources
|
|
75
|
+
|
|
76
|
+
# List sources for specific collector
|
|
77
|
+
sumo-query list-sources --collector-id 12345
|
|
78
|
+
DESC
|
|
79
|
+
option :collector_id, type: :string, desc: 'Collector ID to list sources for'
|
|
80
|
+
def list_sources
|
|
81
|
+
$DEBUG = true if options[:debug]
|
|
82
|
+
|
|
83
|
+
client = create_client
|
|
84
|
+
|
|
85
|
+
if options[:collector_id]
|
|
86
|
+
list_sources_for_collector(client, options[:collector_id])
|
|
87
|
+
else
|
|
88
|
+
list_all_sources(client)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
desc 'version', 'Show version information'
|
|
93
|
+
def version
|
|
94
|
+
puts "sumo-query version #{Sumologic::VERSION}"
|
|
95
|
+
end
|
|
96
|
+
map %w[-v --version] => :version
|
|
97
|
+
|
|
98
|
+
default_task :search
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def create_client
|
|
103
|
+
Client.new
|
|
104
|
+
rescue AuthenticationError => e
|
|
105
|
+
error "Authentication Error: #{e.message}"
|
|
106
|
+
error "\nPlease set environment variables:"
|
|
107
|
+
error " export SUMO_ACCESS_ID='your_access_id'"
|
|
108
|
+
error " export SUMO_ACCESS_KEY='your_access_key'"
|
|
109
|
+
error " export SUMO_DEPLOYMENT='us2' # Optional, defaults to us2"
|
|
110
|
+
exit 1
|
|
111
|
+
rescue Error => e
|
|
112
|
+
error "Error: #{e.message}"
|
|
113
|
+
exit 1
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def list_sources_for_collector(client, collector_id)
|
|
117
|
+
warn "Fetching sources for collector: #{collector_id}"
|
|
118
|
+
sources = client.list_sources(collector_id: collector_id)
|
|
119
|
+
|
|
120
|
+
output_json(
|
|
121
|
+
collector_id: collector_id,
|
|
122
|
+
total: sources.size,
|
|
123
|
+
sources: sources.map { |s| format_source(s) }
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def list_all_sources(client)
|
|
128
|
+
warn 'Fetching all sources from all collectors...'
|
|
129
|
+
warn 'This may take a minute...'
|
|
130
|
+
|
|
131
|
+
all_sources = client.list_all_sources
|
|
132
|
+
|
|
133
|
+
output_json(
|
|
134
|
+
total_collectors: all_sources.size,
|
|
135
|
+
total_sources: all_sources.sum { |c| c['sources'].size },
|
|
136
|
+
data: all_sources.map do |item|
|
|
137
|
+
{
|
|
138
|
+
collector: item['collector'],
|
|
139
|
+
sources: item['sources'].map { |s| format_source(s) }
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def format_collector(collector)
|
|
146
|
+
{
|
|
147
|
+
id: collector['id'],
|
|
148
|
+
name: collector['name'],
|
|
149
|
+
collectorType: collector['collectorType'],
|
|
150
|
+
alive: collector['alive'],
|
|
151
|
+
category: collector['category']
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def format_source(source)
|
|
156
|
+
{
|
|
157
|
+
id: source['id'],
|
|
158
|
+
name: source['name'],
|
|
159
|
+
category: source['category'],
|
|
160
|
+
sourceType: source['sourceType'],
|
|
161
|
+
alive: source['alive']
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def output_json(data)
|
|
166
|
+
json_output = JSON.pretty_generate(data)
|
|
167
|
+
|
|
168
|
+
if options[:output]
|
|
169
|
+
File.write(options[:output], json_output)
|
|
170
|
+
warn "\nResults saved to: #{options[:output]}"
|
|
171
|
+
else
|
|
172
|
+
puts json_output
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def error(message)
|
|
177
|
+
warn message
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def log_search_info
|
|
181
|
+
warn "Querying Sumo Logic: #{options[:from]} to #{options[:to]}"
|
|
182
|
+
warn "Query: #{options[:query]}"
|
|
183
|
+
warn 'This may take 1-3 minutes depending on data volume...'
|
|
184
|
+
$stderr.puts
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def execute_search(client)
|
|
188
|
+
client.search(
|
|
189
|
+
query: options[:query],
|
|
190
|
+
from_time: options[:from],
|
|
191
|
+
to_time: options[:to],
|
|
192
|
+
time_zone: options[:time_zone],
|
|
193
|
+
limit: options[:limit]
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def output_search_results(results)
|
|
198
|
+
output_json(
|
|
199
|
+
query: options[:query],
|
|
200
|
+
from: options[:from],
|
|
201
|
+
to: options[:to],
|
|
202
|
+
time_zone: options[:time_zone],
|
|
203
|
+
message_count: results.size,
|
|
204
|
+
messages: results
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
data/lib/sumologic/client.rb
CHANGED
|
@@ -1,215 +1,59 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'uri'
|
|
6
|
-
require 'base64'
|
|
7
|
-
|
|
8
3
|
module Sumologic
|
|
9
|
-
#
|
|
10
|
-
#
|
|
4
|
+
# Facade for Sumo Logic API operations
|
|
5
|
+
# Coordinates HTTP, Search, and Metadata components
|
|
11
6
|
class Client
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
7
|
+
attr_reader :config
|
|
8
|
+
|
|
9
|
+
def initialize(config = nil)
|
|
10
|
+
@config = config || Configuration.new
|
|
11
|
+
@config.validate!
|
|
12
|
+
|
|
13
|
+
# Initialize HTTP layer
|
|
14
|
+
authenticator = Http::Authenticator.new(
|
|
15
|
+
access_id: @config.access_id,
|
|
16
|
+
access_key: @config.access_key
|
|
17
|
+
)
|
|
18
|
+
@http = Http::Client.new(
|
|
19
|
+
base_url: @config.base_url,
|
|
20
|
+
authenticator: authenticator
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Initialize domain components
|
|
24
|
+
@search = Search::Job.new(http_client: @http, config: @config)
|
|
25
|
+
@collector = Metadata::Collector.new(http_client: @http)
|
|
26
|
+
@source = Metadata::Source.new(http_client: @http, collector_client: @collector)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Search logs with query
|
|
30
|
+
# Returns array of messages
|
|
32
31
|
def search(query:, from_time:, to_time:, time_zone: 'UTC', limit: nil)
|
|
33
|
-
|
|
34
|
-
poll_until_complete(job_id)
|
|
35
|
-
messages = fetch_all_messages(job_id, limit)
|
|
36
|
-
delete_job(job_id)
|
|
37
|
-
messages
|
|
38
|
-
rescue StandardError => e
|
|
39
|
-
delete_job(job_id) if job_id
|
|
40
|
-
raise Error, "Search failed: #{e.message}"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def validate_credentials!
|
|
46
|
-
raise AuthenticationError, 'SUMO_ACCESS_ID not set' unless @access_id
|
|
47
|
-
raise AuthenticationError, 'SUMO_ACCESS_KEY not set' unless @access_key
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def deployment_url(deployment)
|
|
51
|
-
case deployment
|
|
52
|
-
when /^http/
|
|
53
|
-
deployment # Full URL provided
|
|
54
|
-
when 'us1'
|
|
55
|
-
'https://api.sumologic.com/api/v1'
|
|
56
|
-
when 'us2'
|
|
57
|
-
'https://api.us2.sumologic.com/api/v1'
|
|
58
|
-
when 'eu'
|
|
59
|
-
'https://api.eu.sumologic.com/api/v1'
|
|
60
|
-
when 'au'
|
|
61
|
-
'https://api.au.sumologic.com/api/v1'
|
|
62
|
-
else
|
|
63
|
-
"https://api.#{deployment}.sumologic.com/api/v1"
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def auth_header
|
|
68
|
-
encoded = Base64.strict_encode64("#{@access_id}:#{@access_key}")
|
|
69
|
-
"Basic #{encoded}"
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def create_job(query, from_time, to_time, time_zone)
|
|
73
|
-
uri = URI("#{@base_url}/search/jobs")
|
|
74
|
-
request = Net::HTTP::Post.new(uri)
|
|
75
|
-
request['Authorization'] = auth_header
|
|
76
|
-
request['Content-Type'] = 'application/json'
|
|
77
|
-
request['Accept'] = 'application/json'
|
|
78
|
-
|
|
79
|
-
body = {
|
|
32
|
+
@search.execute(
|
|
80
33
|
query: query,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
response = http_request(uri, request)
|
|
88
|
-
data = JSON.parse(response.body)
|
|
89
|
-
|
|
90
|
-
raise Error, "Failed to create job: #{data['message']}" unless data['id']
|
|
91
|
-
|
|
92
|
-
log_info "Created search job: #{data['id']}"
|
|
93
|
-
data['id']
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def poll_until_complete(job_id, timeout: DEFAULT_TIMEOUT)
|
|
97
|
-
uri = URI("#{@base_url}/search/jobs/#{job_id}")
|
|
98
|
-
start_time = Time.now
|
|
99
|
-
interval = INITIAL_POLL_INTERVAL
|
|
100
|
-
poll_count = 0
|
|
101
|
-
|
|
102
|
-
loop do
|
|
103
|
-
raise TimeoutError, "Search job timed out after #{timeout} seconds" if Time.now - start_time > timeout
|
|
104
|
-
|
|
105
|
-
request = Net::HTTP::Get.new(uri)
|
|
106
|
-
request['Authorization'] = auth_header
|
|
107
|
-
request['Accept'] = 'application/json'
|
|
108
|
-
|
|
109
|
-
response = http_request(uri, request)
|
|
110
|
-
data = JSON.parse(response.body)
|
|
111
|
-
|
|
112
|
-
state = data['state']
|
|
113
|
-
msg_count = data['messageCount']
|
|
114
|
-
rec_count = data['recordCount']
|
|
115
|
-
log_info "Job state: #{state} (#{msg_count} messages, #{rec_count} records) [interval: #{interval}s]"
|
|
116
|
-
|
|
117
|
-
case state
|
|
118
|
-
when 'DONE GATHERING RESULTS'
|
|
119
|
-
elapsed = Time.now - start_time
|
|
120
|
-
log_info "Job completed in #{elapsed.round(1)} seconds after #{poll_count + 1} polls"
|
|
121
|
-
return data
|
|
122
|
-
when 'CANCELLED', 'FORCE PAUSED'
|
|
123
|
-
raise Error, "Search job #{state.downcase}"
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
sleep interval
|
|
127
|
-
poll_count += 1
|
|
128
|
-
|
|
129
|
-
# Adaptive backoff: gradually increase interval for long-running jobs
|
|
130
|
-
# This reduces API calls while maintaining responsiveness for quick jobs
|
|
131
|
-
interval = [interval * POLL_BACKOFF_FACTOR, MAX_POLL_INTERVAL].min
|
|
132
|
-
end
|
|
34
|
+
from_time: from_time,
|
|
35
|
+
to_time: to_time,
|
|
36
|
+
time_zone: time_zone,
|
|
37
|
+
limit: limit
|
|
38
|
+
)
|
|
133
39
|
end
|
|
134
40
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
loop do
|
|
141
|
-
batch_limit = if limit
|
|
142
|
-
[MAX_MESSAGES_PER_REQUEST, limit - total_fetched].min
|
|
143
|
-
else
|
|
144
|
-
MAX_MESSAGES_PER_REQUEST
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
break if batch_limit <= 0
|
|
148
|
-
|
|
149
|
-
uri = URI("#{@base_url}/search/jobs/#{job_id}/messages")
|
|
150
|
-
uri.query = URI.encode_www_form(offset: offset, limit: batch_limit)
|
|
151
|
-
|
|
152
|
-
request = Net::HTTP::Get.new(uri)
|
|
153
|
-
request['Authorization'] = auth_header
|
|
154
|
-
request['Accept'] = 'application/json'
|
|
155
|
-
|
|
156
|
-
response = http_request(uri, request)
|
|
157
|
-
data = JSON.parse(response.body)
|
|
158
|
-
|
|
159
|
-
batch = data['messages'] || []
|
|
160
|
-
messages.concat(batch)
|
|
161
|
-
total_fetched += batch.size
|
|
162
|
-
|
|
163
|
-
log_info "Fetched #{batch.size} messages (total: #{total_fetched})"
|
|
164
|
-
|
|
165
|
-
break if batch.size < batch_limit # No more messages
|
|
166
|
-
break if limit && total_fetched >= limit
|
|
167
|
-
|
|
168
|
-
offset += batch.size
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
messages
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def delete_job(job_id)
|
|
175
|
-
return unless job_id
|
|
176
|
-
|
|
177
|
-
uri = URI("#{@base_url}/search/jobs/#{job_id}")
|
|
178
|
-
request = Net::HTTP::Delete.new(uri)
|
|
179
|
-
request['Authorization'] = auth_header
|
|
180
|
-
|
|
181
|
-
http_request(uri, request)
|
|
182
|
-
log_info "Deleted search job: #{job_id}"
|
|
183
|
-
rescue StandardError => e
|
|
184
|
-
log_error "Failed to delete job #{job_id}: #{e.message}"
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def http_request(uri, request)
|
|
188
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
189
|
-
http.use_ssl = true
|
|
190
|
-
http.read_timeout = 60
|
|
191
|
-
http.open_timeout = 10
|
|
192
|
-
|
|
193
|
-
response = http.request(request)
|
|
194
|
-
|
|
195
|
-
case response.code.to_i
|
|
196
|
-
when 200..299
|
|
197
|
-
response
|
|
198
|
-
when 401, 403
|
|
199
|
-
raise AuthenticationError, "Authentication failed: #{response.body}"
|
|
200
|
-
when 429
|
|
201
|
-
raise Error, "Rate limit exceeded: #{response.body}"
|
|
202
|
-
else
|
|
203
|
-
raise Error, "HTTP #{response.code}: #{response.body}"
|
|
204
|
-
end
|
|
41
|
+
# List all collectors
|
|
42
|
+
# Returns array of collector objects
|
|
43
|
+
def list_collectors
|
|
44
|
+
@collector.list
|
|
205
45
|
end
|
|
206
46
|
|
|
207
|
-
|
|
208
|
-
|
|
47
|
+
# List sources for a specific collector
|
|
48
|
+
# Returns array of source objects
|
|
49
|
+
def list_sources(collector_id:)
|
|
50
|
+
@source.list(collector_id: collector_id)
|
|
209
51
|
end
|
|
210
52
|
|
|
211
|
-
|
|
212
|
-
|
|
53
|
+
# List all sources from all collectors
|
|
54
|
+
# Returns array of hashes with collector and sources
|
|
55
|
+
def list_all_sources
|
|
56
|
+
@source.list_all
|
|
213
57
|
end
|
|
214
58
|
end
|
|
215
59
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sumologic
|
|
4
|
+
# Centralized configuration for Sumo Logic client
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_accessor :access_id, :access_key, :deployment, :timeout, :initial_poll_interval, :max_poll_interval,
|
|
7
|
+
:poll_backoff_factor, :max_messages_per_request
|
|
8
|
+
|
|
9
|
+
API_VERSION = 'v1'
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
# Authentication
|
|
13
|
+
@access_id = ENV.fetch('SUMO_ACCESS_ID', nil)
|
|
14
|
+
@access_key = ENV.fetch('SUMO_ACCESS_KEY', nil)
|
|
15
|
+
@deployment = ENV['SUMO_DEPLOYMENT'] || 'us2'
|
|
16
|
+
|
|
17
|
+
# Search job polling
|
|
18
|
+
@initial_poll_interval = 5 # seconds - start fast for small queries
|
|
19
|
+
@max_poll_interval = 20 # seconds - slow down for large queries
|
|
20
|
+
@poll_backoff_factor = 1.5 # increase interval by 50% each time
|
|
21
|
+
|
|
22
|
+
# Timeouts and limits
|
|
23
|
+
@timeout = 300 # seconds (5 minutes)
|
|
24
|
+
@max_messages_per_request = 10_000
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def base_url
|
|
28
|
+
@base_url ||= build_base_url
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate!
|
|
32
|
+
raise AuthenticationError, 'SUMO_ACCESS_ID not set' unless @access_id
|
|
33
|
+
raise AuthenticationError, 'SUMO_ACCESS_KEY not set' unless @access_key
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def build_base_url
|
|
39
|
+
case @deployment
|
|
40
|
+
when /^http/
|
|
41
|
+
@deployment # Full URL provided
|
|
42
|
+
when 'us1'
|
|
43
|
+
"https://api.sumologic.com/api/#{API_VERSION}"
|
|
44
|
+
when 'us2'
|
|
45
|
+
"https://api.us2.sumologic.com/api/#{API_VERSION}"
|
|
46
|
+
when 'eu'
|
|
47
|
+
"https://api.eu.sumologic.com/api/#{API_VERSION}"
|
|
48
|
+
when 'au'
|
|
49
|
+
"https://api.au.sumologic.com/api/#{API_VERSION}"
|
|
50
|
+
else
|
|
51
|
+
"https://api.#{@deployment}.sumologic.com/api/#{API_VERSION}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module Sumologic
|
|
6
|
+
module Http
|
|
7
|
+
# Handles authentication header generation for Sumo Logic API
|
|
8
|
+
class Authenticator
|
|
9
|
+
def initialize(access_id:, access_key:)
|
|
10
|
+
@access_id = access_id
|
|
11
|
+
@access_key = access_key
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def auth_header
|
|
15
|
+
encoded = Base64.strict_encode64("#{@access_id}:#{@access_key}")
|
|
16
|
+
"Basic #{encoded}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module Sumologic
|
|
8
|
+
module Http
|
|
9
|
+
# Handles HTTP communication with Sumo Logic API
|
|
10
|
+
# Responsibilities: request execution, error handling, SSL configuration
|
|
11
|
+
class Client
|
|
12
|
+
READ_TIMEOUT = 60
|
|
13
|
+
OPEN_TIMEOUT = 10
|
|
14
|
+
|
|
15
|
+
def initialize(base_url:, authenticator:)
|
|
16
|
+
@base_url = base_url
|
|
17
|
+
@authenticator = authenticator
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Execute HTTP request with error handling
|
|
21
|
+
def request(method:, path:, body: nil, query_params: nil)
|
|
22
|
+
uri = build_uri(path, query_params)
|
|
23
|
+
request = build_request(method, uri, body)
|
|
24
|
+
|
|
25
|
+
response = execute_request(uri, request)
|
|
26
|
+
handle_response(response)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def build_uri(path, query_params)
|
|
32
|
+
uri = URI("#{@base_url}#{path}")
|
|
33
|
+
uri.query = URI.encode_www_form(query_params) if query_params
|
|
34
|
+
uri
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def build_request(method, uri, body)
|
|
38
|
+
request_class = case method
|
|
39
|
+
when :get then Net::HTTP::Get
|
|
40
|
+
when :post then Net::HTTP::Post
|
|
41
|
+
when :delete then Net::HTTP::Delete
|
|
42
|
+
else raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
request = request_class.new(uri)
|
|
46
|
+
request['Authorization'] = @authenticator.auth_header
|
|
47
|
+
request['Accept'] = 'application/json'
|
|
48
|
+
|
|
49
|
+
if body
|
|
50
|
+
request['Content-Type'] = 'application/json'
|
|
51
|
+
request.body = body.to_json
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
request
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def execute_request(uri, request)
|
|
58
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
59
|
+
http.use_ssl = true
|
|
60
|
+
http.read_timeout = READ_TIMEOUT
|
|
61
|
+
http.open_timeout = OPEN_TIMEOUT
|
|
62
|
+
|
|
63
|
+
http.request(request)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def handle_response(response)
|
|
67
|
+
case response.code.to_i
|
|
68
|
+
when 200..299
|
|
69
|
+
JSON.parse(response.body)
|
|
70
|
+
when 401, 403
|
|
71
|
+
raise AuthenticationError, "Authentication failed: #{response.body}"
|
|
72
|
+
when 429
|
|
73
|
+
raise Error, "Rate limit exceeded: #{response.body}"
|
|
74
|
+
else
|
|
75
|
+
raise Error, "HTTP #{response.code}: #{response.body}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sumologic
|
|
4
|
+
module Metadata
|
|
5
|
+
# Handles collector metadata operations
|
|
6
|
+
class Collector
|
|
7
|
+
def initialize(http_client:)
|
|
8
|
+
@http = http_client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# List all collectors
|
|
12
|
+
# Returns array of collector objects
|
|
13
|
+
def list
|
|
14
|
+
data = @http.request(
|
|
15
|
+
method: :get,
|
|
16
|
+
path: '/collectors'
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
collectors = data['collectors'] || []
|
|
20
|
+
log_info "Found #{collectors.size} collectors"
|
|
21
|
+
collectors
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
raise Error, "Failed to list collectors: #{e.message}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def log_info(message)
|
|
29
|
+
warn "[Sumologic::Metadata::Collector] #{message}" if ENV['SUMO_DEBUG'] || $DEBUG
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|