wavefront-sdk 1.2.1 → 1.3.0

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 +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