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.
@@ -24,27 +24,14 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
24
24
  attr_accessor :options, :arguments
25
25
 
26
26
  def run
27
- alerts = Wavefront::Alerting.new(@options[:token])
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
- unless queries.include?(query)
35
- raise 'State must be one of: ' + queries.each {|q| q.to_s}.join(', ')
36
- end
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
- result = alerts.send(query, options)
44
+ begin
45
+ result = wfa.send(query, options)
46
+ rescue
47
+ raise 'Unable to execute query.'
48
+ end
58
49
 
59
- case @options[:format].to_sym
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
- puts "Invalid output format, See --help for more detail."
68
- exit 1
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
- exit 0
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#{' ' * offset}")
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
@@ -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
@@ -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