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.
- checksums.yaml +5 -5
- data/.gitignore +2 -0
- data/.rubocop.yml +62 -72
- data/.travis.yml +5 -4
- data/README.md +10 -0
- data/lib/wavefront-sdk/base.rb +28 -8
- data/lib/wavefront-sdk/constants.rb +3 -0
- data/lib/wavefront-sdk/credentials.rb +36 -17
- data/lib/wavefront-sdk/mixins.rb +3 -22
- data/lib/wavefront-sdk/notificant.rb +1 -1
- data/lib/wavefront-sdk/parse_time.rb +58 -0
- data/lib/wavefront-sdk/response.rb +7 -6
- data/lib/wavefront-sdk/search.rb +5 -2
- data/lib/wavefront-sdk/user.rb +7 -0
- data/lib/wavefront-sdk/validators.rb +15 -7
- data/lib/wavefront-sdk/version.rb +1 -1
- data/lib/wavefront-sdk/write.rb +29 -3
- data/spec/.rubocop.yml +3 -0
- data/spec/spec_helper.rb +24 -18
- data/spec/wavefront-sdk/alert_spec.rb +5 -0
- data/spec/wavefront-sdk/base_spec.rb +9 -9
- data/spec/wavefront-sdk/credentials_spec.rb +123 -4
- data/spec/wavefront-sdk/event_spec.rb +4 -5
- data/spec/wavefront-sdk/externallink_spec.rb +1 -1
- data/spec/wavefront-sdk/maintenancewindow_spec.rb +2 -2
- data/spec/wavefront-sdk/metric_spec.rb +2 -2
- data/spec/wavefront-sdk/mixins_spec.rb +1 -1
- data/spec/wavefront-sdk/parse_time_spec.rb +65 -0
- data/spec/wavefront-sdk/resources/test2.conf +4 -0
- data/spec/wavefront-sdk/response_spec.rb +3 -5
- data/spec/wavefront-sdk/savedsearch_spec.rb +1 -1
- data/spec/wavefront-sdk/source_spec.rb +1 -1
- data/spec/wavefront-sdk/user_spec.rb +5 -3
- data/spec/wavefront-sdk/validators_spec.rb +65 -62
- data/spec/wavefront-sdk/webhook_spec.rb +1 -1
- data/spec/wavefront-sdk/write_spec.rb +19 -1
- data/wavefront-sdk.gemspec +4 -3
- metadata +29 -15
data/.travis.yml
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
language: ruby
|
2
2
|
cache: bundler
|
3
3
|
rvm:
|
4
|
-
- 2.2.
|
5
|
-
- 2.3.
|
6
|
-
- 2.4.
|
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.
|
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.
|
data/lib/wavefront-sdk/base.rb
CHANGED
@@ -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, :
|
25
|
-
:
|
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
|
-
|
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
|
@@ -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
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
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
|
75
|
-
#
|
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
|
data/lib/wavefront-sdk/mixins.rb
CHANGED
@@ -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
|
-
|
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,
|
@@ -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
|
-
|
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
|
data/lib/wavefront-sdk/search.rb
CHANGED
@@ -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
|
-
|
76
|
-
|
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')
|
data/lib/wavefront-sdk/user.rb
CHANGED
@@ -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(
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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.
|
1
|
+
WF_SDK_VERSION = '1.3.0'.freeze
|
data/lib/wavefront-sdk/write.rb
CHANGED
@@ -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
|
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
|
-
|
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],
|
215
|
+
@sock = TCPSocket.new(net[:proxy], port)
|
190
216
|
rescue => e
|
191
217
|
log(e, :error)
|
192
218
|
raise Wavefront::Exception::InvalidEndpoint
|