wavefront-client 3.5.4 → 3.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README-cli.md +162 -9
- data/bin/wavefront +41 -1
- data/lib/wavefront/alerting.rb +111 -24
- data/lib/wavefront/cli/alerts.rb +61 -7
- data/lib/wavefront/cli/dashboards.rb +138 -0
- data/lib/wavefront/client/version.rb +1 -1
- data/lib/wavefront/constants.rb +5 -2
- data/lib/wavefront/dashboards.rb +106 -0
- data/lib/wavefront/metadata.rb +0 -34
- data/lib/wavefront/mixins.rb +57 -8
- data/spec/cli_spec.rb +335 -9
- data/spec/spec_helper.rb +7 -0
- data/spec/wavefront/alerting_spec.rb +62 -15
- data/spec/wavefront/cli/alerts_spec.rb +2 -2
- data/spec/wavefront/cli/resources/alert.human.erb +2 -2
- data/spec/wavefront/cli/resources/alert.human2 +2 -2
- data/spec/wavefront/{resources → cli/resources}/conf.yaml +0 -0
- data/spec/wavefront/cli/resources/input_alert.json +34 -0
- data/spec/wavefront/cli/resources/input_alert.yaml +22 -0
- data/spec/wavefront/cli/resources/sample_alert.json +63 -0
- data/spec/wavefront/cli/resources/sample_alert.txt +2 -0
- data/spec/wavefront/cli/resources/sample_alert.yaml +47 -0
- data/spec/wavefront/cli/resources/sample_dash.json +1 -0
- data/spec/wavefront/cli/resources/sample_dash.txt +2 -0
- data/spec/wavefront/cli/resources/sample_dash.yaml +1114 -0
- data/spec/wavefront/opt_handler_spec.rb +3 -5
- metadata +22 -4
data/lib/wavefront/cli/alerts.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
1
|
# Copyright 2015 Wavefront Inc.
|
4
2
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
3
|
# you may not use this file except in compliance with the License.
|
@@ -15,24 +13,38 @@
|
|
15
13
|
|
16
14
|
require 'wavefront/alerting'
|
17
15
|
require 'wavefront/cli'
|
16
|
+
require 'wavefront/mixins'
|
18
17
|
require 'json'
|
18
|
+
require 'yaml'
|
19
19
|
require 'pp'
|
20
20
|
require 'time'
|
21
21
|
|
22
22
|
class Wavefront::Cli::Alerts < Wavefront::Cli
|
23
|
+
include Wavefront::Mixins
|
23
24
|
|
24
|
-
attr_accessor :options, :arguments
|
25
|
+
attr_accessor :options, :arguments, :wfa
|
25
26
|
|
26
27
|
def run
|
27
28
|
raise 'Missing token.' if ! @options[:token] || @options[:token].empty?
|
28
29
|
raise 'Missing query.' if arguments.empty?
|
29
|
-
|
30
|
+
valid_format?(@options[:alertformat].to_sym)
|
30
31
|
|
31
|
-
wfa = Wavefront::Alerting.new(@options[:token], @options[:endpoint],
|
32
|
+
@wfa = Wavefront::Alerting.new(@options[:token], @options[:endpoint],
|
32
33
|
@options[:debug], {
|
33
34
|
noop: @options[:noop], verbose: @options[:verbose]})
|
35
|
+
|
36
|
+
if options[:export]
|
37
|
+
export_alert(options[:'<timestamp>'])
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
if options[:import]
|
42
|
+
import_alert
|
43
|
+
return
|
44
|
+
end
|
45
|
+
|
46
|
+
query = arguments[0].to_sym
|
34
47
|
valid_state?(wfa, query)
|
35
|
-
valid_format?(@options[:alertformat].to_sym)
|
36
48
|
options = { host: @options[:endpoint] }
|
37
49
|
|
38
50
|
if @options[:shared]
|
@@ -54,6 +66,46 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
54
66
|
exit
|
55
67
|
end
|
56
68
|
|
69
|
+
def import_alert
|
70
|
+
raw = load_file(options[:'<file>'])
|
71
|
+
|
72
|
+
begin
|
73
|
+
prepped = wfa.import_to_create(raw)
|
74
|
+
rescue => e
|
75
|
+
puts e if options[:debug]
|
76
|
+
raise 'could not parse input.'
|
77
|
+
end
|
78
|
+
|
79
|
+
begin
|
80
|
+
wfa.create_alert(prepped)
|
81
|
+
puts 'Alert imported.' unless options[:noop]
|
82
|
+
rescue RestClient::BadRequest
|
83
|
+
raise '400 error: alert probably exists.'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def export_alert(id)
|
88
|
+
begin
|
89
|
+
resp = wfa.get_alert(id)
|
90
|
+
rescue => e
|
91
|
+
puts e if @options[:debug]
|
92
|
+
raise 'Unable to retrieve alert.'
|
93
|
+
end
|
94
|
+
|
95
|
+
return if options[:noop]
|
96
|
+
|
97
|
+
case options[:alertformat].to_sym
|
98
|
+
when :json
|
99
|
+
puts JSON.pretty_generate(resp)
|
100
|
+
when :yaml
|
101
|
+
puts resp.to_yaml
|
102
|
+
when :human
|
103
|
+
puts humanize([resp])
|
104
|
+
else
|
105
|
+
puts 'unknown output format.'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
57
109
|
def format_result(result, format)
|
58
110
|
#
|
59
111
|
# Call a suitable method to display the output of the API call,
|
@@ -66,6 +118,8 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
66
118
|
pp result
|
67
119
|
when :json
|
68
120
|
puts JSON.pretty_generate(JSON.parse(result))
|
121
|
+
when :yaml
|
122
|
+
puts JSON.parse(result).to_yaml
|
69
123
|
when :human
|
70
124
|
puts humanize(JSON.parse(result))
|
71
125
|
else
|
@@ -137,7 +191,7 @@ class Wavefront::Cli::Alerts < Wavefront::Cli
|
|
137
191
|
# The 'created' and 'updated' timestamps are in epoch
|
138
192
|
# milliseconds
|
139
193
|
#
|
140
|
-
human_line(k, Time.at(v / 1000))
|
194
|
+
human_line(k, "#{Time.at(v / 1000)} (#{v})")
|
141
195
|
end
|
142
196
|
|
143
197
|
def human_line_updated(k, v)
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'wavefront/dashboards'
|
2
|
+
require 'wavefront/cli'
|
3
|
+
require 'pathname'
|
4
|
+
require 'json'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
class Wavefront::Cli::Dashboards < Wavefront::Cli
|
8
|
+
attr_accessor :wfd
|
9
|
+
|
10
|
+
include Wavefront::Constants
|
11
|
+
include Wavefront::Mixins
|
12
|
+
|
13
|
+
def run
|
14
|
+
@wfd = Wavefront::Dashboards.new(
|
15
|
+
options[:token], options[:endpoint], options[:debug],
|
16
|
+
noop: options[:noop], verbose: options[:verbose]
|
17
|
+
)
|
18
|
+
|
19
|
+
list_dashboards if options[:list]
|
20
|
+
export_dash if options[:export]
|
21
|
+
create_dash if options[:create]
|
22
|
+
delete_dash if options[:delete]
|
23
|
+
undelete_dash if options[:undelete]
|
24
|
+
history_dash if options[:history]
|
25
|
+
clone_dash if options[:clone]
|
26
|
+
import_dash if options[:import]
|
27
|
+
end
|
28
|
+
|
29
|
+
def import_dash
|
30
|
+
wfd.import(load_file(options[:'<file>']).to_json, options[:force])
|
31
|
+
puts 'Dashboard imported' unless options[:noop]
|
32
|
+
rescue RestClient::BadRequest
|
33
|
+
raise '400 error: dashboard probably exists, and force not used'
|
34
|
+
end
|
35
|
+
|
36
|
+
def clone_dash
|
37
|
+
wfd.clone(options[:'<source_id>'], options[:'<new_id>'],
|
38
|
+
options[:'<new_name>'], options[:version])
|
39
|
+
puts 'Dashboard cloned' unless options[:noop]
|
40
|
+
rescue RestClient::BadRequest
|
41
|
+
raise '400 error: either target exists or source does not'
|
42
|
+
end
|
43
|
+
|
44
|
+
def history_dash
|
45
|
+
begin
|
46
|
+
resp = wfd.history(options[:'<dashboard_id>'],
|
47
|
+
options[:start] || 100,
|
48
|
+
options[:limit] || nil)
|
49
|
+
rescue RestClient::ResourceNotFound
|
50
|
+
raise 'Dashboard does not exist'
|
51
|
+
end
|
52
|
+
|
53
|
+
display_resp(resp, :human_history)
|
54
|
+
end
|
55
|
+
|
56
|
+
def undelete_dash
|
57
|
+
wfd.undelete(options[:'<dashboard_id>'])
|
58
|
+
puts 'dashboard undeleted' unless options[:noop]
|
59
|
+
rescue RestClient::ResourceNotFound
|
60
|
+
raise 'Dashboard does not exist'
|
61
|
+
end
|
62
|
+
|
63
|
+
def delete_dash
|
64
|
+
wfd.delete(options[:'<dashboard_id>'])
|
65
|
+
puts 'dashboard deleted' unless options[:noop]
|
66
|
+
rescue RestClient::ResourceNotFound
|
67
|
+
raise 'Dashboard does not exist'
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_dash
|
71
|
+
wfd.create(options[:'<dashboard_id>'], options[:'<name>'])
|
72
|
+
puts 'dashboard created' unless options[:noop]
|
73
|
+
rescue RestClient::BadRequest
|
74
|
+
raise '400 error: dashboard probably exists'
|
75
|
+
end
|
76
|
+
|
77
|
+
def export_dash
|
78
|
+
resp = wfd.export(options[:'<dashboard_id>'], options[:version] || nil)
|
79
|
+
options[:dashformat] = :json if options[:dashformat] == :human
|
80
|
+
display_resp(resp)
|
81
|
+
end
|
82
|
+
|
83
|
+
def list_dashboards
|
84
|
+
resp = wfd.list({ private: options[:privatetag],
|
85
|
+
shared: options[:sharedtag] })
|
86
|
+
display_resp(resp, :human_list)
|
87
|
+
end
|
88
|
+
|
89
|
+
def display_resp(resp, human_method = nil)
|
90
|
+
return if options[:noop]
|
91
|
+
|
92
|
+
case options[:dashformat].to_sym
|
93
|
+
when :json
|
94
|
+
if resp.is_a?(String)
|
95
|
+
puts resp
|
96
|
+
else
|
97
|
+
puts resp.to_json
|
98
|
+
end
|
99
|
+
when :yaml
|
100
|
+
puts resp.to_yaml
|
101
|
+
when :human
|
102
|
+
unless human_method
|
103
|
+
raise 'human output format is not supported by this subcommand'
|
104
|
+
end
|
105
|
+
|
106
|
+
send(human_method, JSON.parse(resp))
|
107
|
+
else
|
108
|
+
raise 'unsupported output format'
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def human_history(resp)
|
113
|
+
resp.each do |rev|
|
114
|
+
puts format('%-4s%s (%s)', rev['version'],
|
115
|
+
Time.at(rev['update_time'].to_i / 1000),
|
116
|
+
rev['update_user'])
|
117
|
+
|
118
|
+
next unless rev['change_description']
|
119
|
+
rev['change_description'].each { |desc| puts ' ' + desc }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def human_list(resp)
|
124
|
+
#
|
125
|
+
# Simply list the dashboards we have. If the user wants more
|
126
|
+
#
|
127
|
+
max_id_width = resp.map { |s| s['url'].size }.max
|
128
|
+
|
129
|
+
puts format("%-#{max_id_width + 1}s%s", 'ID', 'NAME')
|
130
|
+
|
131
|
+
resp.each do |dash|
|
132
|
+
next if !options[:all] && dash['isTrash']
|
133
|
+
line = format("%-#{max_id_width + 1}s%s", dash['url'], dash['name'])
|
134
|
+
line.<< ' (in trash)' if dash['isTrash']
|
135
|
+
puts line
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
data/lib/wavefront/constants.rb
CHANGED
@@ -26,10 +26,12 @@ module Wavefront
|
|
26
26
|
DEFAULT_STRICT = true
|
27
27
|
DEFAULT_OBSOLETE_METRICS = false
|
28
28
|
FORMATS = [ :raw, :ruby, :graphite, :highcharts, :human ]
|
29
|
-
ALERT_FORMATS = [:ruby, :json, :human]
|
29
|
+
ALERT_FORMATS = [:ruby, :json, :human, :yaml]
|
30
30
|
SOURCE_FORMATS = [:ruby, :json, :human]
|
31
|
+
DASH_FORMATS = [:json, :human, :yaml]
|
31
32
|
DEFAULT_ALERT_FORMAT = :human
|
32
33
|
DEFAULT_SOURCE_FORMAT = :human
|
34
|
+
DEFAULT_DASH_FORMAT = :human
|
33
35
|
GRANULARITIES = %w( s m h d )
|
34
36
|
EVENT_STATE_DIR = Pathname.new('/var/tmp/wavefront/events')
|
35
37
|
EVENT_LEVELS = %w(info smoke warn severe)
|
@@ -51,7 +53,8 @@ module Wavefront
|
|
51
53
|
format: DEFAULT_FORMAT, # ts output format
|
52
54
|
alertformat: DEFAULT_ALERT_FORMAT, # alert command output format
|
53
55
|
infileformat: DEFAULT_INFILE_FORMAT, # batch writer file format
|
54
|
-
sourceformat: DEFAULT_SOURCE_FORMAT, # source
|
56
|
+
sourceformat: DEFAULT_SOURCE_FORMAT, # source output format
|
57
|
+
dashformat: DEFAULT_DASH_FORMAT, # dashboard output format
|
55
58
|
}.freeze
|
56
59
|
end
|
57
60
|
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require_relative 'client/version'
|
2
|
+
require_relative 'exception'
|
3
|
+
require 'rest_client'
|
4
|
+
require 'uri'
|
5
|
+
require 'logger'
|
6
|
+
require 'wavefront/constants'
|
7
|
+
require 'wavefront/mixins'
|
8
|
+
|
9
|
+
module Wavefront
|
10
|
+
#
|
11
|
+
# Wrappers around the v1 dashboards API
|
12
|
+
#
|
13
|
+
class Dashboards
|
14
|
+
include Wavefront::Constants
|
15
|
+
include Wavefront::Mixins
|
16
|
+
DEFAULT_PATH = '/api/dashboard/'.freeze
|
17
|
+
|
18
|
+
attr_reader :headers, :noop, :verbose, :endpoint
|
19
|
+
|
20
|
+
def initialize(token, host = DEFAULT_HOST, debug = false, options = {})
|
21
|
+
#
|
22
|
+
# Following existing convention, 'host' is the Wavefront API endpoint.
|
23
|
+
#
|
24
|
+
@headers = { :'X-AUTH-TOKEN' => token }
|
25
|
+
@endpoint = host
|
26
|
+
debug(debug)
|
27
|
+
@noop = options[:noop]
|
28
|
+
@verbose = options[:verbose]
|
29
|
+
end
|
30
|
+
|
31
|
+
def import(schema, force = false)
|
32
|
+
#
|
33
|
+
# Imports a dashboard described as a JSON string (schema)
|
34
|
+
#
|
35
|
+
qs = force ? nil : 'rejectIfExists=true'
|
36
|
+
call_post(create_uri(qs: qs), schema, 'application/json')
|
37
|
+
end
|
38
|
+
|
39
|
+
def clone(source_id, dest_id, dest_name, source_ver = nil)
|
40
|
+
#
|
41
|
+
# Clone a dashboard. If source_ver is not truthy, the latest
|
42
|
+
# version of the source is used.
|
43
|
+
#
|
44
|
+
qs = hash_to_qs(name: dest_name, url: dest_id)
|
45
|
+
qs.<< "&v=#{source_ver}" if source_ver
|
46
|
+
|
47
|
+
call_post(create_uri(path: uri_concat(source_id, 'clone')), qs,
|
48
|
+
'application/x-www-form-urlencoded')
|
49
|
+
end
|
50
|
+
|
51
|
+
def history(id, start = 100, limit = nil)
|
52
|
+
qs = "start=#{start}"
|
53
|
+
qs.<< "&limit=#{limit}" if limit
|
54
|
+
|
55
|
+
call_get(create_uri(path: uri_concat(id, 'history'), qs: qs))
|
56
|
+
end
|
57
|
+
|
58
|
+
def list(opts = {})
|
59
|
+
qs = []
|
60
|
+
|
61
|
+
opts[:private].map { |t| qs.<< "userTag=#{t}" } if opts[:private]
|
62
|
+
opts[:shared].map { |t| qs.<< "customerTag=#{t}" } if opts[:shared]
|
63
|
+
|
64
|
+
call_get(create_uri(qs: qs.join('&')))
|
65
|
+
end
|
66
|
+
|
67
|
+
def undelete(id)
|
68
|
+
call_post(create_uri(path: uri_concat(id, 'undelete')))
|
69
|
+
end
|
70
|
+
|
71
|
+
def delete(id)
|
72
|
+
call_post(create_uri(path: uri_concat(id, 'delete')))
|
73
|
+
end
|
74
|
+
|
75
|
+
def export(id, version = nil)
|
76
|
+
path = version ? uri_concat(id, version) : id
|
77
|
+
resp = call_get(create_uri(path: path)) || '{}'
|
78
|
+
JSON.parse(resp)
|
79
|
+
end
|
80
|
+
|
81
|
+
def create(id, name)
|
82
|
+
call_post(create_uri(path: uri_concat([id, 'create'])),
|
83
|
+
"name=#{URI.encode(name)}",
|
84
|
+
'application/x-www-form-urlencoded')
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_uri(options = {})
|
88
|
+
#
|
89
|
+
# Build the URI we use to send a 'create' request.
|
90
|
+
#
|
91
|
+
options[:host] ||= endpoint
|
92
|
+
options[:path] ||= ''
|
93
|
+
options[:qs] ||= nil
|
94
|
+
|
95
|
+
URI::HTTPS.build(
|
96
|
+
host: options[:host],
|
97
|
+
path: uri_concat(DEFAULT_PATH, options[:path]),
|
98
|
+
query: options[:qs]
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
def debug(enabled)
|
103
|
+
RestClient.log = 'stdout' if enabled
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/wavefront/metadata.rb
CHANGED
@@ -209,40 +209,6 @@ module Wavefront
|
|
209
209
|
)
|
210
210
|
end
|
211
211
|
|
212
|
-
def call_get(uri)
|
213
|
-
if (verbose || noop)
|
214
|
-
puts 'GET ' + uri.to_s
|
215
|
-
puts 'HEADERS ' + headers.to_s
|
216
|
-
end
|
217
|
-
return if noop
|
218
|
-
RestClient.get(uri.to_s, headers)
|
219
|
-
end
|
220
|
-
|
221
|
-
def call_delete(uri)
|
222
|
-
if (verbose || noop)
|
223
|
-
puts 'DELETE ' + uri.to_s
|
224
|
-
puts 'HEADERS ' + headers.to_s
|
225
|
-
end
|
226
|
-
return if noop
|
227
|
-
RestClient.delete(uri.to_s, headers)
|
228
|
-
end
|
229
|
-
|
230
|
-
def call_post(uri, query = nil)
|
231
|
-
h = headers
|
232
|
-
if (verbose || noop)
|
233
|
-
puts 'POST ' + uri.to_s
|
234
|
-
puts 'QUERY ' + query if query
|
235
|
-
puts 'HEADERS ' + h.to_s
|
236
|
-
end
|
237
|
-
return if noop
|
238
|
-
|
239
|
-
RestClient.post(uri.to_s, query,
|
240
|
-
h.merge(:'Content-Type' => 'text/plain',
|
241
|
-
:Accept => 'application/json'
|
242
|
-
)
|
243
|
-
)
|
244
|
-
end
|
245
|
-
|
246
212
|
def debug(enabled)
|
247
213
|
RestClient.log = 'stdout' if enabled
|
248
214
|
end
|
data/lib/wavefront/mixins.rb
CHANGED
@@ -33,14 +33,12 @@ module Wavefront
|
|
33
33
|
#
|
34
34
|
# Return a time as an integer, however it might come in.
|
35
35
|
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
raise "cannot parse timestamp '#{t}'."
|
43
|
-
end
|
36
|
+
return t if t.is_a?(Integer)
|
37
|
+
return t.to_i if t.is_a?(Time)
|
38
|
+
return t.to_i if t.is_a?(String) && t.match(/^\d+$/)
|
39
|
+
DateTime.parse("#{t} #{Time.now.getlocal.zone}").to_time.utc.to_i
|
40
|
+
rescue
|
41
|
+
raise "cannot parse timestamp '#{t}'."
|
44
42
|
end
|
45
43
|
|
46
44
|
def time_to_ms(t)
|
@@ -62,5 +60,56 @@ module Wavefront
|
|
62
60
|
def uri_concat(*args)
|
63
61
|
args.join('/').squeeze('/')
|
64
62
|
end
|
63
|
+
|
64
|
+
def call_get(uri)
|
65
|
+
if verbose || noop
|
66
|
+
puts 'GET ' + uri.to_s
|
67
|
+
puts 'HEADERS ' + headers.to_s
|
68
|
+
end
|
69
|
+
return if noop
|
70
|
+
RestClient.get(uri.to_s, headers)
|
71
|
+
end
|
72
|
+
|
73
|
+
def call_post(uri, query = nil, ctype = 'text/plain')
|
74
|
+
h = headers
|
75
|
+
if verbose || noop
|
76
|
+
puts 'POST ' + uri.to_s
|
77
|
+
puts 'QUERY ' + query if query
|
78
|
+
puts 'HEADERS ' + h.to_s
|
79
|
+
end
|
80
|
+
return if noop
|
81
|
+
|
82
|
+
RestClient.post(uri.to_s, query,
|
83
|
+
h.merge(:'Content-Type' => ctype,
|
84
|
+
:Accept => 'application/json'))
|
85
|
+
end
|
86
|
+
|
87
|
+
def call_delete(uri)
|
88
|
+
if verbose || noop
|
89
|
+
puts 'DELETE ' + uri.to_s
|
90
|
+
puts 'HEADERS ' + headers.to_s
|
91
|
+
end
|
92
|
+
return if noop
|
93
|
+
RestClient.delete(uri.to_s, headers)
|
94
|
+
end
|
95
|
+
|
96
|
+
def load_file(path)
|
97
|
+
#
|
98
|
+
# Give it a path to a file (as a string) and it will return the
|
99
|
+
# contents of that file as a Ruby object. Automatically detects
|
100
|
+
# JSON and YAML. Raises an exception if it doesn't look like
|
101
|
+
# either.
|
102
|
+
#
|
103
|
+
file = Pathname.new(path)
|
104
|
+
raise 'Import file does not exist.' unless file.exist?
|
105
|
+
|
106
|
+
if file.extname == '.json'
|
107
|
+
JSON.parse(IO.read(file))
|
108
|
+
elsif file.extname == '.yaml' || file.extname == '.yml'
|
109
|
+
YAML.load(IO.read(file))
|
110
|
+
else
|
111
|
+
raise 'Unsupported file format.'
|
112
|
+
end
|
113
|
+
end
|
65
114
|
end
|
66
115
|
end
|