sumologic-query 1.3.5 → 1.4.0
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 +31 -0
- data/README.md +1 -1
- data/lib/sumologic/cli/commands/base_command.rb +0 -20
- data/lib/sumologic/cli/commands/{discover_sources_command.rb → discover_source_metadata_command.rb} +4 -4
- data/lib/sumologic/cli/commands/export_content_command.rb +20 -0
- data/lib/sumologic/cli/commands/get_content_command.rb +20 -0
- data/lib/sumologic/cli/commands/get_dashboard_command.rb +20 -0
- data/lib/sumologic/cli/commands/get_lookup_command.rb +20 -0
- data/lib/sumologic/cli/commands/get_monitor_command.rb +20 -0
- data/lib/sumologic/cli/commands/list_apps_command.rb +22 -0
- data/lib/sumologic/cli/commands/list_collectors_command.rb +1 -1
- data/lib/sumologic/cli/commands/list_dashboards_command.rb +22 -0
- data/lib/sumologic/cli/commands/list_fields_command.rb +27 -0
- data/lib/sumologic/cli/commands/list_folders_command.rb +55 -0
- data/lib/sumologic/cli/commands/list_health_events_command.rb +22 -0
- data/lib/sumologic/cli/commands/list_monitors_command.rb +27 -0
- data/lib/sumologic/cli/commands/list_sources_command.rb +2 -9
- data/lib/sumologic/cli/commands/search_command.rb +56 -18
- data/lib/sumologic/cli.rb +290 -12
- data/lib/sumologic/client.rb +207 -12
- data/lib/sumologic/configuration.rb +23 -9
- data/lib/sumologic/http/client.rb +76 -11
- data/lib/sumologic/http/connection_pool.rb +7 -5
- data/lib/sumologic/http/response_handler.rb +65 -1
- data/lib/sumologic/metadata/app.rb +34 -0
- data/lib/sumologic/metadata/content.rb +95 -0
- data/lib/sumologic/metadata/dashboard.rb +104 -0
- data/lib/sumologic/metadata/field.rb +49 -0
- data/lib/sumologic/metadata/folder.rb +89 -0
- data/lib/sumologic/metadata/health_event.rb +35 -0
- data/lib/sumologic/metadata/lookup_table.rb +34 -0
- data/lib/sumologic/metadata/models.rb +2 -80
- data/lib/sumologic/metadata/monitor.rb +113 -0
- data/lib/sumologic/metadata/source.rb +5 -7
- data/lib/sumologic/metadata/{dynamic_source_discovery.rb → source_metadata_discovery.rb} +7 -7
- data/lib/sumologic/version.rb +1 -1
- data/lib/sumologic.rb +23 -1
- metadata +23 -4
|
@@ -4,7 +4,8 @@ module Sumologic
|
|
|
4
4
|
# Centralized configuration for Sumo Logic client
|
|
5
5
|
class Configuration
|
|
6
6
|
attr_accessor :access_id, :access_key, :deployment, :timeout, :initial_poll_interval, :max_poll_interval,
|
|
7
|
-
:poll_backoff_factor, :max_messages_per_request, :max_workers, :request_delay
|
|
7
|
+
:poll_backoff_factor, :max_messages_per_request, :max_workers, :request_delay,
|
|
8
|
+
:connect_timeout, :read_timeout, :max_retries, :retry_base_delay, :retry_max_delay
|
|
8
9
|
|
|
9
10
|
API_VERSION = 'v1'
|
|
10
11
|
|
|
@@ -20,9 +21,16 @@ module Sumologic
|
|
|
20
21
|
@poll_backoff_factor = 1.5 # increase interval by 50% each time
|
|
21
22
|
|
|
22
23
|
# Timeouts and limits
|
|
23
|
-
@timeout = 300 # seconds (5 minutes)
|
|
24
|
+
@timeout = 300 # seconds (5 minutes) - overall operation timeout
|
|
25
|
+
@connect_timeout = ENV.fetch('SUMO_CONNECT_TIMEOUT', '10').to_i # seconds
|
|
26
|
+
@read_timeout = ENV.fetch('SUMO_READ_TIMEOUT', '60').to_i # seconds
|
|
24
27
|
@max_messages_per_request = 10_000
|
|
25
28
|
|
|
29
|
+
# Retry configuration
|
|
30
|
+
@max_retries = ENV.fetch('SUMO_MAX_RETRIES', '3').to_i
|
|
31
|
+
@retry_base_delay = ENV.fetch('SUMO_RETRY_BASE_DELAY', '1.0').to_f # seconds
|
|
32
|
+
@retry_max_delay = ENV.fetch('SUMO_RETRY_MAX_DELAY', '30.0').to_f # seconds
|
|
33
|
+
|
|
26
34
|
# Rate limiting (default: 5 workers, 250ms delay)
|
|
27
35
|
@max_workers = ENV.fetch('SUMO_MAX_WORKERS', '5').to_i
|
|
28
36
|
@request_delay = ENV.fetch('SUMO_REQUEST_DELAY', '0.25').to_f
|
|
@@ -32,6 +40,11 @@ module Sumologic
|
|
|
32
40
|
@base_url ||= build_base_url
|
|
33
41
|
end
|
|
34
42
|
|
|
43
|
+
# Base URL for v2 API endpoints (Content Library, etc.)
|
|
44
|
+
def base_url_v2
|
|
45
|
+
@base_url_v2 ||= build_base_url('v2')
|
|
46
|
+
end
|
|
47
|
+
|
|
35
48
|
def validate!
|
|
36
49
|
raise AuthenticationError, 'SUMO_ACCESS_ID not set' unless @access_id
|
|
37
50
|
raise AuthenticationError, 'SUMO_ACCESS_KEY not set' unless @access_key
|
|
@@ -39,20 +52,21 @@ module Sumologic
|
|
|
39
52
|
|
|
40
53
|
private
|
|
41
54
|
|
|
42
|
-
def build_base_url
|
|
55
|
+
def build_base_url(version = API_VERSION)
|
|
43
56
|
case @deployment
|
|
44
57
|
when /^http/
|
|
45
|
-
|
|
58
|
+
# Full URL provided - replace version if present
|
|
59
|
+
@deployment.sub(%r{/api/v\d+}, "/api/#{version}")
|
|
46
60
|
when 'us1'
|
|
47
|
-
"https://api.sumologic.com/api/#{
|
|
61
|
+
"https://api.sumologic.com/api/#{version}"
|
|
48
62
|
when 'us2'
|
|
49
|
-
"https://api.us2.sumologic.com/api/#{
|
|
63
|
+
"https://api.us2.sumologic.com/api/#{version}"
|
|
50
64
|
when 'eu'
|
|
51
|
-
"https://api.eu.sumologic.com/api/#{
|
|
65
|
+
"https://api.eu.sumologic.com/api/#{version}"
|
|
52
66
|
when 'au'
|
|
53
|
-
"https://api.au.sumologic.com/api/#{
|
|
67
|
+
"https://api.au.sumologic.com/api/#{version}"
|
|
54
68
|
else
|
|
55
|
-
"https://api.#{@deployment}.sumologic.com/api/#{
|
|
69
|
+
"https://api.#{@deployment}.sumologic.com/api/#{version}"
|
|
56
70
|
end
|
|
57
71
|
end
|
|
58
72
|
end
|
|
@@ -11,8 +11,30 @@ module Sumologic
|
|
|
11
11
|
# Orchestrates HTTP communication with Sumo Logic API
|
|
12
12
|
# Delegates to specialized components for request building,
|
|
13
13
|
# response handling, connection pooling, and cookie management
|
|
14
|
+
#
|
|
15
|
+
# Features automatic retry with exponential backoff for:
|
|
16
|
+
# - Rate limit errors (429)
|
|
17
|
+
# - Server errors (5xx)
|
|
18
|
+
# - Connection errors
|
|
14
19
|
class Client
|
|
15
|
-
|
|
20
|
+
# Errors that are safe to retry
|
|
21
|
+
RETRYABLE_EXCEPTIONS = [
|
|
22
|
+
Errno::ECONNRESET,
|
|
23
|
+
Errno::EPIPE,
|
|
24
|
+
Errno::ETIMEDOUT,
|
|
25
|
+
Errno::ECONNREFUSED,
|
|
26
|
+
EOFError,
|
|
27
|
+
Net::HTTPBadResponse,
|
|
28
|
+
Net::OpenTimeout,
|
|
29
|
+
Net::ReadTimeout
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
def initialize(base_url:, authenticator:, config: nil)
|
|
33
|
+
@config = config
|
|
34
|
+
@max_retries = config&.max_retries || 3
|
|
35
|
+
@retry_base_delay = config&.retry_base_delay || 1.0
|
|
36
|
+
@retry_max_delay = config&.retry_max_delay || 30.0
|
|
37
|
+
|
|
16
38
|
@cookie_jar = CookieJar.new
|
|
17
39
|
@request_builder = RequestBuilder.new(
|
|
18
40
|
base_url: base_url,
|
|
@@ -20,25 +42,47 @@ module Sumologic
|
|
|
20
42
|
cookie_jar: @cookie_jar
|
|
21
43
|
)
|
|
22
44
|
@response_handler = ResponseHandler.new
|
|
23
|
-
@connection_pool = ConnectionPool.new(
|
|
45
|
+
@connection_pool = ConnectionPool.new(
|
|
46
|
+
base_url: base_url,
|
|
47
|
+
max_connections: 10,
|
|
48
|
+
read_timeout: config&.read_timeout,
|
|
49
|
+
connect_timeout: config&.connect_timeout
|
|
50
|
+
)
|
|
24
51
|
end
|
|
25
52
|
|
|
26
|
-
# Execute HTTP request with
|
|
53
|
+
# Execute HTTP request with automatic retry for transient errors
|
|
27
54
|
# Uses connection pool for thread-safe parallel execution
|
|
28
55
|
def request(method:, path:, body: nil, query_params: nil)
|
|
29
56
|
uri = @request_builder.build_uri(path, query_params)
|
|
30
|
-
|
|
57
|
+
attempt = 0
|
|
31
58
|
|
|
32
|
-
|
|
59
|
+
loop do
|
|
60
|
+
attempt += 1
|
|
61
|
+
request = @request_builder.build_request(method, uri, body)
|
|
33
62
|
|
|
34
|
-
|
|
63
|
+
DebugLogger.log_request(method, uri, body, request.to_hash)
|
|
35
64
|
|
|
36
|
-
|
|
65
|
+
begin
|
|
66
|
+
response = execute_request(uri, request)
|
|
67
|
+
DebugLogger.log_response(response)
|
|
37
68
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
69
|
+
# Check if response is retryable before handling
|
|
70
|
+
if @response_handler.retryable?(response) && attempt <= @max_retries
|
|
71
|
+
delay = calculate_retry_delay(attempt, response)
|
|
72
|
+
log_retry(attempt, delay, "HTTP #{response.code}")
|
|
73
|
+
sleep(delay)
|
|
74
|
+
next
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
return @response_handler.handle(response)
|
|
78
|
+
rescue *RETRYABLE_EXCEPTIONS => e
|
|
79
|
+
raise Error, "Connection error: #{e.message}" if attempt > @max_retries
|
|
80
|
+
|
|
81
|
+
delay = calculate_retry_delay(attempt)
|
|
82
|
+
log_retry(attempt, delay, e.class.name)
|
|
83
|
+
sleep(delay)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
42
86
|
end
|
|
43
87
|
|
|
44
88
|
# Close all connections in the pool
|
|
@@ -58,6 +102,27 @@ module Sumologic
|
|
|
58
102
|
|
|
59
103
|
response
|
|
60
104
|
end
|
|
105
|
+
|
|
106
|
+
def calculate_retry_delay(attempt, response = nil)
|
|
107
|
+
# Use Retry-After header if available (for rate limits)
|
|
108
|
+
if response
|
|
109
|
+
info = @response_handler.extract_rate_limit_info(response)
|
|
110
|
+
return info[:retry_after] if info[:retry_after]&.positive?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Exponential backoff with jitter
|
|
114
|
+
base_delay = @retry_base_delay * (2**(attempt - 1))
|
|
115
|
+
jitter = rand * 0.5 * base_delay # Add up to 50% jitter
|
|
116
|
+
delay = base_delay + jitter
|
|
117
|
+
|
|
118
|
+
[delay, @retry_max_delay].min
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def log_retry(attempt, delay, reason)
|
|
122
|
+
return unless ENV['SUMO_DEBUG'] || $DEBUG
|
|
123
|
+
|
|
124
|
+
warn "[Sumologic::Http::Client] Retry #{attempt}/#{@max_retries} after #{delay.round(2)}s (#{reason})"
|
|
125
|
+
end
|
|
61
126
|
end
|
|
62
127
|
end
|
|
63
128
|
end
|
|
@@ -7,12 +7,14 @@ module Sumologic
|
|
|
7
7
|
# Thread-safe connection pool for HTTP clients
|
|
8
8
|
# Allows multiple threads to have their own connections
|
|
9
9
|
class ConnectionPool
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
DEFAULT_READ_TIMEOUT = 60
|
|
11
|
+
DEFAULT_OPEN_TIMEOUT = 10
|
|
12
12
|
|
|
13
|
-
def initialize(base_url:, max_connections: 10)
|
|
13
|
+
def initialize(base_url:, max_connections: 10, read_timeout: nil, connect_timeout: nil)
|
|
14
14
|
@base_url = base_url
|
|
15
15
|
@max_connections = max_connections
|
|
16
|
+
@read_timeout = read_timeout || DEFAULT_READ_TIMEOUT
|
|
17
|
+
@connect_timeout = connect_timeout || DEFAULT_OPEN_TIMEOUT
|
|
16
18
|
@pool = []
|
|
17
19
|
@mutex = Mutex.new
|
|
18
20
|
end
|
|
@@ -83,8 +85,8 @@ module Sumologic
|
|
|
83
85
|
def create_connection(uri)
|
|
84
86
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
85
87
|
http.use_ssl = true
|
|
86
|
-
http.read_timeout =
|
|
87
|
-
http.open_timeout =
|
|
88
|
+
http.read_timeout = @read_timeout
|
|
89
|
+
http.open_timeout = @connect_timeout
|
|
88
90
|
http.keep_alive_timeout = 30
|
|
89
91
|
|
|
90
92
|
# SSL configuration
|
|
@@ -15,14 +15,34 @@ module Sumologic
|
|
|
15
15
|
handle_authentication_error(response)
|
|
16
16
|
when 429
|
|
17
17
|
handle_rate_limit_error(response)
|
|
18
|
+
when 500..599
|
|
19
|
+
handle_server_error(response)
|
|
18
20
|
else
|
|
19
21
|
handle_generic_error(response)
|
|
20
22
|
end
|
|
21
23
|
end
|
|
22
24
|
|
|
25
|
+
# Check if response indicates a retryable error
|
|
26
|
+
def retryable?(response)
|
|
27
|
+
code = response.code.to_i
|
|
28
|
+
code == 429 || code.between?(500, 599)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Extract rate limit info from response headers
|
|
32
|
+
def extract_rate_limit_info(response)
|
|
33
|
+
{
|
|
34
|
+
retry_after: parse_retry_after(response),
|
|
35
|
+
limit: response['X-RateLimit-Limit']&.to_i,
|
|
36
|
+
remaining: response['X-RateLimit-Remaining']&.to_i,
|
|
37
|
+
reset_at: parse_reset_time(response)
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
23
41
|
private
|
|
24
42
|
|
|
25
43
|
def parse_success(response)
|
|
44
|
+
return {} if response.body.nil? || response.body.empty?
|
|
45
|
+
|
|
26
46
|
JSON.parse(response.body)
|
|
27
47
|
end
|
|
28
48
|
|
|
@@ -31,12 +51,56 @@ module Sumologic
|
|
|
31
51
|
end
|
|
32
52
|
|
|
33
53
|
def handle_rate_limit_error(response)
|
|
34
|
-
|
|
54
|
+
info = extract_rate_limit_info(response)
|
|
55
|
+
message = 'Rate limit exceeded'
|
|
56
|
+
message += " (retry after #{info[:retry_after]}s)" if info[:retry_after]
|
|
57
|
+
|
|
58
|
+
raise RateLimitError.new(
|
|
59
|
+
message,
|
|
60
|
+
retry_after: info[:retry_after],
|
|
61
|
+
limit: info[:limit],
|
|
62
|
+
remaining: info[:remaining],
|
|
63
|
+
reset_at: info[:reset_at]
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def handle_server_error(response)
|
|
68
|
+
raise Error, "Server error HTTP #{response.code}: #{response.body}"
|
|
35
69
|
end
|
|
36
70
|
|
|
37
71
|
def handle_generic_error(response)
|
|
38
72
|
raise Error, "HTTP #{response.code}: #{response.body}"
|
|
39
73
|
end
|
|
74
|
+
|
|
75
|
+
def parse_retry_after(response)
|
|
76
|
+
# Try Retry-After header first (standard HTTP)
|
|
77
|
+
retry_after = response['Retry-After']
|
|
78
|
+
return retry_after.to_i if retry_after&.match?(/^\d+$/)
|
|
79
|
+
|
|
80
|
+
# Try X-RateLimit-Reset (common alternative)
|
|
81
|
+
reset = response['X-RateLimit-Reset']
|
|
82
|
+
return nil unless reset
|
|
83
|
+
|
|
84
|
+
# Reset can be seconds or Unix timestamp
|
|
85
|
+
reset_val = reset.to_i
|
|
86
|
+
if reset_val > 1_000_000_000 # Likely a Unix timestamp
|
|
87
|
+
[reset_val - Time.now.to_i, 1].max
|
|
88
|
+
else
|
|
89
|
+
reset_val
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parse_reset_time(response)
|
|
94
|
+
reset = response['X-RateLimit-Reset']
|
|
95
|
+
return nil unless reset
|
|
96
|
+
|
|
97
|
+
reset_val = reset.to_i
|
|
98
|
+
if reset_val > 1_000_000_000 # Unix timestamp
|
|
99
|
+
Time.at(reset_val)
|
|
100
|
+
else
|
|
101
|
+
Time.now + reset_val
|
|
102
|
+
end
|
|
103
|
+
end
|
|
40
104
|
end
|
|
41
105
|
end
|
|
42
106
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'loggable'
|
|
4
|
+
|
|
5
|
+
module Sumologic
|
|
6
|
+
module Metadata
|
|
7
|
+
# Handles app catalog operations
|
|
8
|
+
# Uses GET /v1/apps endpoint
|
|
9
|
+
# Note: This lists the app catalog (available apps), not installed apps
|
|
10
|
+
class App
|
|
11
|
+
include Loggable
|
|
12
|
+
|
|
13
|
+
def initialize(http_client:)
|
|
14
|
+
@http = http_client
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# List available apps from the Sumo Logic app catalog
|
|
18
|
+
#
|
|
19
|
+
# @return [Array<Hash>] Array of app data
|
|
20
|
+
def list
|
|
21
|
+
data = @http.request(
|
|
22
|
+
method: :get,
|
|
23
|
+
path: '/apps'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
apps = data['apps'] || []
|
|
27
|
+
log_info "Fetched #{apps.size} apps from catalog"
|
|
28
|
+
apps
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
raise Error, "Failed to list apps: #{e.message}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'loggable'
|
|
4
|
+
|
|
5
|
+
module Sumologic
|
|
6
|
+
module Metadata
|
|
7
|
+
# Handles content library operations
|
|
8
|
+
# Uses v2 content API endpoints for path lookup and export
|
|
9
|
+
class Content
|
|
10
|
+
include Loggable
|
|
11
|
+
|
|
12
|
+
EXPORT_POLL_INTERVAL = 2 # seconds
|
|
13
|
+
EXPORT_MAX_WAIT = 120 # seconds
|
|
14
|
+
|
|
15
|
+
def initialize(http_client:)
|
|
16
|
+
@http = http_client
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Get a content item by its library path
|
|
20
|
+
# Returns item ID, type, name, and parent folder
|
|
21
|
+
#
|
|
22
|
+
# @param path [String] Content library path (e.g., '/Library/Users/me/My Search')
|
|
23
|
+
# @return [Hash] Content item data
|
|
24
|
+
def get_by_path(path)
|
|
25
|
+
data = @http.request(
|
|
26
|
+
method: :get,
|
|
27
|
+
path: '/content/path',
|
|
28
|
+
query_params: { path: path }
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
log_info "Retrieved content at path: #{path}"
|
|
32
|
+
data
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
raise Error, "Failed to get content at path '#{path}': #{e.message}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Export a content item as JSON
|
|
38
|
+
# Handles the async job lifecycle: start → poll → fetch result
|
|
39
|
+
#
|
|
40
|
+
# @param content_id [String] The content item ID to export
|
|
41
|
+
# @return [Hash] Exported content data
|
|
42
|
+
def export(content_id)
|
|
43
|
+
# Start export job
|
|
44
|
+
job = @http.request(
|
|
45
|
+
method: :post,
|
|
46
|
+
path: "/content/#{content_id}/export"
|
|
47
|
+
)
|
|
48
|
+
job_id = job['id']
|
|
49
|
+
log_info "Started export job #{job_id} for content #{content_id}"
|
|
50
|
+
|
|
51
|
+
# Poll until complete
|
|
52
|
+
poll_export_status(content_id, job_id)
|
|
53
|
+
|
|
54
|
+
# Fetch result
|
|
55
|
+
result = @http.request(
|
|
56
|
+
method: :get,
|
|
57
|
+
path: "/content/#{content_id}/export/#{job_id}/result"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
log_info "Export complete for content #{content_id}"
|
|
61
|
+
result
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
raise Error, "Failed to export content #{content_id}: #{e.message}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def poll_export_status(content_id, job_id)
|
|
69
|
+
start_time = Time.now
|
|
70
|
+
|
|
71
|
+
loop do
|
|
72
|
+
elapsed = Time.now - start_time
|
|
73
|
+
raise TimeoutError, "Export job timed out after #{EXPORT_MAX_WAIT}s" if elapsed > EXPORT_MAX_WAIT
|
|
74
|
+
|
|
75
|
+
status = @http.request(
|
|
76
|
+
method: :get,
|
|
77
|
+
path: "/content/#{content_id}/export/#{job_id}/status"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
state = status['status']
|
|
81
|
+
log_info "Export status: #{state}"
|
|
82
|
+
|
|
83
|
+
case state
|
|
84
|
+
when 'Success'
|
|
85
|
+
return
|
|
86
|
+
when 'Failed'
|
|
87
|
+
raise Error, "Export job failed: #{status['error']&.dig('message') || 'unknown error'}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
sleep EXPORT_POLL_INTERVAL
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'loggable'
|
|
4
|
+
require_relative 'models'
|
|
5
|
+
|
|
6
|
+
module Sumologic
|
|
7
|
+
module Metadata
|
|
8
|
+
# Handles dashboard operations via v2 API
|
|
9
|
+
# Uses GET /v2/dashboards endpoints
|
|
10
|
+
class Dashboard
|
|
11
|
+
include Loggable
|
|
12
|
+
|
|
13
|
+
def initialize(http_client:)
|
|
14
|
+
@http = http_client
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# List all dashboards
|
|
18
|
+
# Returns array of dashboard objects
|
|
19
|
+
#
|
|
20
|
+
# @param limit [Integer] Maximum number of dashboards to return (default: 100)
|
|
21
|
+
# @return [Array<Hash>] Array of dashboard data
|
|
22
|
+
def list(limit: 100)
|
|
23
|
+
dashboards = []
|
|
24
|
+
token = nil
|
|
25
|
+
|
|
26
|
+
loop do
|
|
27
|
+
query_params = { limit: [limit - dashboards.size, 100].min }
|
|
28
|
+
query_params[:token] = token if token
|
|
29
|
+
|
|
30
|
+
data = @http.request(
|
|
31
|
+
method: :get,
|
|
32
|
+
path: '/dashboards',
|
|
33
|
+
query_params: query_params
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
batch = data['dashboards'] || []
|
|
37
|
+
dashboards.concat(batch)
|
|
38
|
+
|
|
39
|
+
log_info "Fetched #{batch.size} dashboards (total: #{dashboards.size})"
|
|
40
|
+
|
|
41
|
+
# Check for pagination
|
|
42
|
+
token = data['next']
|
|
43
|
+
break if token.nil? || dashboards.size >= limit
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
dashboards.take(limit)
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
raise Error, "Failed to list dashboards: #{e.message}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Get a specific dashboard by ID
|
|
52
|
+
# Returns full dashboard details including panels
|
|
53
|
+
#
|
|
54
|
+
# @param dashboard_id [String] The dashboard ID
|
|
55
|
+
# @return [Hash] Dashboard data
|
|
56
|
+
def get(dashboard_id)
|
|
57
|
+
data = @http.request(
|
|
58
|
+
method: :get,
|
|
59
|
+
path: "/dashboards/#{dashboard_id}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
log_info "Retrieved dashboard: #{data['title']} (#{dashboard_id})"
|
|
63
|
+
data
|
|
64
|
+
rescue StandardError => e
|
|
65
|
+
raise Error, "Failed to get dashboard #{dashboard_id}: #{e.message}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Search dashboards by title or description
|
|
69
|
+
# Returns matching dashboards
|
|
70
|
+
#
|
|
71
|
+
# @param query [String] Search query
|
|
72
|
+
# @param limit [Integer] Maximum results (default: 100)
|
|
73
|
+
# @return [Array<Hash>] Matching dashboards
|
|
74
|
+
def search(query:, limit: 100)
|
|
75
|
+
# Use list and filter client-side
|
|
76
|
+
dashboards = list(limit: limit * 2)
|
|
77
|
+
query_lower = query.downcase
|
|
78
|
+
|
|
79
|
+
filtered = dashboards.select do |d|
|
|
80
|
+
title_match = d['title']&.downcase&.include?(query_lower)
|
|
81
|
+
desc_match = d['description']&.downcase&.include?(query_lower)
|
|
82
|
+
title_match || desc_match
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
filtered.take(limit)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# List dashboards in a specific folder
|
|
89
|
+
#
|
|
90
|
+
# @param folder_id [String] Folder ID to search in
|
|
91
|
+
# @param limit [Integer] Maximum results
|
|
92
|
+
# @return [Array<Hash>] Dashboards in folder
|
|
93
|
+
def list_by_folder(folder_id:, limit: 100)
|
|
94
|
+
dashboards = list(limit: limit * 2)
|
|
95
|
+
|
|
96
|
+
filtered = dashboards.select do |d|
|
|
97
|
+
d['folderId'] == folder_id
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
filtered.take(limit)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'loggable'
|
|
4
|
+
|
|
5
|
+
module Sumologic
|
|
6
|
+
module Metadata
|
|
7
|
+
# Handles field operations
|
|
8
|
+
# Uses GET /v1/fields and GET /v1/fields/builtin endpoints
|
|
9
|
+
class Field
|
|
10
|
+
include Loggable
|
|
11
|
+
|
|
12
|
+
def initialize(http_client:)
|
|
13
|
+
@http = http_client
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# List custom fields
|
|
17
|
+
#
|
|
18
|
+
# @return [Array<Hash>] Array of custom field data
|
|
19
|
+
def list
|
|
20
|
+
data = @http.request(
|
|
21
|
+
method: :get,
|
|
22
|
+
path: '/fields'
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
fields = data['data'] || []
|
|
26
|
+
log_info "Fetched #{fields.size} custom fields"
|
|
27
|
+
fields
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
raise Error, "Failed to list fields: #{e.message}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# List built-in fields
|
|
33
|
+
#
|
|
34
|
+
# @return [Array<Hash>] Array of built-in field data
|
|
35
|
+
def list_builtin
|
|
36
|
+
data = @http.request(
|
|
37
|
+
method: :get,
|
|
38
|
+
path: '/fields/builtin'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
fields = data['data'] || []
|
|
42
|
+
log_info "Fetched #{fields.size} built-in fields"
|
|
43
|
+
fields
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
raise Error, "Failed to list built-in fields: #{e.message}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'loggable'
|
|
4
|
+
require_relative 'models'
|
|
5
|
+
|
|
6
|
+
module Sumologic
|
|
7
|
+
module Metadata
|
|
8
|
+
# Handles folder/content library operations
|
|
9
|
+
# Folders organize dashboards, searches, and other content
|
|
10
|
+
# NOTE: Content API uses v2, not v1
|
|
11
|
+
class Folder
|
|
12
|
+
include Loggable
|
|
13
|
+
|
|
14
|
+
# @param http_client [Http::Client] HTTP client configured for v2 API
|
|
15
|
+
def initialize(http_client:)
|
|
16
|
+
@http = http_client
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Get the personal folder for the current user
|
|
20
|
+
# Returns folder with children
|
|
21
|
+
#
|
|
22
|
+
# @return [Hash] Personal folder data
|
|
23
|
+
def personal
|
|
24
|
+
data = @http.request(
|
|
25
|
+
method: :get,
|
|
26
|
+
path: '/content/folders/personal'
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
log_info "Retrieved personal folder: #{data['name']}"
|
|
30
|
+
data
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
raise Error, "Failed to get personal folder: #{e.message}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Get a specific folder by ID
|
|
36
|
+
# Returns folder details with children
|
|
37
|
+
#
|
|
38
|
+
# @param folder_id [String] The folder ID
|
|
39
|
+
# @return [Hash] Folder data with children
|
|
40
|
+
def get(folder_id)
|
|
41
|
+
data = @http.request(
|
|
42
|
+
method: :get,
|
|
43
|
+
path: "/content/folders/#{folder_id}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
log_info "Retrieved folder: #{data['name']} (#{folder_id})"
|
|
47
|
+
data
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
raise Error, "Failed to get folder #{folder_id}: #{e.message}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# List all items in a folder (recursive tree)
|
|
53
|
+
# Builds a tree structure of all content
|
|
54
|
+
#
|
|
55
|
+
# @param folder_id [String] Starting folder ID (nil for personal)
|
|
56
|
+
# @param max_depth [Integer] Maximum recursion depth (default: 3)
|
|
57
|
+
# @return [Hash] Folder tree with nested children
|
|
58
|
+
def tree(folder_id: nil, max_depth: 3)
|
|
59
|
+
root = folder_id ? get(folder_id) : personal
|
|
60
|
+
build_tree(root, 0, max_depth)
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
raise Error, "Failed to build folder tree: #{e.message}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def build_tree(folder, depth, max_depth)
|
|
68
|
+
return folder if depth >= max_depth
|
|
69
|
+
|
|
70
|
+
children = folder['children'] || []
|
|
71
|
+
folder['children'] = children.map do |child|
|
|
72
|
+
if child['itemType'] == 'Folder'
|
|
73
|
+
begin
|
|
74
|
+
child_folder = get(child['id'])
|
|
75
|
+
build_tree(child_folder, depth + 1, max_depth)
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
log_error "Failed to fetch child folder #{child['id']}: #{e.message}"
|
|
78
|
+
child
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
child
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
folder
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|