wavefront-cli 2.3.1 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0712d7ce74bb2e1b5d61a0bbbe33f90fdd36c9d79d41d481b392f903f46228b3
4
- data.tar.gz: 6fd070960f3e2184d31f3ce6b3c25c2ebbdbb512934e7236b4468d3ee26fa094
3
+ metadata.gz: 903c8e5083d5ea318618590cf9e366f451e6cf044037c9771791dd647703ec68
4
+ data.tar.gz: 7e4fe291218861044b5d117806eaecc17457da88079704fc3394eaebfcfabeca
5
5
  SHA512:
6
- metadata.gz: 84a8143458a13242879ffb590abcee1d98b505e85ce59889423bc83d76d11394193baf17c66a112af20c9e8b16b140907ee0e4d674c8a7f422f850c30edf0fa5
7
- data.tar.gz: 99c7f6877e10db8f529b3bda1dbdfc8ff3e15db2aa595e69d35ac3cf09f514342b33ce91397fcf7cda100c246e1a279801bf76adff3c0ffff3f682eda268f902
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=
@@ -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
- abort "API #{data.status.code}: #{data.status.message}."
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
- case format
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
- raise "Unknown output format '#{format}'."
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 [-DnV] [-c file] [-P profile] [-E proxy] [-t time] ' \
11
+ ['point [-DnViq] [-c file] [-P profile] [-E proxy] [-t time] ' \
12
12
  '[-p port] [-H host] [-T tag...] <metric> <value>',
13
- 'file [-DnV] [-c file] [-P profile] [-E proxy] [-H host] ' \
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 block
2
+ # this definition
3
3
  #
4
- # dir = Pathname.new(__FILE__).dirname.realpath.parent.parent.parent
5
- # $LOAD_PATH.<< dir + 'lib'
6
- # $LOAD_PATH.<< dir + 'wavefront-sdk' + 'lib'
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
- %i[sent rejected unsent].each do |k|
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
@@ -0,0 +1,10 @@
1
+ module WavefrontOutput
2
+ class Base
3
+ attr_reader :resp, :options
4
+
5
+ def initialize(resp, options)
6
+ @resp = resp
7
+ @options = options
8
+ end
9
+ end
10
+ end