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