wavefront-client 3.2.0 → 3.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.
- checksums.yaml +8 -8
- data/.travis.yml +1 -0
- data/README-cli.md +108 -1
- data/bin/wavefront +49 -11
- data/lib/wavefront/batch_writer.rb +247 -0
- data/lib/wavefront/cli/alerts.rb +50 -27
- data/lib/wavefront/cli/batch_write.rb +227 -0
- data/lib/wavefront/cli/events.rb +1 -1
- data/lib/wavefront/cli/ts.rb +2 -2
- data/lib/wavefront/cli/write.rb +89 -0
- data/lib/wavefront/cli.rb +19 -13
- data/lib/wavefront/client/version.rb +1 -1
- data/lib/wavefront/constants.rb +3 -0
- data/lib/wavefront/events.rb +39 -18
- data/lib/wavefront/exception.rb +8 -1
- data/lib/wavefront/mixins.rb +9 -2
- data/lib/wavefront/writer.rb +7 -2
- data/spec/spec_helper.rb +52 -1
- data/spec/wavefront/batch_writer_spec.rb +523 -0
- data/spec/wavefront/cli/alerts_spec.rb +153 -0
- data/spec/wavefront/cli/batch_write_spec.rb +251 -0
- data/spec/wavefront/cli/events_spec.rb +43 -0
- data/spec/wavefront/cli/resources/alert.human.erb +14 -0
- data/spec/wavefront/cli/resources/alert.human2 +14 -0
- data/spec/wavefront/cli/resources/alert.json +38 -0
- data/spec/wavefront/cli/resources/alert.raw +1 -0
- data/spec/wavefront/cli/resources/alert.ruby +1 -0
- data/spec/wavefront/cli/resources/write.parabola +49 -0
- data/spec/wavefront/cli/write_spec.rb +112 -0
- data/spec/wavefront/cli_spec.rb +68 -0
- data/spec/wavefront/events_spec.rb +111 -0
- data/spec/wavefront/mixins_spec.rb +16 -1
- data/spec/wavefront/writer_spec.rb +0 -7
- data/wavefront-client.gemspec +1 -1
- metadata +35 -6
data/lib/wavefront/cli/alerts.rb
CHANGED
@@ -24,27 +24,14 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
24
24
|
attr_accessor :options, :arguments
|
25
25
|
|
26
26
|
def run
|
27
|
-
|
28
|
-
queries = alerts.public_methods(false).sort
|
29
|
-
queries.delete(:token)
|
30
|
-
|
27
|
+
raise 'Missing token.' if ! @options[:token] || @options[:token].empty?
|
31
28
|
raise 'Missing query.' if arguments.empty?
|
32
29
|
query = arguments[0].to_sym
|
33
30
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
unless Wavefront::Client::ALERT_FORMATS.include?(
|
39
|
-
@options[:format].to_sym)
|
40
|
-
raise 'Output format must be one of: ' +
|
41
|
-
Wavefront::Client::ALERT_FORMATS.join(', ')
|
42
|
-
end
|
43
|
-
|
44
|
-
# This isn't especially nice, but if require to
|
45
|
-
# avoiding breaking the Alerting interface :(
|
46
|
-
options = Hash.new
|
47
|
-
options[:host] = @options[:endpoint]
|
31
|
+
wfa = Wavefront::Alerting.new(@options[:token])
|
32
|
+
valid_state?(wfa, query)
|
33
|
+
valid_format?(@options[:format].to_sym)
|
34
|
+
options = { host: @options[:endpoint] }
|
48
35
|
|
49
36
|
if @options[:shared]
|
50
37
|
options[:shared_tags] = @options[:shared].delete(' ').split(',')
|
@@ -54,9 +41,22 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
54
41
|
options[:private_tags] = @options[:private].delete(' ').split(',')
|
55
42
|
end
|
56
43
|
|
57
|
-
|
44
|
+
begin
|
45
|
+
result = wfa.send(query, options)
|
46
|
+
rescue
|
47
|
+
raise 'Unable to execute query.'
|
48
|
+
end
|
58
49
|
|
59
|
-
|
50
|
+
format_result(result, @options[:format].to_sym)
|
51
|
+
exit
|
52
|
+
end
|
53
|
+
|
54
|
+
def format_result(result, format)
|
55
|
+
#
|
56
|
+
# Call a suitable method to display the output of the API call,
|
57
|
+
# which is JSON.
|
58
|
+
#
|
59
|
+
case format
|
60
60
|
when :ruby
|
61
61
|
pp result
|
62
62
|
when :json
|
@@ -64,11 +64,32 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
64
64
|
when :human
|
65
65
|
puts humanize(JSON.parse(result))
|
66
66
|
else
|
67
|
-
|
68
|
-
|
67
|
+
raise "Invalid output format '#{format}'. See --help for more detail."
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def valid_format?(fmt)
|
72
|
+
fmt = fmt.to_sym if fmt.is_a?(String)
|
73
|
+
|
74
|
+
unless Wavefront::Client::ALERT_FORMATS.include?(fmt)
|
75
|
+
raise 'Output format must be one of: ' +
|
76
|
+
Wavefront::Client::ALERT_FORMATS.join(', ') + '.'
|
69
77
|
end
|
78
|
+
true
|
79
|
+
end
|
70
80
|
|
71
|
-
|
81
|
+
def valid_state?(wfa, state)
|
82
|
+
#
|
83
|
+
# Check the alert type we've been given is valid. There needs to
|
84
|
+
# be a public method in the 'alerting' class for every one.
|
85
|
+
#
|
86
|
+
s = wfa.public_methods(false).sort
|
87
|
+
s.delete(:token)
|
88
|
+
unless s.include?(state)
|
89
|
+
raise 'State must be one of: ' + s.each { |q| q.to_s }.join(', ') +
|
90
|
+
'.'
|
91
|
+
end
|
92
|
+
true
|
72
93
|
end
|
73
94
|
|
74
95
|
def humanize(alerts)
|
@@ -104,7 +125,7 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
104
125
|
end
|
105
126
|
|
106
127
|
def human_line(k, v)
|
107
|
-
'%-22s%s' % [k, v]
|
128
|
+
('%-22s%s' % [k, v]).rstrip
|
108
129
|
end
|
109
130
|
|
110
131
|
def human_line_created(k, v)
|
@@ -121,9 +142,10 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
121
142
|
|
122
143
|
def human_line_hostsUsed(k, v)
|
123
144
|
#
|
124
|
-
# Put each host on its own line, indented.
|
145
|
+
# Put each host on its own line, indented. Does this by
|
146
|
+
# returning an array.
|
125
147
|
#
|
126
|
-
return k unless v
|
148
|
+
return k unless v && v.is_a?(Array) && ! v.empty?
|
127
149
|
v.sort!
|
128
150
|
[human_line(k, v.shift)] + v.map {|el| human_line('', el)}
|
129
151
|
end
|
@@ -144,6 +166,7 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
144
166
|
#
|
145
167
|
# hanging indent long lines to fit in an 80-column terminal
|
146
168
|
#
|
147
|
-
line.gsub(/(.{1,#{cols - offset}})(\s+|\Z)/, "\\1\n#{' ' *
|
169
|
+
line.gsub(/(.{1,#{cols - offset}})(\s+|\Z)/, "\\1\n#{' ' *
|
170
|
+
offset}").rstrip
|
148
171
|
end
|
149
172
|
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'pathname'
|
3
|
+
require 'wavefront/cli'
|
4
|
+
require 'wavefront/batch_writer'
|
5
|
+
#
|
6
|
+
# Push datapoints into Wavefront, via a proxy. Uses the
|
7
|
+
# 'batch_writer' class.
|
8
|
+
#
|
9
|
+
class Wavefront::Cli::BatchWrite < Wavefront::Cli
|
10
|
+
attr_reader :opts, :sock, :fmt, :wf
|
11
|
+
|
12
|
+
include Wavefront::Constants
|
13
|
+
include Wavefront::Mixins
|
14
|
+
|
15
|
+
def run
|
16
|
+
raise 'Invalid format string.' unless valid_format?(options[:format])
|
17
|
+
|
18
|
+
file = options[:'<file>']
|
19
|
+
setup_opts(options)
|
20
|
+
|
21
|
+
if options.key?(:format)
|
22
|
+
setup_fmt(options[:format])
|
23
|
+
else
|
24
|
+
setup_fmt
|
25
|
+
end
|
26
|
+
|
27
|
+
@wf = Wavefront::BatchWriter.new(options)
|
28
|
+
|
29
|
+
begin
|
30
|
+
wf.open_socket
|
31
|
+
rescue
|
32
|
+
raise 'unable to connect to proxy'
|
33
|
+
end
|
34
|
+
|
35
|
+
begin
|
36
|
+
if file == '-'
|
37
|
+
STDIN.each_line { |l| wf.write(process_line(l.strip)) }
|
38
|
+
else
|
39
|
+
process_filedata(load_data(Pathname.new(file)))
|
40
|
+
end
|
41
|
+
ensure
|
42
|
+
wf.close_socket
|
43
|
+
end
|
44
|
+
|
45
|
+
puts "Point summary: " + (%w(sent unsent rejected).map do |p|
|
46
|
+
[wf.summary[p.to_sym], p].join(' ')
|
47
|
+
end.join(', ')) + '.'
|
48
|
+
end
|
49
|
+
|
50
|
+
def setup_fmt(fmt = DEFAULT_INFILE_FORMAT)
|
51
|
+
@fmt = fmt.split('')
|
52
|
+
end
|
53
|
+
|
54
|
+
def setup_opts(options)
|
55
|
+
@opts = {
|
56
|
+
prefix: options[:metric] || '',
|
57
|
+
source: options[:host] || Socket.gethostname,
|
58
|
+
tags: tags_to_hash(options[:tag]),
|
59
|
+
endpoint: options[:proxy],
|
60
|
+
port: options[:port],
|
61
|
+
verbose: options[:verbose],
|
62
|
+
noop: options[:noop],
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def tags_to_hash(tags)
|
67
|
+
#
|
68
|
+
# Turn a docopt array of key=value tags into a hash for the
|
69
|
+
# batch_writer class. If key or value are quoted, we remove the
|
70
|
+
# quotes.
|
71
|
+
#
|
72
|
+
tags = [tags] if tags.is_a?(String)
|
73
|
+
tags = {} unless tags.is_a?(Array)
|
74
|
+
|
75
|
+
tags.each_with_object({}) do |t, m|
|
76
|
+
k, v = t.split('=', 2)
|
77
|
+
m[k.gsub(/^["']|["']$/, '').to_sym] =
|
78
|
+
v.to_s.gsub(/^["']|["']$/, '') if v
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def load_data(file)
|
83
|
+
begin
|
84
|
+
IO.read(file)
|
85
|
+
rescue
|
86
|
+
raise "Cannot open file '#{file}'." unless file.exist?
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def process_filedata(data)
|
91
|
+
#
|
92
|
+
# we know what order the fields are in from the format string,
|
93
|
+
# which contains 't', 'm', and 'v' in some order
|
94
|
+
#
|
95
|
+
data.split("\n").each { |l| wf.write(process_line(l)) }
|
96
|
+
end
|
97
|
+
|
98
|
+
def valid_format?(fmt)
|
99
|
+
# The format string must contain a 'v'. It must not contain
|
100
|
+
# anything other than 'm', 't', 'T' or 'v', and the 'T', if
|
101
|
+
# there, must be at the end. No letter must appear more than
|
102
|
+
# once.
|
103
|
+
#
|
104
|
+
fmt.include?('v') && fmt.match(/^[mtv]+T?$/) && fmt ==
|
105
|
+
fmt.split('').uniq.join
|
106
|
+
end
|
107
|
+
|
108
|
+
def valid_line?(l)
|
109
|
+
#
|
110
|
+
# Make sure we have the right number of columns, according to
|
111
|
+
# the format string. We want to take every precaution we can to
|
112
|
+
# stop users accidentally polluting their metric namespace with
|
113
|
+
# junk.
|
114
|
+
#
|
115
|
+
# If the format string says we are expecting point tags, we may
|
116
|
+
# have more columns than the length of the format string.
|
117
|
+
#
|
118
|
+
ncols = l.split.length
|
119
|
+
|
120
|
+
if fmt.include?('T')
|
121
|
+
return false unless ncols >= fmt.length
|
122
|
+
else
|
123
|
+
return false unless ncols == fmt.length
|
124
|
+
end
|
125
|
+
|
126
|
+
true
|
127
|
+
end
|
128
|
+
|
129
|
+
def valid_timestamp?(ts)
|
130
|
+
#
|
131
|
+
# Another attempt to stop the user accidentally sending nonsense
|
132
|
+
# data. See if the time looks valid. We'll assume anything before
|
133
|
+
# 2000/01/01 or after a year from now is wrong. Arbitrary, but
|
134
|
+
# there has to be a cut-off somewhere.
|
135
|
+
#
|
136
|
+
(ts.is_a?(Integer) || ts.match(/^\d+$/)) &&
|
137
|
+
ts.to_i > 946684800 && ts.to_i < (Time.now.to_i + 31557600)
|
138
|
+
end
|
139
|
+
|
140
|
+
def valid_value?(val)
|
141
|
+
val.is_a?(Numeric) || (val.match(/^-?[\d\.e]+$/) && val.count('.') < 2)
|
142
|
+
end
|
143
|
+
|
144
|
+
def process_line(l)
|
145
|
+
#
|
146
|
+
# Process a line of input, as described by the format string
|
147
|
+
# held in opts[:fmt]. Produces a hash suitable for batch_writer
|
148
|
+
# to send on.
|
149
|
+
#
|
150
|
+
# We let the user define most of the fields, but anything beyond
|
151
|
+
# what they define is always assumed to be point tags. This is
|
152
|
+
# because you can have arbitrarily many of those for each point.
|
153
|
+
#
|
154
|
+
return true if l.empty?
|
155
|
+
m_prefix = opts[:prefix]
|
156
|
+
chunks = l.split(/\s+/, fmt.length)
|
157
|
+
|
158
|
+
begin
|
159
|
+
raise 'wrong number of fields' unless valid_line?(l)
|
160
|
+
|
161
|
+
begin
|
162
|
+
v = chunks[fmt.index('v')]
|
163
|
+
|
164
|
+
if valid_value?(v)
|
165
|
+
point = { value: v.to_f }
|
166
|
+
else
|
167
|
+
raise "invalid value '#{v}'"
|
168
|
+
end
|
169
|
+
|
170
|
+
rescue TypeError
|
171
|
+
raise "no value in '#{l}'"
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
# The user can supply a time. If they have told us they won't
|
176
|
+
# be, we'll use the current time.
|
177
|
+
#
|
178
|
+
|
179
|
+
point[:ts] = begin
|
180
|
+
ts = chunks[fmt.index('t')]
|
181
|
+
|
182
|
+
if valid_timestamp?(ts)
|
183
|
+
Time.at(parse_time(ts))
|
184
|
+
else
|
185
|
+
raise "invalid timestamp '#{ts}'"
|
186
|
+
end
|
187
|
+
|
188
|
+
rescue TypeError
|
189
|
+
Time.now.utc.to_i
|
190
|
+
end
|
191
|
+
|
192
|
+
# The source is normally the local hostname, but the user can
|
193
|
+
# override that.
|
194
|
+
|
195
|
+
point[:source] = begin
|
196
|
+
chunks[fmt.index('s')]
|
197
|
+
rescue TypeError
|
198
|
+
opts[:source]
|
199
|
+
end
|
200
|
+
|
201
|
+
# The metric path can be in the data, or passed as an option, or
|
202
|
+
# both. If the latter, then we assume the option is a prefix,
|
203
|
+
# and concatenate the value in the data.
|
204
|
+
#
|
205
|
+
begin
|
206
|
+
m = chunks[fmt.index('m')]
|
207
|
+
point[:path] = m_prefix.empty? ? m : [m_prefix, m].join('.')
|
208
|
+
rescue TypeError
|
209
|
+
if m_prefix
|
210
|
+
point[:path] = m_prefix
|
211
|
+
else
|
212
|
+
raise "metric path in '#{l}'"
|
213
|
+
end
|
214
|
+
end
|
215
|
+
rescue => e
|
216
|
+
puts "WARNING: #{e}. Skipping."
|
217
|
+
return false
|
218
|
+
end
|
219
|
+
|
220
|
+
if fmt.last == 'T'
|
221
|
+
point[:tags] =
|
222
|
+
tags_to_hash(chunks.last.split(/\s(?=(?:[^"]|"[^"]*")*$)/))
|
223
|
+
end
|
224
|
+
|
225
|
+
point
|
226
|
+
end
|
227
|
+
end
|
data/lib/wavefront/cli/events.rb
CHANGED
@@ -60,7 +60,7 @@ class Wavefront::Cli::Events < Wavefront::Cli
|
|
60
60
|
options[t] ? time_to_ms(parse_time(options[t])) : false
|
61
61
|
end
|
62
62
|
|
63
|
-
def prep_hosts(hosts)
|
63
|
+
def prep_hosts(hosts = false)
|
64
64
|
#
|
65
65
|
# We allow the user to associate an event with multiple hosts,
|
66
66
|
# or to pass in some identifer other than the hostname. If they
|
data/lib/wavefront/cli/ts.rb
CHANGED
@@ -48,11 +48,11 @@ class Wavefront::Cli::Ts < Wavefront::Cli
|
|
48
48
|
options[:prefix_length] = @options[:prefixlength].to_i
|
49
49
|
|
50
50
|
if @options[:start]
|
51
|
-
options[:start_time] = parse_time(@options[:start])
|
51
|
+
options[:start_time] = Time.at(parse_time(@options[:start]))
|
52
52
|
end
|
53
53
|
|
54
54
|
if @options[:end]
|
55
|
-
options[:end_time] = parse_time(@options[:end])
|
55
|
+
options[:end_time] = Time.at(parse_time(@options[:end]))
|
56
56
|
end
|
57
57
|
|
58
58
|
wave = Wavefront::Client.new(@options[:token], @options[:endpoint], @options[:debug])
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'wavefront/writer'
|
2
|
+
require 'wavefront/cli'
|
3
|
+
require 'socket'
|
4
|
+
#
|
5
|
+
# Push datapoints into Wavefront, via a proxy. This class deals in
|
6
|
+
# single points. It cannot batch, or deal with files or streams of
|
7
|
+
# data. This is because it depends on the very simple 'writer'
|
8
|
+
# class, which cannot be significantly changed, so as to maintain
|
9
|
+
# backward compatibility.
|
10
|
+
#
|
11
|
+
class Wavefront::Cli::Write < Wavefront::Cli
|
12
|
+
|
13
|
+
include Wavefront::Constants
|
14
|
+
include Wavefront::Mixins
|
15
|
+
|
16
|
+
def run
|
17
|
+
valid_value?(options[:'<value>'])
|
18
|
+
valid_metric?(options[:'<metric>'])
|
19
|
+
ts = options[:time] ? parse_time(options[:time]) : false
|
20
|
+
|
21
|
+
[:proxy, :host].each do |h|
|
22
|
+
raise Wavefront::Exception::InvalidHostname unless valid_host?(h)
|
23
|
+
end
|
24
|
+
|
25
|
+
write_opts = {
|
26
|
+
agent_host: options[:proxy],
|
27
|
+
host_name: options[:host],
|
28
|
+
metric_name: options[:'<metric>'],
|
29
|
+
point_tags: prep_tags(options[:tag]),
|
30
|
+
timestamp: ts,
|
31
|
+
noop: options[:noop],
|
32
|
+
}
|
33
|
+
|
34
|
+
write_metric(options[:'<value>'].to_i, options[:'<metric>'], write_opts)
|
35
|
+
end
|
36
|
+
|
37
|
+
def write_metric(value, name, opts)
|
38
|
+
wf = Wavefront::Writer.new(opts)
|
39
|
+
wf.write(value, name, opts)
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid_host?(hostname)
|
43
|
+
#
|
44
|
+
# quickly make sure a hostname looks vaguely sensible
|
45
|
+
#
|
46
|
+
hostname.match(/^[\w\.\-]+$/) && hostname.length < 1024
|
47
|
+
end
|
48
|
+
|
49
|
+
def valid_value?(value)
|
50
|
+
#
|
51
|
+
# Values, it seems, will always come in as strings. We need to
|
52
|
+
# cast them to numbers. I don't think there's any reasonable way
|
53
|
+
# to allow exponential notation.
|
54
|
+
#
|
55
|
+
unless value.is_a?(Numeric) || value.match(/^-?\d*\.?\d*$/) ||
|
56
|
+
value.match(/^-?\d*\.?\d*e\d+$/)
|
57
|
+
raise Wavefront::Exception::InvalidMetricValue
|
58
|
+
end
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
def valid_metric?(metric)
|
63
|
+
#
|
64
|
+
# Apply some common-sense rules to metric paths. Check it's a
|
65
|
+
# string, and that it has at least one dot in it. Don't allow
|
66
|
+
# through odd characters or whitespace.
|
67
|
+
#
|
68
|
+
begin
|
69
|
+
raise unless metric.is_a?(String) &&
|
70
|
+
metric.split('.').length > 1 &&
|
71
|
+
metric.match(/^[\w\-\._]+$/) &&
|
72
|
+
metric.length < 1024
|
73
|
+
rescue
|
74
|
+
fail Wavefront::Exception::InvalidMetricName
|
75
|
+
end
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
def prep_tags(tags)
|
80
|
+
#
|
81
|
+
# Takes an array of key=value tags (as produced by docopt) and
|
82
|
+
# turns it into an array of [key, value] arrays (as required
|
83
|
+
# by various of our own methods). Anything not of the form
|
84
|
+
# key=val is dropped.
|
85
|
+
#
|
86
|
+
return [] unless tags.is_a?(Array)
|
87
|
+
tags.map { |t| t.split('=') }.select { |e| e.length == 2 }
|
88
|
+
end
|
89
|
+
end
|