sumologic-query 1.3.2 → 1.3.4
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/CHANGELOG.md +164 -5
- data/README.md +77 -35
- data/lib/sumologic/cli/commands/base_command.rb +57 -0
- data/lib/sumologic/cli/commands/list_collectors_command.rb +22 -0
- data/lib/sumologic/cli/commands/list_sources_command.rb +53 -0
- data/lib/sumologic/cli/commands/search_command.rb +107 -0
- data/lib/sumologic/cli.rb +34 -158
- data/lib/sumologic/http/client.rb +27 -50
- data/lib/sumologic/http/connection_pool.rb +14 -0
- data/lib/sumologic/http/cookie_jar.rb +52 -0
- data/lib/sumologic/http/debug_logger.rb +46 -0
- data/lib/sumologic/http/request_builder.rb +63 -0
- data/lib/sumologic/http/response_handler.rb +42 -0
- data/lib/sumologic/utils/time_parser.rb +147 -0
- data/lib/sumologic/version.rb +1 -1
- metadata +11 -2
data/lib/sumologic/cli.rb
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require 'thor'
|
|
4
4
|
require 'json'
|
|
5
|
+
require_relative 'cli/commands/search_command'
|
|
6
|
+
require_relative 'cli/commands/list_collectors_command'
|
|
7
|
+
require_relative 'cli/commands/list_sources_command'
|
|
5
8
|
|
|
6
9
|
module Sumologic
|
|
7
10
|
# Thor-based CLI for Sumo Logic query tool
|
|
11
|
+
# Delegates commands to specialized command classes
|
|
8
12
|
class CLI < Thor
|
|
9
13
|
class_option :debug, type: :boolean, aliases: '-d', desc: 'Enable debug output'
|
|
10
14
|
class_option :output, type: :string, aliases: '-o', desc: 'Output file (default: stdout)'
|
|
@@ -13,49 +17,51 @@ module Sumologic
|
|
|
13
17
|
true
|
|
14
18
|
end
|
|
15
19
|
|
|
20
|
+
def initialize(*args)
|
|
21
|
+
super
|
|
22
|
+
$DEBUG = true if options[:debug]
|
|
23
|
+
end
|
|
24
|
+
|
|
16
25
|
desc 'search', 'Search Sumo Logic logs'
|
|
17
26
|
long_desc <<~DESC
|
|
18
27
|
Search Sumo Logic logs using a query string.
|
|
19
28
|
|
|
29
|
+
Time Formats:
|
|
30
|
+
--from and --to support multiple formats:
|
|
31
|
+
• 'now' - current time
|
|
32
|
+
• Relative: '-30s', '-5m', '-2h', '-7d', '-1w', '-1M' (sec/min/hour/day/week/month)
|
|
33
|
+
• Unix timestamp: '1700000000' (seconds since epoch)
|
|
34
|
+
• ISO 8601: '2025-11-13T14:00:00'
|
|
35
|
+
|
|
20
36
|
Examples:
|
|
21
|
-
#
|
|
37
|
+
# Last 30 minutes
|
|
38
|
+
sumo-query search --query 'error' --from '-30m' --to 'now'
|
|
39
|
+
|
|
40
|
+
# Last hour with ISO format
|
|
22
41
|
sumo-query search --query 'error | timeslice 5m | count' \\
|
|
23
42
|
--from '2025-11-13T14:00:00' --to '2025-11-13T15:00:00'
|
|
24
43
|
|
|
25
|
-
#
|
|
44
|
+
# Last 7 days
|
|
26
45
|
sumo-query search --query '"connection timeout"' \\
|
|
27
|
-
--from '
|
|
28
|
-
|
|
46
|
+
--from '-7d' --to 'now' --limit 100
|
|
47
|
+
|
|
48
|
+
# Using Unix timestamps
|
|
49
|
+
sumo-query search --query 'error' \\
|
|
50
|
+
--from '1700000000' --to '1700003600'
|
|
29
51
|
|
|
30
52
|
# Interactive mode with FZF
|
|
31
53
|
sumo-query search --query 'error' \\
|
|
32
|
-
--from '
|
|
33
|
-
--interactive
|
|
54
|
+
--from '-1h' --to 'now' --interactive
|
|
34
55
|
DESC
|
|
35
56
|
option :query, type: :string, required: true, aliases: '-q', desc: 'Search query'
|
|
36
|
-
option :from, type: :string, required: true, aliases: '-f', desc: 'Start time (ISO 8601)'
|
|
37
|
-
option :to, type: :string, required: true, aliases: '-t', desc: 'End time (ISO 8601)'
|
|
38
|
-
option :time_zone, type: :string, default: 'UTC', aliases: '-z',
|
|
57
|
+
option :from, type: :string, required: true, aliases: '-f', desc: 'Start time (now, -30m, unix timestamp, ISO 8601)'
|
|
58
|
+
option :to, type: :string, required: true, aliases: '-t', desc: 'End time (now, -30m, unix timestamp, ISO 8601)'
|
|
59
|
+
option :time_zone, type: :string, default: 'UTC', aliases: '-z',
|
|
60
|
+
desc: 'Time zone (UTC, EST, AEST, +00:00, America/New_York, Australia/Sydney)'
|
|
39
61
|
option :limit, type: :numeric, aliases: '-l', desc: 'Maximum messages to return'
|
|
40
62
|
option :interactive, type: :boolean, aliases: '-i', desc: 'Launch interactive browser (requires fzf)'
|
|
41
63
|
def search
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
client = create_client
|
|
45
|
-
|
|
46
|
-
log_search_info
|
|
47
|
-
results = execute_search(client)
|
|
48
|
-
|
|
49
|
-
warn '=' * 60
|
|
50
|
-
warn "Results: #{results.size} messages"
|
|
51
|
-
warn '=' * 60
|
|
52
|
-
$stderr.puts
|
|
53
|
-
|
|
54
|
-
if options[:interactive]
|
|
55
|
-
launch_interactive_mode(results)
|
|
56
|
-
else
|
|
57
|
-
output_search_results(results)
|
|
58
|
-
end
|
|
64
|
+
Commands::SearchCommand.new(options, create_client).execute
|
|
59
65
|
end
|
|
60
66
|
|
|
61
67
|
desc 'list-collectors', 'List all Sumo Logic collectors'
|
|
@@ -66,17 +72,7 @@ module Sumologic
|
|
|
66
72
|
sumo-query list-collectors --output collectors.json
|
|
67
73
|
DESC
|
|
68
74
|
def list_collectors
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
client = create_client
|
|
72
|
-
|
|
73
|
-
warn 'Fetching collectors...'
|
|
74
|
-
collectors = client.list_collectors
|
|
75
|
-
|
|
76
|
-
output_json(
|
|
77
|
-
total: collectors.size,
|
|
78
|
-
collectors: collectors.map { |c| format_collector(c) }
|
|
79
|
-
)
|
|
75
|
+
Commands::ListCollectorsCommand.new(options, create_client).execute
|
|
80
76
|
end
|
|
81
77
|
|
|
82
78
|
desc 'list-sources', 'List sources from collectors'
|
|
@@ -92,15 +88,7 @@ module Sumologic
|
|
|
92
88
|
DESC
|
|
93
89
|
option :collector_id, type: :string, desc: 'Collector ID to list sources for'
|
|
94
90
|
def list_sources
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
client = create_client
|
|
98
|
-
|
|
99
|
-
if options[:collector_id]
|
|
100
|
-
list_sources_for_collector(client, options[:collector_id])
|
|
101
|
-
else
|
|
102
|
-
list_all_sources(client)
|
|
103
|
-
end
|
|
91
|
+
Commands::ListSourcesCommand.new(options, create_client).execute
|
|
104
92
|
end
|
|
105
93
|
|
|
106
94
|
desc 'version', 'Show version information'
|
|
@@ -127,120 +115,8 @@ module Sumologic
|
|
|
127
115
|
exit 1
|
|
128
116
|
end
|
|
129
117
|
|
|
130
|
-
def list_sources_for_collector(client, collector_id)
|
|
131
|
-
warn "Fetching sources for collector: #{collector_id}"
|
|
132
|
-
sources = client.list_sources(collector_id: collector_id)
|
|
133
|
-
|
|
134
|
-
output_json(
|
|
135
|
-
collector_id: collector_id,
|
|
136
|
-
total: sources.size,
|
|
137
|
-
sources: sources.map { |s| format_source(s) }
|
|
138
|
-
)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def list_all_sources(client)
|
|
142
|
-
warn 'Fetching all sources from all collectors...'
|
|
143
|
-
warn 'This may take a minute...'
|
|
144
|
-
|
|
145
|
-
all_sources = client.list_all_sources
|
|
146
|
-
|
|
147
|
-
output_json(
|
|
148
|
-
total_collectors: all_sources.size,
|
|
149
|
-
total_sources: all_sources.sum { |c| c['sources'].size },
|
|
150
|
-
data: all_sources.map do |item|
|
|
151
|
-
{
|
|
152
|
-
collector: item['collector'],
|
|
153
|
-
sources: item['sources'].map { |s| format_source(s) }
|
|
154
|
-
}
|
|
155
|
-
end
|
|
156
|
-
)
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def format_collector(collector)
|
|
160
|
-
{
|
|
161
|
-
id: collector['id'],
|
|
162
|
-
name: collector['name'],
|
|
163
|
-
collectorType: collector['collectorType'],
|
|
164
|
-
alive: collector['alive'],
|
|
165
|
-
category: collector['category']
|
|
166
|
-
}
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def format_source(source)
|
|
170
|
-
{
|
|
171
|
-
id: source['id'],
|
|
172
|
-
name: source['name'],
|
|
173
|
-
category: source['category'],
|
|
174
|
-
sourceType: source['sourceType'],
|
|
175
|
-
alive: source['alive']
|
|
176
|
-
}
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def output_json(data)
|
|
180
|
-
json_output = JSON.pretty_generate(data)
|
|
181
|
-
|
|
182
|
-
if options[:output]
|
|
183
|
-
File.write(options[:output], json_output)
|
|
184
|
-
warn "\nResults saved to: #{options[:output]}"
|
|
185
|
-
else
|
|
186
|
-
puts json_output
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
118
|
def error(message)
|
|
191
119
|
warn message
|
|
192
120
|
end
|
|
193
|
-
|
|
194
|
-
def log_search_info
|
|
195
|
-
warn '=' * 60
|
|
196
|
-
warn 'Sumo Logic Search Query'
|
|
197
|
-
warn '=' * 60
|
|
198
|
-
warn "Time Range: #{options[:from]} to #{options[:to]}"
|
|
199
|
-
warn "Query: #{options[:query]}"
|
|
200
|
-
warn "Limit: #{options[:limit] || 'unlimited'}"
|
|
201
|
-
warn '-' * 60
|
|
202
|
-
warn 'Creating search job...'
|
|
203
|
-
$stderr.puts
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def execute_search(client)
|
|
207
|
-
client.search(
|
|
208
|
-
query: options[:query],
|
|
209
|
-
from_time: options[:from],
|
|
210
|
-
to_time: options[:to],
|
|
211
|
-
time_zone: options[:time_zone],
|
|
212
|
-
limit: options[:limit]
|
|
213
|
-
)
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def output_search_results(results)
|
|
217
|
-
output_json(
|
|
218
|
-
query: options[:query],
|
|
219
|
-
from: options[:from],
|
|
220
|
-
to: options[:to],
|
|
221
|
-
time_zone: options[:time_zone],
|
|
222
|
-
message_count: results.size,
|
|
223
|
-
messages: results
|
|
224
|
-
)
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def launch_interactive_mode(results)
|
|
228
|
-
require_relative 'interactive'
|
|
229
|
-
|
|
230
|
-
# Format results for interactive mode
|
|
231
|
-
formatted_results = {
|
|
232
|
-
'query' => options[:query],
|
|
233
|
-
'from' => options[:from],
|
|
234
|
-
'to' => options[:to],
|
|
235
|
-
'time_zone' => options[:time_zone],
|
|
236
|
-
'message_count' => results.size,
|
|
237
|
-
'messages' => results
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
Sumologic::Interactive.launch(formatted_results)
|
|
241
|
-
rescue Sumologic::Interactive::Error => e
|
|
242
|
-
error e.message
|
|
243
|
-
exit 1
|
|
244
|
-
end
|
|
245
121
|
end
|
|
246
122
|
end
|
|
@@ -1,30 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'uri'
|
|
6
3
|
require_relative 'connection_pool'
|
|
4
|
+
require_relative 'debug_logger'
|
|
5
|
+
require_relative 'cookie_jar'
|
|
6
|
+
require_relative 'request_builder'
|
|
7
|
+
require_relative 'response_handler'
|
|
7
8
|
|
|
8
9
|
module Sumologic
|
|
9
10
|
module Http
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# Orchestrates HTTP communication with Sumo Logic API
|
|
12
|
+
# Delegates to specialized components for request building,
|
|
13
|
+
# response handling, connection pooling, and cookie management
|
|
13
14
|
class Client
|
|
14
15
|
def initialize(base_url:, authenticator:)
|
|
15
|
-
@
|
|
16
|
-
@
|
|
16
|
+
@cookie_jar = CookieJar.new
|
|
17
|
+
@request_builder = RequestBuilder.new(
|
|
18
|
+
base_url: base_url,
|
|
19
|
+
authenticator: authenticator,
|
|
20
|
+
cookie_jar: @cookie_jar
|
|
21
|
+
)
|
|
22
|
+
@response_handler = ResponseHandler.new
|
|
17
23
|
@connection_pool = ConnectionPool.new(base_url: base_url, max_connections: 10)
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
# Execute HTTP request with error handling
|
|
21
27
|
# Uses connection pool for thread-safe parallel execution
|
|
22
28
|
def request(method:, path:, body: nil, query_params: nil)
|
|
23
|
-
uri = build_uri(path, query_params)
|
|
24
|
-
request = build_request(method, uri, body)
|
|
29
|
+
uri = @request_builder.build_uri(path, query_params)
|
|
30
|
+
request = @request_builder.build_request(method, uri, body)
|
|
31
|
+
|
|
32
|
+
DebugLogger.log_request(method, uri, body, request.to_hash)
|
|
25
33
|
|
|
26
34
|
response = execute_request(uri, request)
|
|
27
|
-
|
|
35
|
+
|
|
36
|
+
DebugLogger.log_response(response)
|
|
37
|
+
|
|
38
|
+
@response_handler.handle(response)
|
|
28
39
|
rescue Errno::ECONNRESET, Errno::EPIPE, EOFError, Net::HTTPBadResponse => e
|
|
29
40
|
# Connection error - raise for retry at higher level
|
|
30
41
|
raise Error, "Connection error: #{e.message}"
|
|
@@ -37,49 +48,15 @@ module Sumologic
|
|
|
37
48
|
|
|
38
49
|
private
|
|
39
50
|
|
|
40
|
-
def build_uri(path, query_params)
|
|
41
|
-
uri = URI("#{@base_url}#{path}")
|
|
42
|
-
uri.query = URI.encode_www_form(query_params) if query_params
|
|
43
|
-
uri
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def build_request(method, uri, body)
|
|
47
|
-
request_class = case method
|
|
48
|
-
when :get then Net::HTTP::Get
|
|
49
|
-
when :post then Net::HTTP::Post
|
|
50
|
-
when :delete then Net::HTTP::Delete
|
|
51
|
-
else raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
request = request_class.new(uri)
|
|
55
|
-
request['Authorization'] = @authenticator.auth_header
|
|
56
|
-
request['Accept'] = 'application/json'
|
|
57
|
-
|
|
58
|
-
if body
|
|
59
|
-
request['Content-Type'] = 'application/json'
|
|
60
|
-
request.body = body.to_json
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
request
|
|
64
|
-
end
|
|
65
|
-
|
|
66
51
|
def execute_request(uri, request)
|
|
67
|
-
@connection_pool.with_connection(uri) do |http|
|
|
52
|
+
response = @connection_pool.with_connection(uri) do |http|
|
|
68
53
|
http.request(request)
|
|
69
54
|
end
|
|
70
|
-
end
|
|
71
55
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
when 401, 403
|
|
77
|
-
raise AuthenticationError, "Authentication failed: #{response.body}"
|
|
78
|
-
when 429
|
|
79
|
-
raise Error, "Rate limit exceeded: #{response.body}"
|
|
80
|
-
else
|
|
81
|
-
raise Error, "HTTP #{response.code}: #{response.body}"
|
|
82
|
-
end
|
|
56
|
+
# Store cookies from response for subsequent requests
|
|
57
|
+
@cookie_jar.store_from_response(response)
|
|
58
|
+
|
|
59
|
+
response
|
|
83
60
|
end
|
|
84
61
|
end
|
|
85
62
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
3
5
|
module Sumologic
|
|
4
6
|
module Http
|
|
5
7
|
# Thread-safe connection pool for HTTP clients
|
|
@@ -84,10 +86,22 @@ module Sumologic
|
|
|
84
86
|
http.read_timeout = READ_TIMEOUT
|
|
85
87
|
http.open_timeout = OPEN_TIMEOUT
|
|
86
88
|
http.keep_alive_timeout = 30
|
|
89
|
+
|
|
90
|
+
# SSL configuration
|
|
91
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
92
|
+
http.cert_store = ssl_cert_store
|
|
93
|
+
|
|
87
94
|
http.start
|
|
88
95
|
http
|
|
89
96
|
end
|
|
90
97
|
|
|
98
|
+
def ssl_cert_store
|
|
99
|
+
# Use system's default certificate store
|
|
100
|
+
store = OpenSSL::X509::Store.new
|
|
101
|
+
store.set_default_paths
|
|
102
|
+
store
|
|
103
|
+
end
|
|
104
|
+
|
|
91
105
|
def create_temporary_connection(uri)
|
|
92
106
|
# Fallback: create a temporary connection if pool is exhausted
|
|
93
107
|
create_connection(uri)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sumologic
|
|
4
|
+
module Http
|
|
5
|
+
# Simple cookie jar for storing and managing HTTP cookies
|
|
6
|
+
# Handles Set-Cookie response headers and Cookie request headers
|
|
7
|
+
class CookieJar
|
|
8
|
+
def initialize
|
|
9
|
+
@cookies = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Store cookies from response Set-Cookie headers
|
|
13
|
+
def store_from_response(response)
|
|
14
|
+
return unless response['Set-Cookie']
|
|
15
|
+
|
|
16
|
+
Array(response['Set-Cookie']).each do |cookie_header|
|
|
17
|
+
parse_and_store(cookie_header)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Format cookies for Cookie request header
|
|
22
|
+
# Returns nil if no cookies stored
|
|
23
|
+
def to_header
|
|
24
|
+
return nil if @cookies.empty?
|
|
25
|
+
|
|
26
|
+
@cookies.map { |name, value| "#{name}=#{value}" }.join('; ')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check if any cookies are stored
|
|
30
|
+
def any?
|
|
31
|
+
@cookies.any?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Clear all stored cookies
|
|
35
|
+
def clear
|
|
36
|
+
@cookies.clear
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def parse_and_store(cookie_header)
|
|
42
|
+
# Parse cookie name=value (ignore path, domain, expires, etc.)
|
|
43
|
+
# Example: "session_id=abc123; Path=/; HttpOnly"
|
|
44
|
+
return unless cookie_header =~ /^([^=]+)=([^;]+)/
|
|
45
|
+
|
|
46
|
+
name = Regexp.last_match(1).strip
|
|
47
|
+
value = Regexp.last_match(2).strip
|
|
48
|
+
@cookies[name] = value
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Sumologic
|
|
6
|
+
module Http
|
|
7
|
+
# Handles debug logging for HTTP requests and responses
|
|
8
|
+
# Only logs when $DEBUG is enabled
|
|
9
|
+
module DebugLogger
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def log_request(method, uri, body, headers = {})
|
|
13
|
+
return unless $DEBUG
|
|
14
|
+
|
|
15
|
+
warn "\n[DEBUG] API Request:"
|
|
16
|
+
warn " Method: #{method.to_s.upcase}"
|
|
17
|
+
warn " URL: #{uri}"
|
|
18
|
+
warn " Cookie: #{headers['Cookie']}" if headers['Cookie']
|
|
19
|
+
log_request_body(body) if body
|
|
20
|
+
warn ''
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def log_response(response)
|
|
24
|
+
return unless $DEBUG
|
|
25
|
+
|
|
26
|
+
warn '[DEBUG] API Response:'
|
|
27
|
+
warn " Status: #{response.code} #{response.message}"
|
|
28
|
+
log_response_body(response.body)
|
|
29
|
+
warn ''
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def log_request_body(body)
|
|
33
|
+
warn " Body: #{JSON.pretty_generate(body)}"
|
|
34
|
+
rescue JSON::GeneratorError
|
|
35
|
+
warn " Body: #{body.inspect}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def log_response_body(body)
|
|
39
|
+
truncated = body.length > 500
|
|
40
|
+
display_body = truncated ? "#{body[0..500]}..." : body
|
|
41
|
+
warn " Body: #{display_body}"
|
|
42
|
+
warn " (truncated, full length: #{body.length} characters)" if truncated
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module Sumologic
|
|
8
|
+
module Http
|
|
9
|
+
# Builds HTTP requests with proper headers, authentication, and cookies
|
|
10
|
+
class RequestBuilder
|
|
11
|
+
def initialize(base_url:, authenticator:, cookie_jar:)
|
|
12
|
+
@base_url = base_url
|
|
13
|
+
@authenticator = authenticator
|
|
14
|
+
@cookie_jar = cookie_jar
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Build complete URI from path and query parameters
|
|
18
|
+
def build_uri(path, query_params = nil)
|
|
19
|
+
uri = URI("#{@base_url}#{path}")
|
|
20
|
+
uri.query = URI.encode_www_form(query_params) if query_params
|
|
21
|
+
uri
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Build HTTP request with all necessary headers
|
|
25
|
+
def build_request(method, uri, body = nil)
|
|
26
|
+
request = create_request_object(method, uri)
|
|
27
|
+
add_headers(request)
|
|
28
|
+
add_body(request, body) if body
|
|
29
|
+
request
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def create_request_object(method, uri)
|
|
35
|
+
request_class = request_class_for(method)
|
|
36
|
+
request_class.new(uri)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def request_class_for(method)
|
|
40
|
+
case method
|
|
41
|
+
when :get then Net::HTTP::Get
|
|
42
|
+
when :post then Net::HTTP::Post
|
|
43
|
+
when :delete then Net::HTTP::Delete
|
|
44
|
+
else raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def add_headers(request)
|
|
49
|
+
request['Authorization'] = @authenticator.auth_header
|
|
50
|
+
request['Accept'] = 'application/json'
|
|
51
|
+
|
|
52
|
+
# Add cookies if available
|
|
53
|
+
cookie_header = @cookie_jar.to_header
|
|
54
|
+
request['Cookie'] = cookie_header if cookie_header
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add_body(request, body)
|
|
58
|
+
request['Content-Type'] = 'application/json'
|
|
59
|
+
request.body = body.to_json
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Sumologic
|
|
6
|
+
module Http
|
|
7
|
+
# Handles HTTP response parsing and error handling
|
|
8
|
+
class ResponseHandler
|
|
9
|
+
# Parse response and handle errors
|
|
10
|
+
def handle(response)
|
|
11
|
+
case response.code.to_i
|
|
12
|
+
when 200..299
|
|
13
|
+
parse_success(response)
|
|
14
|
+
when 401, 403
|
|
15
|
+
handle_authentication_error(response)
|
|
16
|
+
when 429
|
|
17
|
+
handle_rate_limit_error(response)
|
|
18
|
+
else
|
|
19
|
+
handle_generic_error(response)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def parse_success(response)
|
|
26
|
+
JSON.parse(response.body)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def handle_authentication_error(response)
|
|
30
|
+
raise AuthenticationError, "Authentication failed: #{response.body}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def handle_rate_limit_error(response)
|
|
34
|
+
raise Error, "Rate limit exceeded: #{response.body}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def handle_generic_error(response)
|
|
38
|
+
raise Error, "HTTP #{response.code}: #{response.body}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|