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