wavefront-sdk 1.2.1 → 1.3.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.
Files changed (38) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +62 -72
  4. data/.travis.yml +5 -4
  5. data/README.md +10 -0
  6. data/lib/wavefront-sdk/base.rb +28 -8
  7. data/lib/wavefront-sdk/constants.rb +3 -0
  8. data/lib/wavefront-sdk/credentials.rb +36 -17
  9. data/lib/wavefront-sdk/mixins.rb +3 -22
  10. data/lib/wavefront-sdk/notificant.rb +1 -1
  11. data/lib/wavefront-sdk/parse_time.rb +58 -0
  12. data/lib/wavefront-sdk/response.rb +7 -6
  13. data/lib/wavefront-sdk/search.rb +5 -2
  14. data/lib/wavefront-sdk/user.rb +7 -0
  15. data/lib/wavefront-sdk/validators.rb +15 -7
  16. data/lib/wavefront-sdk/version.rb +1 -1
  17. data/lib/wavefront-sdk/write.rb +29 -3
  18. data/spec/.rubocop.yml +3 -0
  19. data/spec/spec_helper.rb +24 -18
  20. data/spec/wavefront-sdk/alert_spec.rb +5 -0
  21. data/spec/wavefront-sdk/base_spec.rb +9 -9
  22. data/spec/wavefront-sdk/credentials_spec.rb +123 -4
  23. data/spec/wavefront-sdk/event_spec.rb +4 -5
  24. data/spec/wavefront-sdk/externallink_spec.rb +1 -1
  25. data/spec/wavefront-sdk/maintenancewindow_spec.rb +2 -2
  26. data/spec/wavefront-sdk/metric_spec.rb +2 -2
  27. data/spec/wavefront-sdk/mixins_spec.rb +1 -1
  28. data/spec/wavefront-sdk/parse_time_spec.rb +65 -0
  29. data/spec/wavefront-sdk/resources/test2.conf +4 -0
  30. data/spec/wavefront-sdk/response_spec.rb +3 -5
  31. data/spec/wavefront-sdk/savedsearch_spec.rb +1 -1
  32. data/spec/wavefront-sdk/source_spec.rb +1 -1
  33. data/spec/wavefront-sdk/user_spec.rb +5 -3
  34. data/spec/wavefront-sdk/validators_spec.rb +65 -62
  35. data/spec/wavefront-sdk/webhook_spec.rb +1 -1
  36. data/spec/wavefront-sdk/write_spec.rb +19 -1
  37. data/wavefront-sdk.gemspec +4 -3
  38. metadata +29 -15
@@ -1,9 +1,10 @@
1
1
  language: ruby
2
2
  cache: bundler
3
3
  rvm:
4
- - 2.2.8
5
- - 2.3.5
6
- - 2.4.2
4
+ - 2.2.9
5
+ - 2.3.6
6
+ - 2.4.3
7
+ - 2.5.0
7
8
  before_install: gem install bundler --no-rdoc --no-ri
8
9
  deploy:
9
10
  provider: rubygems
@@ -13,4 +14,4 @@ deploy:
13
14
  on:
14
15
  tags: true
15
16
  repo: snltd/wavefront-sdk
16
- ruby: 2.3.5
17
+ ruby: 2.3.6
data/README.md CHANGED
@@ -69,6 +69,16 @@ wf.list.response.items.each { |user| puts user[:identifier] }
69
69
  wf.delete('lolex@oldplace.com')
70
70
  ```
71
71
 
72
+ All API classes expect `user` support pagination and will only
73
+ return blocks of results. The `everything()` method returns a lazy
74
+ enumerator to make dealing with pagination simpler.
75
+
76
+ ```ruby
77
+ Wavefront::Alert.new(c.creds).everything.each_with_index do |m, i|
78
+ puts "#{i} #{m.id}"
79
+ end
80
+ ```
81
+
72
82
  Retrieve a timeseries over the last 10 minutes, with one minute bucket
73
83
  granularity. We will describe the time as a Ruby object, but could also use
74
84
  an epoch timestamp. The SDK happily converts between the two.
@@ -21,8 +21,8 @@ module Wavefront
21
21
  class Base
22
22
  include Wavefront::Validators
23
23
  include Wavefront::Mixins
24
- attr_reader :opts, :debug, :noop, :verbose, :net, :api_base, :conn,
25
- :update_keys, :logger
24
+ attr_reader :opts, :debug, :noop, :verbose, :net, :conn, :update_keys,
25
+ :logger
26
26
 
27
27
  # Create a new API object. This will always be called from a
28
28
  # class which inherits this one. If the inheriting class defines
@@ -181,18 +181,21 @@ module Wavefront
181
181
  # :debug to DEBUG.
182
182
  #
183
183
  def log(msg, level = nil)
184
-
185
184
  if logger
186
185
  logger.send(level || :info, msg)
187
186
  else
188
- # print it unless it's a debug and we're not in debug
189
- #
190
- return if level == :debug && ! opts[:debug]
191
- return if level == :info && ! opts[:verbose]
192
- puts msg
187
+ print_message(msg, level)
193
188
  end
194
189
  end
195
190
 
191
+ # Print it unless it's a debug and we're not in debug
192
+ #
193
+ def print_message(msg, level)
194
+ return if level == :debug && ! opts[:debug]
195
+ return if level == :info && ! opts[:verbose]
196
+ puts msg
197
+ end
198
+
196
199
  # If we need to massage a raw response to fit what the
197
200
  # Wavefront::Response class expects (I'm looking at you,
198
201
  # 'User'), a class can provide a {#response_shim} method.
@@ -205,6 +208,23 @@ module Wavefront
205
208
  Wavefront::Response.new(body, resp.status, debug)
206
209
  end
207
210
 
211
+ # Return all objects using a lazy enumerator
212
+ # @return Enumerable
213
+ #
214
+ def everything
215
+ Enumerator.new do |y|
216
+ offset = 0
217
+ limit = 100
218
+
219
+ loop do
220
+ resp = api_get('', { offset: offset, limit: limit }).response
221
+ resp.items.map { |i| y.<< i }
222
+ offset += limit
223
+ raise StopIteration unless resp.moreItems == true
224
+ end
225
+ end.lazy
226
+ end
227
+
208
228
  private
209
229
 
210
230
  # Try to describe the actual HTTP calls we make. There's a bit
@@ -0,0 +1,3 @@
1
+ # Constants
2
+ #
3
+ DELTA = "\u2206" # "Increment" -- alt-J on a Mac
@@ -31,11 +31,17 @@ module Wavefront
31
31
  # and/or 'profile' which select a profile section from 'file'
32
32
  #
33
33
  def initialize(options = {})
34
- raw = load_from_file(options)
35
- raw = env_override(raw)
36
- populate(raw)
34
+ raw = load_from_file(cred_files(options), options[:profile] ||
35
+ 'default')
36
+ populate(env_override(raw))
37
37
  end
38
38
 
39
+ # If the user has set certain environment variables, their
40
+ # values will override values from the config file or
41
+ # command-line.
42
+ # @param raw [Hash] the existing credentials
43
+ # @return [Hash] the modified credentials
44
+ #
39
45
  def env_override(raw)
40
46
  { endpoint: 'WAVEFRONT_ENDPOINT',
41
47
  token: 'WAVEFRONT_TOKEN',
@@ -44,25 +50,39 @@ module Wavefront
44
50
  raw
45
51
  end
46
52
 
53
+ # Make the helper values. We use a Map so they're super-easy to
54
+ # access
55
+ #
56
+ # @param raw [Hash] the combined options from config file,
57
+ # command-line and env vars.
58
+ # @return void
59
+ #
47
60
  def populate(raw)
48
61
  @config = Map(raw)
49
62
  @creds = Map(raw.select { |k, _v| [:endpoint, :token].include?(k) })
50
63
  @proxy = Map(raw.select { |k, _v| [:proxy, :port].include?(k) })
51
64
  end
52
65
 
53
- def load_from_file(opts)
54
- ret = {}
55
-
56
- profile = opts[:profile] || 'default'
66
+ # @return [Array] a list of possible credential files
67
+ #
68
+ def cred_files(opts = {})
69
+ if opts.key?(:file)
70
+ Array(Pathname.new(opts[:file]))
71
+ else
72
+ [Pathname.new('/etc/wavefront/credentials'),
73
+ Pathname.new(ENV['HOME']) + '.wavefront']
74
+ end
75
+ end
57
76
 
58
- c_file = if opts.key?(:file)
59
- Array(Pathname.new(opts[:file]))
60
- else
61
- [Pathname.new('/etc/wavefront/credentials'),
62
- Pathname.new(ENV['HOME']) + '.wavefront']
63
- end
77
+ # @param files [Array][Pathname] a list of ini-style config files
78
+ # @param profile [String] a profile name
79
+ # @return [Hash] the given profile from the given list of files.
80
+ # If multiple files match, the last one will be used
81
+ #
82
+ def load_from_file(files, profile = 'default')
83
+ ret = {}
64
84
 
65
- c_file.each do |f|
85
+ files.each do |f|
66
86
  next unless f.exist?
67
87
  ret = load_profile(f, profile)
68
88
  ret[:file] = f
@@ -71,9 +91,8 @@ module Wavefront
71
91
  ret
72
92
  end
73
93
 
74
- # Load in configuration (optionally) given section of an
75
- # ini-style configuration file not there, we don't consider that
76
- # an error.
94
+ # Load in an (optionally) given section of an ini-style
95
+ # configuration file not there, we don't consider that an error.
77
96
  #
78
97
  # @param file [Pathname] the file to read
79
98
  # @param profile [String] the section in the config to read
@@ -1,5 +1,6 @@
1
1
  require 'date'
2
2
  require_relative './exception'
3
+ require_relative './parse_time'
3
4
 
4
5
  module Wavefront
5
6
  module Mixins
@@ -13,28 +14,8 @@ module Wavefront
13
14
  # @raise Wavefront::InvalidTimestamp
14
15
  #
15
16
  def parse_time(t, ms = false)
16
- #
17
- # Numbers, or things that look like numbers, pass straight
18
- # through. No validation, because maybe the user means one
19
- # second past the epoch, or the year 2525.
20
- #
21
- return t if t.is_a?(Numeric)
22
-
23
- if t.is_a?(String)
24
- return t.to_i if t.match(/^\d+$/)
25
-
26
- if t.start_with?('-') || t.start_with?('+')
27
- return relative_time(t, ms)
28
- end
29
-
30
- begin
31
- t = DateTime.parse("#{t} #{Time.now.getlocal.zone}")
32
- rescue
33
- raise Wavefront::Exception::InvalidTimestamp
34
- end
35
- end
36
-
37
- ms ? t.to_datetime.strftime('%Q').to_i : t.strftime('%s').to_i
17
+ return relative_time(t, ms) if t =~ /^[\-+]/
18
+ ParseTime.new(t, ms).parse!
38
19
  end
39
20
 
40
21
  # Return a timetamp described by the given string. That is,
@@ -80,5 +80,5 @@ module Wavefront
80
80
  wf_notificant_id?(id)
81
81
  api_post(['test', id].uri_concat, nil)
82
82
  end
83
- end
83
+ end
84
84
  end
@@ -0,0 +1,58 @@
1
+ module Wavefront
2
+ # Parse various times into integers. This class is not for direct
3
+ # consumption: it's used by the mixins parse_time method, which
4
+ # does all the type sanity stuff.
5
+ #
6
+ class ParseTime
7
+ attr_reader :t, :ms
8
+
9
+ # param t [Numeric] a timestamp
10
+ # param ms [Bool] whether the timestamp is in milliseconds
11
+ #
12
+ def initialize(t, ms = false)
13
+ @t = t
14
+ @ms = ms
15
+ end
16
+
17
+ # @return [Fixnum] timestamp
18
+ #
19
+ def parse_time_Fixnum
20
+ t
21
+ end
22
+
23
+ # @return [Integer] timestamp
24
+ #
25
+ def parse_time_Integer
26
+ t
27
+ end
28
+
29
+ # @return [Fixnum] timestamp
30
+ #
31
+ def parse_time_String
32
+ return t.to_i if t =~ /^\d+$/
33
+ @t = DateTime.parse("#{t} #{Time.now.getlocal.zone}")
34
+ parse_time_Time
35
+ end
36
+
37
+ # @return [Integer] timestamp
38
+ #
39
+ def parse_time_Time
40
+ if ms
41
+ t.to_datetime.strftime('%Q').to_i
42
+ else
43
+ t.strftime('%s').to_i
44
+ end
45
+ end
46
+
47
+ def parse_time_DateTime
48
+ parse_time_Time
49
+ end
50
+
51
+ def parse!
52
+ method = ('parse_time_' + t.class.name).to_sym
53
+ send(method)
54
+ rescue
55
+ raise Wavefront::Exception::InvalidTimestamp
56
+ end
57
+ end
58
+ end
@@ -31,12 +31,7 @@ module Wavefront
31
31
  # has changed underneath us.
32
32
  #
33
33
  def initialize(json, status, debug = false)
34
- begin
35
- raw = json.empty? ? {} : JSON.parse(json, symbolize_names: true)
36
- rescue
37
- raw = { message: json, code: status }
38
- end
39
-
34
+ raw = raw_response(json, status)
40
35
  @status = build_status(raw, status)
41
36
  @response = build_response(raw)
42
37
 
@@ -47,6 +42,12 @@ module Wavefront
47
42
  raise Wavefront::Exception::UnparseableResponse
48
43
  end
49
44
 
45
+ def raw_response(json, status)
46
+ json.empty? ? {} : JSON.parse(json, symbolize_names: true)
47
+ rescue
48
+ { message: json, code: status }
49
+ end
50
+
50
51
  def build_status(raw, status)
51
52
  Wavefront::Type::Status.new(raw, status)
52
53
  end
@@ -72,8 +72,11 @@ module Wavefront
72
72
  # active (false) entities
73
73
  #
74
74
  def raw_search(entity = nil, body = nil, deleted = false)
75
- raise ArgumentError unless entity.is_a?(String) || entity.is_a?(Symbol)
76
- raise ArgumentError unless body.is_a?(Hash)
75
+ unless (entity.is_a?(String) || entity.is_a?(Symbol)) &&
76
+ body.is_a?(Hash)
77
+ raise ArgumentError
78
+ end
79
+
77
80
  path = [entity]
78
81
  path.<< 'deleted' if deleted
79
82
  api_post(path, body.to_json, 'application/json')
@@ -95,5 +95,12 @@ module Wavefront
95
95
  code: status },
96
96
  }.to_json
97
97
  end
98
+
99
+ # the user API class does not support pagination. Be up-front
100
+ # about that.
101
+ #
102
+ def everything
103
+ raise NoMethodError
104
+ end
98
105
  end
99
106
  end
@@ -1,3 +1,4 @@
1
+ require_relative './constants'
1
2
  require_relative './exception'
2
3
 
3
4
  module Wavefront
@@ -35,7 +36,8 @@ module Wavefront
35
36
  #
36
37
  def wf_metric_name?(v)
37
38
  if v.is_a?(String) && v.size < 1024 &&
38
- (v.match(/^[\w\-\.]+$/) || v.match(%r{^\"[\w\-\.\/,]+\"$}))
39
+ (v.match(/^#{DELTA}?[\w\-\.]+$/) ||
40
+ v.match(%r{^\"#{DELTA}?[\w\-\.\/,]+\"$}))
39
41
  return true
40
42
  end
41
43
 
@@ -162,13 +164,19 @@ module Wavefront
162
164
  #
163
165
  def wf_point_tags?(tags)
164
166
  raise Wavefront::Exception::InvalidTag unless tags.is_a?(Hash)
167
+ tags.each { |k, v| wf_point_tag?(k, v) }
168
+ end
165
169
 
166
- tags.each do |k, v|
167
- unless k && v && (k.size + v.size < 254) && k.match(/^[\w\-\.:]+$/)
168
- raise Wavefront::Exception::InvalidTag
169
- end
170
- end
171
- true
170
+ # Validate a single point tag, probably on behalf of
171
+ # #wf_point_tags?
172
+ # @param k [String] tag key
173
+ # @param v [String] tag value
174
+ # @raise Wavefront::Exception::InvalidTag if any tag is not valid
175
+ # @return nil
176
+ #
177
+ def wf_point_tag?(k, v)
178
+ return if k && v && (k.size + v.size < 254) && k =~ /^[\w\-\.:]+$/
179
+ raise Wavefront::Exception::InvalidTag
172
180
  end
173
181
 
174
182
  # Ensure the given argument is a valid Wavefront proxy ID
@@ -1 +1 @@
1
- WF_SDK_VERSION = '1.2.1'.freeze
1
+ WF_SDK_VERSION = '1.3.0'.freeze
@@ -1,4 +1,5 @@
1
1
  require 'socket'
2
+ require_relative './constants'
2
3
  require_relative './base'
3
4
 
4
5
  HOSTNAME = Socket.gethostname.freeze
@@ -108,6 +109,28 @@ module Wavefront
108
109
  Wavefront::Response.new(resp, nil)
109
110
  end
110
111
 
112
+ # A wrapper method around #write() which guarantees all points
113
+ # will be sent as deltas. You can still manually prefix any
114
+ # metric with a Δ and use #write(), but depending on your
115
+ # use-case, this method may be safer. It's easy to forget the Δ.
116
+ #
117
+ # @param points [Array[Hash]] see #write()
118
+ # @param openclose [Bool] see #write()
119
+ #
120
+ def write_delta(points, openclose = true)
121
+ write(paths_to_deltas(points), openclose)
122
+ end
123
+
124
+ # Prefix all paths in a points array (as passed to
125
+ # #write_delta() with a delta symbol
126
+ #
127
+ # @param points [Array[Hash]] see #write()
128
+ # @return [Array[Hash]]
129
+ #
130
+ def paths_to_deltas(points)
131
+ [points].flatten.map { |p| p.tap { p[:path] = DELTA + p[:path] } }
132
+ end
133
+
111
134
  def valid_point?(p)
112
135
  return true if opts[:novalidate]
113
136
 
@@ -126,13 +149,14 @@ module Wavefront
126
149
  end
127
150
  end
128
151
 
129
- # Convert a validated point has to a string conforming to
152
+ # Convert a validated point to a string conforming to
130
153
  # https://community.wavefront.com/docs/DOC-1031. No validation
131
154
  # is done here.
132
155
  #
133
156
  # @param p [Hash] a hash describing a point. See #write() for
134
157
  # the format.
135
158
  #
159
+ # rubocop:disable Metrics/CyclomaticComplexity
136
160
  def hash_to_wf(p)
137
161
  unless p.key?(:path) && p.key?(:value)
138
162
  raise Wavefront::Exception::InvalidPoint
@@ -183,10 +207,12 @@ module Wavefront
183
207
  return true
184
208
  end
185
209
 
186
- log("Connecting to #{net[:proxy]}:#{net[:port]}.", :info)
210
+ port = net[:port] || 2878
211
+
212
+ log("Connecting to #{net[:proxy]}:#{port}.", :info)
187
213
 
188
214
  begin
189
- @sock = TCPSocket.new(net[:proxy], net[:port])
215
+ @sock = TCPSocket.new(net[:proxy], port)
190
216
  rescue => e
191
217
  log(e, :error)
192
218
  raise Wavefront::Exception::InvalidEndpoint