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 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