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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +0 -0
  3. data/.gitignore +0 -2
  4. data/.travis.yml +7 -0
  5. data/README.md +54 -27
  6. data/Rakefile +8 -3
  7. data/bin/sumo +2 -69
  8. data/lib/sumo/cli.rb +43 -0
  9. data/lib/sumo/client.rb +82 -0
  10. data/lib/sumo/collection.rb +97 -0
  11. data/lib/sumo/config.rb +11 -14
  12. data/lib/sumo/error.rb +8 -11
  13. data/lib/sumo/search.rb +69 -0
  14. data/lib/sumo/version.rb +9 -2
  15. data/lib/sumo.rb +52 -18
  16. data/spec/fixtures/sumo-creds +1 -0
  17. data/spec/lib/sumo/client_spec.rb +136 -0
  18. data/spec/lib/sumo/config_spec.rb +55 -74
  19. data/spec/lib/sumo/search_spec.rb +106 -0
  20. data/spec/lib/sumo_spec.rb +0 -30
  21. data/spec/spec_helper.rb +5 -7
  22. data/spec/support/vcr.rb +41 -5
  23. data/spec/vcr/Sumo_Search/_create/sets_the_id_and_client.yml +42 -0
  24. data/spec/vcr/Sumo_Search/_delete_/deletes_the_search.yml +102 -0
  25. data/spec/vcr/Sumo_Search/_messages/returns_an_Enumerator_of_each_message_in_the_search.yml +2579 -0
  26. data/spec/vcr/Sumo_Search/_records/returns_an_Enumerator_of_each_record_in_the_search.yml +2348 -0
  27. data/spec/vcr/Sumo_Search/_status/returns_the_status_of_the_search.yml +71 -0
  28. data/sumo-search.gemspec +5 -6
  29. metadata +62 -57
  30. data/lib/sumo/formatter.rb +0 -21
  31. data/lib/sumo/query_builder.rb +0 -61
  32. data/spec/fixtures/sumo_creds +0 -1
  33. data/spec/lib/sumo/formatter_spec.rb +0 -51
  34. data/spec/lib/sumo/query_builder_spec.rb +0 -128
  35. data/spec/vcr/Sumo/_search/when_the_credentials_can_be_found/and_the_query_is_valid/parses_the_response.yml +0 -49
  36. data/spec/vcr/Sumo/_search/when_the_credentials_can_be_found/but_the_query_is_invalid/raises_an_error.yml +0 -43
  37. data/spec/vcr/Sumo_QueryBuilder/integration/when_the_request_is_invalid/raises_an_error.yml +0 -43
  38. 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: 193a26c19dd5ba23174ee1bbb198c691d7374744
4
- data.tar.gz: 52a5b12148a20750ac5a1d3213c082560e8fbfce
3
+ metadata.gz: 62af7dc96394b1bea0a7bff6a0f92e0b7a8c9ba8
4
+ data.tar.gz: e22438c39474302d64d49765e4833c7be5821eec
5
5
  SHA512:
6
- metadata.gz: 302b0a034afc2813476b992ce98fb5fd74d85b9094945080738d82c4150e208ea14c05dcaf1a9da73bf9315be4cbafbce7ad2faf47216ae43d39a6377b2dfb54
7
- data.tar.gz: f82388e8f5adbde5107e54e6ba7a0e8938586bcd882b60249e719808d56b67fde7989ae6dcfc19adafb510315b6ed202166d6396abc19227e3dfe1d78a1b0700
6
+ metadata.gz: 8cb8205eb109d953c1764cc293f4134776b882630747ca2e9ef5b46dc326a17c5e89f46ffec02ca36e1ba1d59215e66c57f81fc89ac7b41c99b5865ca10022ab
7
+ data.tar.gz: 52ac1a59652216c58d6c873333605589c5621a6e221f6ccb91424fdd62a6614650b7b37873d178c36ccfc6e1fc21990999eee29d8ea5447607b0f5e3b2ad8900
data/.cane ADDED
File without changes
data/.gitignore CHANGED
@@ -2,5 +2,3 @@
2
2
  *.swp
3
3
  *.gem
4
4
  Gemfile.lock
5
- coverage/
6
- .rbenv-version
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1.0
7
+ script: bundle exec rake --trace
data/README.md CHANGED
@@ -1,45 +1,72 @@
1
1
  # sumo-search
2
2
 
3
- This library can be used to query Sumo Logic's search API.
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
- ```shell
10
- $ sudo gem install sumo-search
10
+ ```bash
11
+ $ [sudo] gem install sumo-search
11
12
  ```
12
13
 
13
- Installing this gem will install a script called `sumo`.
14
- Note that this gem requires a Ruby version of at least 2.0.0.
14
+ From your application's `Gemfile`:
15
15
 
16
- ## Configuration
16
+ ```ruby
17
+ gem 'sumo-search'
18
+ ```
19
+
20
+ ## Ruby Usage
17
21
 
18
- To use this script, you must supply your Sumo Logic credentials.
19
- The script can read credentials from one of two places; either an environment variable or a configuration file.
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
- username@website:password
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
- ## Usage
40
+ Similarly, iterating through the records can be acheived through the `#records` method.
28
41
 
29
- ```shell
30
- $ sumo [*options/flags*]
42
+ ```ruby
43
+ search.records.each { |record| puts record }
31
44
  ```
32
45
 
33
- This script wraps the [Sumo Search API](https://github.com/SumoLogic/sumo-api-doc/wiki/search-api).
34
- The API only has one endpoint, `/api/v1/logs/search` which searches your logs.
35
-
36
- | Command Line Option | Corresponding Query Parameter | Description |
37
- |------------------------|-------------------------------|--------------------------------------|
38
- | -q, --query QUERY | q | The query to search (required) |
39
- | -f, --from START | from | Begin searching at START (ISO8601) |
40
- | -t, --to END | to | Finish searching at END (ISO8601) |
41
- | -z, --time-zone ZONE | tz | The timezone of the search |
42
- | -c, --config-file FILE | - | Use the specified config file |
43
- | -e, --extract-key KEY | - | Extract the given key from your logs |
44
- | -h, --help | - | Display the help message |
45
- | -v, --version | - | Display the version |
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.unshift(File.join(File.dirname(__FILE__), 'lib'))
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) { |t| t.pattern = 'spec/**/*_spec.rb' }
11
- Cane::RakeTask.new(:quality) { |t| t.canefile = '.cane' }
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
- include Sumo
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
@@ -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