wavefront-cli 2.3.1 → 2.4.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 +4 -4
- data/.travis.yml +4 -0
- data/lib/wavefront-cli/base.rb +26 -10
- data/lib/wavefront-cli/base_write.rb +231 -0
- data/lib/wavefront-cli/commands/report.rb +35 -0
- data/lib/wavefront-cli/commands/write.rb +5 -2
- data/lib/wavefront-cli/controller.rb +8 -4
- data/lib/wavefront-cli/display/report.rb +17 -0
- data/lib/wavefront-cli/display/write.rb +7 -4
- data/lib/wavefront-cli/output/base.rb +10 -0
- data/lib/wavefront-cli/output/hcl.rb +20 -0
- data/lib/wavefront-cli/output/hcl/alert.rb +14 -0
- data/lib/wavefront-cli/output/hcl/base.rb +92 -0
- data/lib/wavefront-cli/output/hcl/dashboard.rb +114 -0
- data/lib/wavefront-cli/output/hcl/notificant.rb +30 -0
- data/lib/wavefront-cli/output/json.rb +12 -0
- data/lib/wavefront-cli/output/ruby.rb +12 -0
- data/lib/wavefront-cli/output/wavefront.tf +257 -0
- data/lib/wavefront-cli/output/yaml.rb +15 -0
- data/lib/wavefront-cli/report.rb +15 -0
- data/lib/wavefront-cli/string.rb +12 -1
- data/lib/wavefront-cli/version.rb +1 -1
- data/lib/wavefront-cli/write.rb +9 -221
- data/spec/wavefront-cli/string_spec.rb +24 -0
- data/wavefront-cli.gemspec +1 -1
- metadata +20 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 903c8e5083d5ea318618590cf9e366f451e6cf044037c9771791dd647703ec68
|
4
|
+
data.tar.gz: 7e4fe291218861044b5d117806eaecc17457da88079704fc3394eaebfcfabeca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eb2afd54929d82f8f97dfee37de753f8c25d4e9f4f31c8413e174446f0d90264122b1002162b3beab2ce0f0caaddf52d029a71ca61a3178b4a99c4e698626761
|
7
|
+
data.tar.gz: 234cb0155c0abaffac24583945bdd4311d049dfec3788f5961fdc579de9339fc6ef4033c83cbdac92e7e7e7301ab7a453178e5ae94137f9ba0d1d212e3d02a6f
|
data/.travis.yml
CHANGED
@@ -15,3 +15,7 @@ deploy:
|
|
15
15
|
tags: true
|
16
16
|
repo: snltd/wavefront-cli
|
17
17
|
ruby: 2.3.6
|
18
|
+
notifications:
|
19
|
+
email: false
|
20
|
+
slack:
|
21
|
+
secure: YrwfBiBscjUCHZIyPHH/FEm5VbHZN3AczHnlOJfETsAdsVpM+JOvHnoCaY0AGjvCvmFMPE9yg11yhwMfXZJVwjAC6b75VrXoCFIvC1tjLqFizuI4VBZXUZk3CQZK0pIh1ZRYVINa0LuYLDyxF0EG2N9KTYqQcMEsBwsVohsca+zjbjyIW5H0FeVWJC4QlFqVHBwFHvylfHnPjh0pQAn3sE9j7Of3W2HQVM753/lsOkMf3sHYOv8AOrzjTNqyrageTxUGnO91S41DirNdesrjF8Qg+/s1RSiNeYSZLkqI2pk+3sdkMkpA+2z2zQ/ZbgudS/38AVlh2Wb2KkmFw0+XhdpUGqQZgLlgWMDKoiS7j2dNQ3zA4guCZIQSW6gqR76wTUqeCZQ3UNalChAhACFnG0FmZj+AIE72r28dOH747zKEmTaoJt5FR7GlSoG6cH1EV9qTeIjZ/33ehL703E6qrWITjQ6VrNrPsTCt8rvoW2MV8TP9qb32JVJqWxabqUMBicIWLEeDjPyAmOZs32cWwfk9zcJ6hIJcFffkxgVJN0vU6Qi4tGWUmYK12EIclVgrKgvN1yHUrUN/r7+sUPX9WRj82RNFU6RSPircekV9oWj9Hr8A2imnFqiMTnPpSb56y02cG9FbwFPqxLSaNNV4lALzoBmluKv0RSeEWhRvrGI=
|
data/lib/wavefront-cli/base.rb
CHANGED
@@ -193,12 +193,20 @@ module WavefrontCli
|
|
193
193
|
|
194
194
|
unless check_status(data.status)
|
195
195
|
handle_error(method, data.status.code) if format_var == :human
|
196
|
-
|
196
|
+
display_api_error(data.status)
|
197
197
|
end
|
198
198
|
|
199
199
|
handle_response(data.response, format_var, method)
|
200
200
|
end
|
201
201
|
|
202
|
+
# @param status [Map] status object from SDK response
|
203
|
+
# @return System exit
|
204
|
+
#
|
205
|
+
def display_api_error(status)
|
206
|
+
msg = status.message || 'No further information'
|
207
|
+
abort format('ERROR: API code %s: %s.', status.code, msg)
|
208
|
+
end
|
209
|
+
|
202
210
|
def display_no_api_response(data, method)
|
203
211
|
handle_response(data, format_var, method)
|
204
212
|
end
|
@@ -216,21 +224,29 @@ module WavefrontCli
|
|
216
224
|
end
|
217
225
|
|
218
226
|
def handle_response(resp, format, method)
|
219
|
-
|
220
|
-
when :json
|
221
|
-
puts resp.to_json
|
222
|
-
when :yaml # We don't want the YAML keys to be symbols.
|
223
|
-
puts JSON.parse(resp.to_json).to_yaml
|
224
|
-
when :ruby
|
225
|
-
p resp
|
226
|
-
when :human
|
227
|
+
if format == :human
|
227
228
|
k = load_display_class
|
228
229
|
k.new(resp, options).run(method)
|
229
230
|
else
|
230
|
-
|
231
|
+
parseable_output(format, resp)
|
231
232
|
end
|
232
233
|
end
|
233
234
|
|
235
|
+
def parseable_output(format, resp)
|
236
|
+
options[:class] = klass_word
|
237
|
+
options[:hcl_fields] = hcl_fields
|
238
|
+
require_relative File.join('output', format.to_s)
|
239
|
+
oclass = Object.const_get(format('WavefrontOutput::%s',
|
240
|
+
format.to_s.capitalize))
|
241
|
+
oclass.new(resp, options).run
|
242
|
+
rescue LoadError
|
243
|
+
raise "Unsupported output format '#{format}'."
|
244
|
+
end
|
245
|
+
|
246
|
+
def hcl_fields
|
247
|
+
[]
|
248
|
+
end
|
249
|
+
|
234
250
|
def load_display_class
|
235
251
|
require_relative File.join('display', klass_word)
|
236
252
|
Object.const_get(klass.name.sub('Wavefront', 'WavefrontDisplay'))
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'wavefront-sdk/mixins'
|
2
|
+
require_relative './base'
|
3
|
+
|
4
|
+
module WavefrontCli
|
5
|
+
#
|
6
|
+
# Send points to a proxy.
|
7
|
+
#
|
8
|
+
class BaseWrite < Base
|
9
|
+
attr_reader :fmt
|
10
|
+
include Wavefront::Mixins
|
11
|
+
|
12
|
+
def do_point
|
13
|
+
p = { path: options[:'<metric>'],
|
14
|
+
value: options[:'<value>'].delete('\\').to_f,
|
15
|
+
tags: tags_to_hash(options[:tag]) }
|
16
|
+
|
17
|
+
p[:source] = options[:host] if options[:host]
|
18
|
+
p[:ts] = parse_time(options[:time]) if options[:time]
|
19
|
+
send_point(p)
|
20
|
+
end
|
21
|
+
|
22
|
+
def send_point(p)
|
23
|
+
wf.write(p)
|
24
|
+
rescue Wavefront::Exception::InvalidEndpoint
|
25
|
+
abort "could not speak to proxy #{options[:proxy]}:#{options[:port]}."
|
26
|
+
end
|
27
|
+
|
28
|
+
def do_file
|
29
|
+
valid_format?(options[:infileformat])
|
30
|
+
setup_fmt(options[:infileformat] || 'tmv')
|
31
|
+
process_input(options[:'<file>'])
|
32
|
+
end
|
33
|
+
|
34
|
+
# Read the input, from a file or from STDIN, and turn each line
|
35
|
+
# into Wavefront points.
|
36
|
+
#
|
37
|
+
def process_input(file)
|
38
|
+
if file == '-'
|
39
|
+
read_stdin
|
40
|
+
else
|
41
|
+
data = load_data(Pathname.new(file)).split("\n").map do |l|
|
42
|
+
process_line(l)
|
43
|
+
end
|
44
|
+
|
45
|
+
wf.write(data)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Read from standard in and stream points through an open
|
50
|
+
# socket. If the user hits ctrl-c, close the socket and exit
|
51
|
+
# politely.
|
52
|
+
#
|
53
|
+
def read_stdin
|
54
|
+
open_connection
|
55
|
+
STDIN.each_line { |l| wf.write(process_line(l.strip), false) }
|
56
|
+
close_connection
|
57
|
+
rescue SystemExit, Interrupt
|
58
|
+
puts 'ctrl-c. Exiting.'
|
59
|
+
wf.close
|
60
|
+
exit 0
|
61
|
+
end
|
62
|
+
|
63
|
+
# Find and return the value in a chunked line of input
|
64
|
+
#
|
65
|
+
# param chunks [Array] a chunked line of input from #process_line
|
66
|
+
# return [Float] the value
|
67
|
+
# raise TypeError if field does not exist
|
68
|
+
# raise Wavefront::Exception::InvalidValue if it's not a value
|
69
|
+
#
|
70
|
+
def extract_value(chunks)
|
71
|
+
v = chunks[fmt.index('v')]
|
72
|
+
v.to_f
|
73
|
+
end
|
74
|
+
|
75
|
+
# Find and return the source in a chunked line of input.
|
76
|
+
#
|
77
|
+
# param chunks [Array] a chunked line of input from #process_line
|
78
|
+
# return [Float] the timestamp, if it is there, or the current
|
79
|
+
# UTC time if it is not.
|
80
|
+
# raise TypeError if field does not exist
|
81
|
+
#
|
82
|
+
def extract_ts(chunks)
|
83
|
+
ts = chunks[fmt.index('t')]
|
84
|
+
return parse_time(ts) if valid_timestamp?(ts)
|
85
|
+
rescue TypeError
|
86
|
+
Time.now.utc.to_i
|
87
|
+
end
|
88
|
+
|
89
|
+
def extract_tags(chunks)
|
90
|
+
tags_to_hash(chunks.last.split(/\s(?=(?:[^"]|"[^"]*")*$)/))
|
91
|
+
end
|
92
|
+
|
93
|
+
# Find and return the metric path in a chunked line of input.
|
94
|
+
# The path can be in the data, or passed as an option, or both.
|
95
|
+
# If the latter, then we assume the option is a prefix, and
|
96
|
+
# concatenate the value in the data.
|
97
|
+
#
|
98
|
+
# param chunks [Array] a chunked line of input from #process_line
|
99
|
+
# return [String] the metric path
|
100
|
+
# raise TypeError if field does not exist
|
101
|
+
#
|
102
|
+
def extract_path(chunks)
|
103
|
+
m = chunks[fmt.index('m')]
|
104
|
+
return options[:metric] ? [options[:metric], m].join('.') : m
|
105
|
+
rescue TypeError
|
106
|
+
return options[:metric] if options[:metric]
|
107
|
+
raise
|
108
|
+
end
|
109
|
+
|
110
|
+
# Find and return the source in a chunked line of input.
|
111
|
+
#
|
112
|
+
# param chunks [Array] a chunked line of input from #process_line
|
113
|
+
# return [String] the source, if it is there, or if not, the
|
114
|
+
# value passed through by -H, or the local hostname.
|
115
|
+
#
|
116
|
+
def extract_source(chunks)
|
117
|
+
return chunks[fmt.index('s')]
|
118
|
+
rescue TypeError
|
119
|
+
options[:source] || Socket.gethostname
|
120
|
+
end
|
121
|
+
|
122
|
+
# Process a line of input, as described by the format string
|
123
|
+
# held in @fmt. Produces a hash suitable for the SDK to send on.
|
124
|
+
#
|
125
|
+
# We let the user define most of the fields, but anything beyond
|
126
|
+
# what they define is always assumed to be point tags. This is
|
127
|
+
# because you can have arbitrarily many of those for each point.
|
128
|
+
#
|
129
|
+
def process_line(l)
|
130
|
+
return true if l.empty?
|
131
|
+
chunks = l.split(/\s+/, fmt.length)
|
132
|
+
raise 'wrong number of fields' unless enough_fields?(l)
|
133
|
+
|
134
|
+
begin
|
135
|
+
point = { path: extract_path(chunks),
|
136
|
+
value: extract_value(chunks) }
|
137
|
+
point[:ts] = extract_ts(chunks) if fmt.include?('t')
|
138
|
+
point[:source] = extract_source(chunks) if fmt.include?('s')
|
139
|
+
point[:tags] = line_tags(chunks)
|
140
|
+
rescue TypeError
|
141
|
+
raise "could not process #{l}"
|
142
|
+
end
|
143
|
+
|
144
|
+
point
|
145
|
+
end
|
146
|
+
|
147
|
+
# We can get tags from the file, from the -T option, or both.
|
148
|
+
# Merge them, making the -T win if there is a collision.
|
149
|
+
#
|
150
|
+
def line_tags(chunks)
|
151
|
+
file_tags = fmt.last == 'T' ? extract_tags(chunks) : {}
|
152
|
+
opt_tags = tags_to_hash(options[:tag])
|
153
|
+
file_tags.merge(opt_tags)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Takes an array of key=value tags (as produced by docopt) and
|
157
|
+
# turns it into a hash of key: value tags. Anything not of the
|
158
|
+
# form key=val is dropped. If key or value are quoted, we
|
159
|
+
# remove the quotes.
|
160
|
+
#
|
161
|
+
# @param tags [Array]
|
162
|
+
# return Hash
|
163
|
+
#
|
164
|
+
def tags_to_hash(tags)
|
165
|
+
return nil unless tags
|
166
|
+
|
167
|
+
[tags].flatten.each_with_object({}) do |t, ret|
|
168
|
+
k, v = t.split('=', 2)
|
169
|
+
k.gsub!(/^["']|["']$/, '')
|
170
|
+
ret[k] = v.to_s.gsub(/^["']|["']$/, '') if v
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# The format string must contain a 'v'. It must not contain
|
175
|
+
# anything other than 'm', 't', 'T', 's', or 'v', and the 'T',
|
176
|
+
# if there, must be at the end. No letter must appear more than
|
177
|
+
# once.
|
178
|
+
#
|
179
|
+
# @param fmt [String] format of input file
|
180
|
+
#
|
181
|
+
def valid_format?(fmt)
|
182
|
+
if fmt.include?('v') && fmt.match(/^[mstv]+T?$/) &&
|
183
|
+
fmt == fmt.split('').uniq.join
|
184
|
+
return true
|
185
|
+
end
|
186
|
+
|
187
|
+
raise 'Invalid format string.'
|
188
|
+
end
|
189
|
+
|
190
|
+
# Make sure we have the right number of columns, according to
|
191
|
+
# the format string. We want to take every precaution we can to
|
192
|
+
# stop users accidentally polluting their metric namespace with
|
193
|
+
# junk.
|
194
|
+
#
|
195
|
+
# If the format string says we are expecting point tags, we
|
196
|
+
# may have more columns than the length of the format string.
|
197
|
+
#
|
198
|
+
def enough_fields?(l)
|
199
|
+
ncols = l.split.length
|
200
|
+
|
201
|
+
if fmt.include?('T')
|
202
|
+
return false unless ncols >= fmt.length
|
203
|
+
else
|
204
|
+
return false unless ncols == fmt.length
|
205
|
+
end
|
206
|
+
|
207
|
+
true
|
208
|
+
end
|
209
|
+
|
210
|
+
# Although the SDK does value checking, we'll add another layer
|
211
|
+
# of input checing here. See if the time looks valid. We'll
|
212
|
+
# assume anything before 2000/01/01 or after a year from now is
|
213
|
+
# wrong. Arbitrary, but there has to be a cut-off somewhere.
|
214
|
+
#
|
215
|
+
def valid_timestamp?(ts)
|
216
|
+
(ts.is_a?(Integer) || ts.match(/^\d+$/)) &&
|
217
|
+
ts.to_i > 946_684_800 && ts.to_i < (Time.now.to_i + 31_557_600)
|
218
|
+
end
|
219
|
+
|
220
|
+
private
|
221
|
+
|
222
|
+
def setup_fmt(fmt)
|
223
|
+
@fmt = fmt.split('')
|
224
|
+
end
|
225
|
+
|
226
|
+
def load_data(file)
|
227
|
+
raise "Cannot open file '#{file}'." unless file.exist?
|
228
|
+
IO.read(file)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative './base'
|
2
|
+
|
3
|
+
# Define the report command.
|
4
|
+
#
|
5
|
+
class WavefrontCommandReport < WavefrontCommandBase
|
6
|
+
def description
|
7
|
+
'send data directly to Wavefront'
|
8
|
+
end
|
9
|
+
|
10
|
+
def _commands
|
11
|
+
["point #{CMN} [-s time] [-H host] [-T tag...] <metric> <value>",
|
12
|
+
"file #{CMN} [-H host] [-F format] [-m metric] [-T tag...] <file>"]
|
13
|
+
end
|
14
|
+
|
15
|
+
def _options
|
16
|
+
[common_options,
|
17
|
+
'-s, --time=TIME time of data point (omit to use ' \
|
18
|
+
'current time)',
|
19
|
+
'-H, --host=STRING source host', \
|
20
|
+
'-T, --tag=TAG point tag in key=value form',
|
21
|
+
'-F, --infileformat=STRING format of input file or stdin',
|
22
|
+
'-m, --metric=STRING the metric path to which contents of ' \
|
23
|
+
'a file will be assigned. If the file contains a metric name, ' \
|
24
|
+
'the two will be dot-concatenated, with this value first',
|
25
|
+
"-q, --quiet don't report the points sent summary " \
|
26
|
+
'(unless there were errors)']
|
27
|
+
end
|
28
|
+
|
29
|
+
def postscript
|
30
|
+
'Files are whitespace separated, and fields can be defined ' \
|
31
|
+
"with the '-F' option. Use 't' for timestamp; 'm' for metric " \
|
32
|
+
"name; 'v' for value, 's' for source, and 'T' for tags. Put 'T' " \
|
33
|
+
'last.'.cmd_fold(TW, 0)
|
34
|
+
end
|
35
|
+
end
|
@@ -8,9 +8,9 @@ class WavefrontCommandWrite < WavefrontCommandBase
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def _commands
|
11
|
-
['point [-
|
11
|
+
['point [-DnViq] [-c file] [-P profile] [-E proxy] [-t time] ' \
|
12
12
|
'[-p port] [-H host] [-T tag...] <metric> <value>',
|
13
|
-
'file [-
|
13
|
+
'file [-DnViq] [-c file] [-P profile] [-E proxy] [-H host] ' \
|
14
14
|
'[-p port] [-F format] [-m metric] [-T tag...] ' \
|
15
15
|
'[-r rate] <file>']
|
16
16
|
end
|
@@ -25,6 +25,9 @@ class WavefrontCommandWrite < WavefrontCommandBase
|
|
25
25
|
'-m, --metric=STRING the metric path to which contents of ' \
|
26
26
|
'a file will be assigned. If the file contains a metric name, ' \
|
27
27
|
'the two will be dot-concatenated, with this value first',
|
28
|
+
'-i, --delta increment metric by given value',
|
29
|
+
"-q, --quiet don't report the points sent summary " \
|
30
|
+
'(unless there were errors)',
|
28
31
|
'-r, --rate=INTEGER throttle point sending to this many ' \
|
29
32
|
'points per second']
|
30
33
|
end
|
@@ -1,9 +1,13 @@
|
|
1
1
|
# For development against a local checkout of the SDK, uncomment
|
2
|
-
# this
|
2
|
+
# this definition
|
3
3
|
#
|
4
|
-
#
|
5
|
-
|
6
|
-
|
4
|
+
# DEVELOPMENT = true
|
5
|
+
|
6
|
+
if defined?(DEVELOPMENT)
|
7
|
+
dir = Pathname.new(__FILE__).dirname.realpath.parent.parent.parent
|
8
|
+
$LOAD_PATH.<< dir + 'lib'
|
9
|
+
$LOAD_PATH.<< dir + 'wavefront-sdk' + 'lib'
|
10
|
+
end
|
7
11
|
|
8
12
|
require 'pathname'
|
9
13
|
require 'pp'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative './base'
|
2
|
+
|
3
|
+
module WavefrontDisplay
|
4
|
+
#
|
5
|
+
# Format human-readable output when writing points directly to
|
6
|
+
# Wavefront.
|
7
|
+
#
|
8
|
+
class Report < Base
|
9
|
+
def do_point
|
10
|
+
puts 'Point received.' unless options[:quiet]
|
11
|
+
end
|
12
|
+
|
13
|
+
def do_file
|
14
|
+
do_point
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -5,15 +5,18 @@ module WavefrontDisplay
|
|
5
5
|
#
|
6
6
|
class Write < Base
|
7
7
|
def do_point
|
8
|
-
|
9
|
-
puts format(' %12s %d', k.to_s, data[k])
|
10
|
-
end
|
11
|
-
|
8
|
+
report unless options[:quiet] || (data[:unsent] + data[:rejected] > 0)
|
12
9
|
exit(data.rejected.zero? && data.unsent.zero? ? 0 : 1)
|
13
10
|
end
|
14
11
|
|
15
12
|
def do_file
|
16
13
|
do_point
|
17
14
|
end
|
15
|
+
|
16
|
+
def report
|
17
|
+
%i[sent rejected unsent].each do |k|
|
18
|
+
puts format(' %12s %d', k.to_s, data[k])
|
19
|
+
end
|
20
|
+
end
|
18
21
|
end
|
19
22
|
end
|