sumo-search 0.1.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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