sumo-search 0.1.1 → 1.0.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/.cane +0 -0
- data/.gitignore +0 -2
- data/.travis.yml +7 -0
- data/README.md +54 -27
- data/Rakefile +8 -3
- data/bin/sumo +2 -69
- data/lib/sumo/cli.rb +43 -0
- data/lib/sumo/client.rb +82 -0
- data/lib/sumo/collection.rb +97 -0
- data/lib/sumo/config.rb +11 -14
- data/lib/sumo/error.rb +8 -11
- data/lib/sumo/search.rb +69 -0
- data/lib/sumo/version.rb +9 -2
- data/lib/sumo.rb +52 -18
- data/spec/fixtures/sumo-creds +1 -0
- data/spec/lib/sumo/client_spec.rb +136 -0
- data/spec/lib/sumo/config_spec.rb +55 -74
- data/spec/lib/sumo/search_spec.rb +106 -0
- data/spec/lib/sumo_spec.rb +0 -30
- data/spec/spec_helper.rb +5 -7
- data/spec/support/vcr.rb +41 -5
- data/spec/vcr/Sumo_Search/_create/sets_the_id_and_client.yml +42 -0
- data/spec/vcr/Sumo_Search/_delete_/deletes_the_search.yml +102 -0
- data/spec/vcr/Sumo_Search/_messages/returns_an_Enumerator_of_each_message_in_the_search.yml +2579 -0
- data/spec/vcr/Sumo_Search/_records/returns_an_Enumerator_of_each_record_in_the_search.yml +2348 -0
- data/spec/vcr/Sumo_Search/_status/returns_the_status_of_the_search.yml +71 -0
- data/sumo-search.gemspec +5 -6
- metadata +62 -57
- data/lib/sumo/formatter.rb +0 -21
- data/lib/sumo/query_builder.rb +0 -61
- data/spec/fixtures/sumo_creds +0 -1
- data/spec/lib/sumo/formatter_spec.rb +0 -51
- data/spec/lib/sumo/query_builder_spec.rb +0 -128
- data/spec/vcr/Sumo/_search/when_the_credentials_can_be_found/and_the_query_is_valid/parses_the_response.yml +0 -49
- data/spec/vcr/Sumo/_search/when_the_credentials_can_be_found/but_the_query_is_invalid/raises_an_error.yml +0 -43
- data/spec/vcr/Sumo_QueryBuilder/integration/when_the_request_is_invalid/raises_an_error.yml +0 -43
- data/spec/vcr/Sumo_QueryBuilder/integration/when_the_request_is_valid/compiles_and_sends_the_query_to_the_server.yml +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 62af7dc96394b1bea0a7bff6a0f92e0b7a8c9ba8
|
4
|
+
data.tar.gz: e22438c39474302d64d49765e4833c7be5821eec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8cb8205eb109d953c1764cc293f4134776b882630747ca2e9ef5b46dc326a17c5e89f46ffec02ca36e1ba1d59215e66c57f81fc89ac7b41c99b5865ca10022ab
|
7
|
+
data.tar.gz: 52ac1a59652216c58d6c873333605589c5621a6e221f6ccb91424fdd62a6614650b7b37873d178c36ccfc6e1fc21990999eee29d8ea5447607b0f5e3b2ad8900
|
data/.cane
ADDED
File without changes
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,45 +1,72 @@
|
|
1
1
|
# sumo-search
|
2
2
|
|
3
|
-
This
|
3
|
+
This gem interfaces with the Sumo Logic [Search Job API](https://github.com/SumoLogic/sumo-api-doc/wiki/search-job-api).
|
4
|
+
It may be used through native Ruby, or via a CLI that has been provided.
|
4
5
|
|
5
6
|
## Installation
|
6
7
|
|
7
8
|
From the command line:
|
8
9
|
|
9
|
-
```
|
10
|
-
$ sudo gem install sumo-search
|
10
|
+
```bash
|
11
|
+
$ [sudo] gem install sumo-search
|
11
12
|
```
|
12
13
|
|
13
|
-
|
14
|
-
Note that this gem requires a Ruby version of at least 2.0.0.
|
14
|
+
From your application's `Gemfile`:
|
15
15
|
|
16
|
-
|
16
|
+
```ruby
|
17
|
+
gem 'sumo-search'
|
18
|
+
```
|
19
|
+
|
20
|
+
## Ruby Usage
|
17
21
|
|
18
|
-
To
|
19
|
-
|
20
|
-
The environment variable is `SUMO_CREDS`, and the configuration file is `~/.sumo_creds`.
|
21
|
-
Whichever you choose, the credentials must be in the following format:
|
22
|
+
To create a search job from ruby, the `Sumo.search` method is provided.
|
23
|
+
For example, the following creates a search job for everything from the 2014-01-01:
|
22
24
|
|
25
|
+
```ruby
|
26
|
+
search = Sumo.search(
|
27
|
+
:query => '*',
|
28
|
+
:from => '2014-01-01T00:00:00',
|
29
|
+
:to => '2014-01-01T23:59:59',
|
30
|
+
:time_zone => 'UTC'
|
31
|
+
)
|
23
32
|
```
|
24
|
-
|
33
|
+
|
34
|
+
To iterate through the messages returned by the API, use the `#messages` method on the object returned by `Sumo.search`.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
search.messages.each { |message| puts message }
|
25
38
|
```
|
26
39
|
|
27
|
-
|
40
|
+
Similarly, iterating through the records can be acheived through the `#records` method.
|
28
41
|
|
29
|
-
```
|
30
|
-
|
42
|
+
```ruby
|
43
|
+
search.records.each { |record| puts record }
|
31
44
|
```
|
32
45
|
|
33
|
-
|
34
|
-
The
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
|
41
|
-
|
42
|
-
| -
|
43
|
-
| -
|
44
|
-
| -
|
45
|
-
| -
|
46
|
+
Note that the two above methods lazily grab the results in chunks, so iterating through these will take some time.
|
47
|
+
The difference between records and messages is described at the bottom of [this section](https://github.com/SumoLogic/sumo-api-doc/wiki/search-job-api#wiki-getting-the-current-search-job-status) of the api docs.
|
48
|
+
|
49
|
+
## CLI Usage
|
50
|
+
|
51
|
+
The executable packaged with this gem is called `sumo`.
|
52
|
+
|
53
|
+
| Option | Required | Description |
|
54
|
+
|------------------|----------|-------------------------------------------------|
|
55
|
+
| -q --query | `true` | The query to send to the API |
|
56
|
+
| -f --from | `true` | The start date of the query (iso8601) |
|
57
|
+
| -t --to | `true` | The end date of the query (iso8601) |
|
58
|
+
| -z --time-zone | `true` | The time zone of the start and end dates |
|
59
|
+
| -e --extract-key | `false` | Extract the given key from the returned message |
|
60
|
+
| -r --records | `false` | Print out the records, not messages |
|
61
|
+
| -v --version | `false` | Print the version and exit |
|
62
|
+
| -h --help | `false` | Print the help message and exit. |
|
63
|
+
|
64
|
+
Examples:
|
65
|
+
|
66
|
+
```bash
|
67
|
+
# Search for everything from 2014-01-01.
|
68
|
+
sumo --query '*' --from '2014-01-01T00:00:00' --to '2014-01-01T23:59:59' --time-zone 'UTC'
|
69
|
+
|
70
|
+
# Search for everything containing 'StagingFitness' in 2013, extracting the 'message' key from the response.
|
71
|
+
sumo --query 'StagingFitness' --from '2013-01-01T00:00:00' --to '2014-01-01T00:00:00' --time-zone 'UTC' --extract-key 'message'
|
72
|
+
```
|
data/Rakefile
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
$LOAD_PATH
|
1
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
|
2
2
|
|
3
3
|
require 'rake'
|
4
4
|
require 'sumo'
|
@@ -7,5 +7,10 @@ require 'cane/rake_task'
|
|
7
7
|
|
8
8
|
task :default => [:spec, :quality]
|
9
9
|
|
10
|
-
RSpec::Core::RakeTask.new(:spec)
|
11
|
-
|
10
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
11
|
+
t.pattern = 'spec/**/*_spec.rb'
|
12
|
+
end
|
13
|
+
|
14
|
+
Cane::RakeTask.new(:quality) do |cane|
|
15
|
+
cane.canefile = '.cane'
|
16
|
+
end
|
data/bin/sumo
CHANGED
@@ -1,72 +1,5 @@
|
|
1
|
-
#!/usr/bin/ruby
|
1
|
+
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'sumo'
|
4
|
-
require 'sumo/version'
|
5
|
-
require 'time'
|
6
|
-
require 'optparse'
|
7
4
|
|
8
|
-
|
9
|
-
|
10
|
-
options = {
|
11
|
-
:query_options => {},
|
12
|
-
:config_file => DEFAULT_CONFIG_FILE,
|
13
|
-
:key => nil
|
14
|
-
}
|
15
|
-
|
16
|
-
OptionParser.new do |opts|
|
17
|
-
opts.banner = 'Usage: sumo [flags/options]'
|
18
|
-
|
19
|
-
opts.separator ''
|
20
|
-
opts.separator 'Specific options:'
|
21
|
-
|
22
|
-
opts.on('-q', '--query QUERY', 'Search for QUERY (required)') do |q|
|
23
|
-
options[:query] = q
|
24
|
-
end
|
25
|
-
|
26
|
-
opts.on('-f', '--from START', 'Begin at the given ISO8601 time') do |f|
|
27
|
-
options[:query_options]['from'] = f
|
28
|
-
end
|
29
|
-
|
30
|
-
opts.on('-t', '--to END', 'Finish at the given ISO8601 time') do |t|
|
31
|
-
options[:query_options]['to'] = t
|
32
|
-
end
|
33
|
-
|
34
|
-
opts.on('-z', '--time-zone ZONE', 'Use the given time zone') do |tz|
|
35
|
-
options[:query_options]['tz'] = tz
|
36
|
-
end
|
37
|
-
|
38
|
-
opts.on('-c', '--config-file FILE',
|
39
|
-
"Use the given config file instead of #{DEFAULT_CONFIG_FILE}") do |f|
|
40
|
-
options[:config_file] = config_file
|
41
|
-
end
|
42
|
-
|
43
|
-
opts.on('-e', '--extract-key KEY', 'Extract the specified key') do |k|
|
44
|
-
options[:key] = k
|
45
|
-
end
|
46
|
-
|
47
|
-
opts.on('-h', '--help', 'Print this message') do
|
48
|
-
puts opts
|
49
|
-
exit 0
|
50
|
-
end
|
51
|
-
|
52
|
-
opts.on('-v', '--version', 'Print the version') do
|
53
|
-
puts VERSION
|
54
|
-
exit 0
|
55
|
-
end
|
56
|
-
end.parse!
|
57
|
-
|
58
|
-
if options[:query].nil?
|
59
|
-
puts 'Please specify a query'
|
60
|
-
exit 1
|
61
|
-
else
|
62
|
-
begin
|
63
|
-
puts search(options[:query],
|
64
|
-
options: options[:query_options],
|
65
|
-
key: options[:key],
|
66
|
-
config_file: options[:config_file])
|
67
|
-
exit 0
|
68
|
-
rescue StandardError => ex
|
69
|
-
puts ex.message
|
70
|
-
exit 1
|
71
|
-
end
|
72
|
-
end
|
5
|
+
Sumo::CLI.run
|
data/lib/sumo/cli.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# This class is used to define a CLI.
|
2
|
+
class Sumo::CLI < Clamp::Command
|
3
|
+
option ['-q', '--query'], 'QUERY', 'The query that will be sent to Sumo'
|
4
|
+
option ['-f', '--from'], 'FROM', 'The start time of the query (iso8601).'
|
5
|
+
option ['-t', '--to'], 'TO', 'The end time of the query (iso8601).'
|
6
|
+
option ['-z', '--time-zone'], 'TZ', 'The time zone of the FROM and TO times.'
|
7
|
+
option ['-e', '--extract-key'], 'KEY', 'The key to extract from the raw JSON.'
|
8
|
+
option ['-r', '--records'], :flag, 'Extract records instead of messages.'
|
9
|
+
option ['-v', '--version'], :flag, 'Print the version.'
|
10
|
+
|
11
|
+
# This method is called when the CLI is run.
|
12
|
+
def execute
|
13
|
+
if version?
|
14
|
+
puts Sumo::VERSION
|
15
|
+
elsif records?
|
16
|
+
search.records.each { |record| puts record }
|
17
|
+
else
|
18
|
+
search.messages.each { |message| puts format_message(message) }
|
19
|
+
end
|
20
|
+
rescue StandardError => ex
|
21
|
+
puts "#{ex.class}: #{ex.message}"
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
|
25
|
+
def format_message(message)
|
26
|
+
if extract_key.nil?
|
27
|
+
message['_raw']
|
28
|
+
else
|
29
|
+
JSON.parse(message['_raw'])[extract_key]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
private :format_message
|
33
|
+
|
34
|
+
def search
|
35
|
+
Sumo::Search.create(
|
36
|
+
:query => query,
|
37
|
+
:from => from,
|
38
|
+
:to => to,
|
39
|
+
:time_zone => time_zone
|
40
|
+
)
|
41
|
+
end
|
42
|
+
private :search
|
43
|
+
end
|
data/lib/sumo/client.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# This class has the lowest-level interface to interact with the Sumo Job API.
|
2
|
+
class Sumo::Client
|
3
|
+
include Sumo::Error
|
4
|
+
|
5
|
+
attr_reader :creds, :cookie
|
6
|
+
|
7
|
+
# The error message raised when the result can be parsed from Sumo.
|
8
|
+
DEFAULT_ERROR_MESSAGE = 'Error sending API request'
|
9
|
+
|
10
|
+
# Create a new `Sumo::Client` with the given credentials.
|
11
|
+
def initialize(creds = Sumo.creds)
|
12
|
+
@creds = creds.freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
# Send a HTTP request to the server, handling any errors that may occur.
|
16
|
+
def request(hash, &block)
|
17
|
+
response = connection.request(add_defaults(hash), &block)
|
18
|
+
handle_errors!(response)
|
19
|
+
set_cookie!(response)
|
20
|
+
response.body
|
21
|
+
end
|
22
|
+
|
23
|
+
# Define methods for the HTTP methods used by the API (#get, #post, and
|
24
|
+
# #delete).
|
25
|
+
[:get, :post, :delete].each do |http_method|
|
26
|
+
define_method(http_method) do |hash, &block|
|
27
|
+
request(hash.merge(:method => http_method), &block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Private functions that operate on the request and response.
|
32
|
+
|
33
|
+
def add_defaults(hash)
|
34
|
+
hash.merge(
|
35
|
+
:headers => default_headers.merge(hash[:headers] || {}),
|
36
|
+
:path => "/api/v#{Sumo::API_VERSION}#{hash[:path]}"
|
37
|
+
)
|
38
|
+
end
|
39
|
+
private :add_defaults
|
40
|
+
|
41
|
+
def handle_errors!(response)
|
42
|
+
case response.status
|
43
|
+
when 400..499 then raise ClientError, extract_error_message(response.body)
|
44
|
+
when 500..599 then raise ServerError, extract_error_message(response.body)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
private :handle_errors!
|
48
|
+
|
49
|
+
def set_cookie!(response)
|
50
|
+
@cookie = response.headers['Set-Cookie'] || @cookie
|
51
|
+
end
|
52
|
+
private :set_cookie!
|
53
|
+
|
54
|
+
def extract_error_message(body)
|
55
|
+
JSON.parse(body)['message'] || DEFAULT_ERROR_MESSAGE
|
56
|
+
rescue
|
57
|
+
DEFAULT_ERROR_MESSAGE
|
58
|
+
end
|
59
|
+
private :extract_error_message
|
60
|
+
|
61
|
+
def default_headers
|
62
|
+
{
|
63
|
+
'Authorization' => "Basic #{encoded_creds}",
|
64
|
+
'Content-Type' => 'application/json',
|
65
|
+
'Cookie' => cookie,
|
66
|
+
'Accept' => 'application/json'
|
67
|
+
}.reject { |_, value| value.nil? }
|
68
|
+
end
|
69
|
+
private :default_headers
|
70
|
+
|
71
|
+
def encoded_creds
|
72
|
+
@encoded_creds ||= Base64.encode64(creds).strip
|
73
|
+
end
|
74
|
+
private :encoded_creds
|
75
|
+
|
76
|
+
def connection
|
77
|
+
@connection ||= Excon.new(
|
78
|
+
'https://api.sumologic.com'
|
79
|
+
)
|
80
|
+
end
|
81
|
+
private :connection
|
82
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# This class is used to un-paginate results from the API. Specifically, this
|
2
|
+
# is currently used to page through records and messages returned by the API.
|
3
|
+
class Sumo::Collection
|
4
|
+
include Enumerable
|
5
|
+
include Sumo::Error
|
6
|
+
|
7
|
+
attr_reader :offset
|
8
|
+
|
9
|
+
# Create a new collection.
|
10
|
+
def initialize(hash = {})
|
11
|
+
@offset = hash[:offset] || 0
|
12
|
+
@get_values = hash[:get_values]
|
13
|
+
@get_status = hash[:get_status]
|
14
|
+
@count_key = hash[:count_key]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Iterate through each member of the collection, lazily making HTTP requests
|
18
|
+
# to get the next member. If no block is given, an `Enumerator` is returned.
|
19
|
+
def each(&block)
|
20
|
+
if block.nil?
|
21
|
+
enumerator
|
22
|
+
else
|
23
|
+
enumerator.each { |value| block.call(value) }
|
24
|
+
self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def enumerator
|
29
|
+
@enumerator ||= Enumerator.new do |values|
|
30
|
+
page.each { |value| values << value }
|
31
|
+
remaining.each { |value| values << value } if has_next_page?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
private :enumerator
|
35
|
+
|
36
|
+
def values(hash)
|
37
|
+
@get_values.call(hash)
|
38
|
+
end
|
39
|
+
private :values
|
40
|
+
|
41
|
+
def status
|
42
|
+
@status ||= get_new_status
|
43
|
+
end
|
44
|
+
private :status
|
45
|
+
|
46
|
+
def get_new_status
|
47
|
+
stat = { 'state' => '', @count_key => @offset }
|
48
|
+
until (@offset < stat[@count_key]) || stat['state'].start_with?('DONE')
|
49
|
+
stat = @get_status.call
|
50
|
+
sleep 1
|
51
|
+
end
|
52
|
+
stat
|
53
|
+
end
|
54
|
+
private :get_new_status
|
55
|
+
|
56
|
+
def total
|
57
|
+
status[@count_key]
|
58
|
+
end
|
59
|
+
private :total
|
60
|
+
|
61
|
+
def state
|
62
|
+
status['state']
|
63
|
+
end
|
64
|
+
private :state
|
65
|
+
|
66
|
+
def page
|
67
|
+
@page ||= has_results? ? values(:offset => offset, :limit => limit) : []
|
68
|
+
end
|
69
|
+
private :page
|
70
|
+
|
71
|
+
def has_results?
|
72
|
+
limit > 0
|
73
|
+
end
|
74
|
+
|
75
|
+
def limit
|
76
|
+
@limit ||= begin
|
77
|
+
natural_limit = total - offset
|
78
|
+
(natural_limit <= 1000) ? natural_limit : 1000
|
79
|
+
end
|
80
|
+
end
|
81
|
+
private :limit
|
82
|
+
|
83
|
+
def has_next_page?
|
84
|
+
['GATHERING RESULTS', 'NOT STARTED'].include?(state)
|
85
|
+
end
|
86
|
+
private :has_next_page?
|
87
|
+
|
88
|
+
def remaining
|
89
|
+
@remaining ||= Sumo::Collection.new(
|
90
|
+
:offset => offset + limit,
|
91
|
+
:get_values => @get_values,
|
92
|
+
:get_status => @get_status,
|
93
|
+
:count_key => @count_key
|
94
|
+
)
|
95
|
+
end
|
96
|
+
private :remaining
|
97
|
+
end
|