wavefront-cli 2.18.0 → 3.0.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 +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +7 -0
- data/.travis.yml +4 -5
- data/HISTORY.md +20 -1
- data/README.md +79 -29
- data/lib/wavefront-cli/base.rb +26 -2
- data/lib/wavefront-cli/commands/alert.rb +10 -10
- data/lib/wavefront-cli/commands/base.rb +15 -2
- data/lib/wavefront-cli/commands/cloudintegration.rb +3 -3
- data/lib/wavefront-cli/commands/config.rb +2 -1
- data/lib/wavefront-cli/commands/dashboard.rb +8 -6
- data/lib/wavefront-cli/commands/derivedmetric.rb +5 -5
- data/lib/wavefront-cli/commands/event.rb +3 -3
- data/lib/wavefront-cli/commands/integration.rb +5 -5
- data/lib/wavefront-cli/commands/link.rb +3 -3
- data/lib/wavefront-cli/commands/message.rb +2 -2
- data/lib/wavefront-cli/commands/metric.rb +1 -1
- data/lib/wavefront-cli/commands/notificant.rb +3 -3
- data/lib/wavefront-cli/commands/proxy.rb +3 -3
- data/lib/wavefront-cli/commands/query.rb +3 -3
- data/lib/wavefront-cli/commands/savedsearch.rb +3 -3
- data/lib/wavefront-cli/commands/settings.rb +22 -0
- data/lib/wavefront-cli/commands/source.rb +3 -3
- data/lib/wavefront-cli/commands/user.rb +4 -4
- data/lib/wavefront-cli/commands/usergroup.rb +5 -8
- data/lib/wavefront-cli/commands/webhook.rb +3 -3
- data/lib/wavefront-cli/commands/window.rb +3 -3
- data/lib/wavefront-cli/commands/write.rb +6 -4
- data/lib/wavefront-cli/config.rb +14 -0
- data/lib/wavefront-cli/controller.rb +2 -0
- data/lib/wavefront-cli/dashboard.rb +133 -14
- data/lib/wavefront-cli/display/base.rb +28 -7
- data/lib/wavefront-cli/display/dashboard.rb +32 -8
- data/lib/wavefront-cli/display/distribution.rb +4 -1
- data/lib/wavefront-cli/display/printer/long.rb +149 -140
- data/lib/wavefront-cli/display/printer/terse.rb +19 -42
- data/lib/wavefront-cli/display/query.rb +6 -0
- data/lib/wavefront-cli/display/settings.rb +21 -0
- data/lib/wavefront-cli/display/write.rb +12 -5
- data/lib/wavefront-cli/event.rb +1 -1
- data/lib/wavefront-cli/exception.rb +1 -0
- data/lib/wavefront-cli/settings.rb +37 -0
- data/lib/wavefront-cli/stdlib/array.rb +20 -0
- data/lib/wavefront-cli/stdlib/string.rb +8 -0
- data/lib/wavefront-cli/user.rb +1 -1
- data/lib/wavefront-cli/version.rb +1 -1
- data/lib/wavefront-cli/write.rb +310 -10
- data/spec/.rubocop.yml +3 -0
- data/spec/spec_helper.rb +81 -1
- data/spec/wavefront-cli/alert_spec.rb +1 -0
- data/spec/wavefront-cli/cloudintegration_spec.rb +1 -0
- data/spec/wavefront-cli/dashboard_spec.rb +47 -4
- data/spec/wavefront-cli/derivedmetric_spec.rb +1 -0
- data/spec/wavefront-cli/display/base_spec.rb +16 -9
- data/spec/wavefront-cli/display/printer/long_spec.rb +75 -106
- data/spec/wavefront-cli/display/printer/terse_spec.rb +33 -21
- data/spec/wavefront-cli/event_spec.rb +1 -0
- data/spec/wavefront-cli/externallink_spec.rb +1 -0
- data/spec/wavefront-cli/integration_spec.rb +1 -0
- data/spec/wavefront-cli/maintenancewindow_spec.rb +1 -0
- data/spec/wavefront-cli/proxy_spec.rb +2 -0
- data/spec/wavefront-cli/query_spec.rb +9 -0
- data/spec/wavefront-cli/resources/display/alert-human-long +24 -0
- data/spec/wavefront-cli/resources/display/alert-input.json +37 -0
- data/spec/wavefront-cli/resources/display/alerts-human-terse +9 -0
- data/spec/wavefront-cli/resources/display/alerts-input.json +1 -0
- data/spec/wavefront-cli/resources/display/user-human-long +19 -0
- data/spec/wavefront-cli/resources/display/user-human-long-no_sep +18 -0
- data/spec/wavefront-cli/resources/display/user-input.json +1 -0
- data/spec/wavefront-cli/resources/responses/README.md +2 -0
- data/spec/wavefront-cli/resources/responses/alert-list.json +1 -0
- data/spec/wavefront-cli/resources/responses/backups.json +1 -0
- data/spec/wavefront-cli/resources/responses/cloudintegration-list.json +1 -0
- data/spec/wavefront-cli/resources/responses/dashboard-list.json +1 -0
- data/spec/wavefront-cli/resources/responses/derivedmetric-list.json +1 -0
- data/spec/wavefront-cli/resources/responses/event-list.json +1 -0
- data/spec/wavefront-cli/resources/responses/integration-list.json +1 -0
- data/spec/wavefront-cli/resources/responses/link-list.json +1 -0
- data/spec/wavefront-cli/resources/responses/notificant-list.json +1 -0
- data/spec/wavefront-cli/resources/responses/proxy-list.json +1 -0
- data/spec/wavefront-cli/resources/responses/query-cpu.json +1 -0
- data/spec/wavefront-cli/resources/responses/savedsearch-list.json +1 -0
- data/spec/wavefront-cli/resources/responses/user-list.json +1 -0
- data/spec/wavefront-cli/resources/responses/usergroup-list.json +1 -0
- data/spec/wavefront-cli/resources/responses/webhook-list.json +1 -0
- data/spec/wavefront-cli/resources/responses/window-list.json +1 -0
- data/spec/wavefront-cli/savedsearch_spec.rb +1 -0
- data/spec/wavefront-cli/settings_spec.rb +19 -0
- data/spec/wavefront-cli/stdlib/array_spec.rb +20 -0
- data/spec/wavefront-cli/stdlib/string_spec.rb +13 -1
- data/spec/wavefront-cli/user_spec.rb +1 -0
- data/spec/wavefront-cli/usergroup_spec.rb +1 -0
- data/spec/wavefront-cli/webhook_spec.rb +1 -0
- data/spec/wavefront-cli/write_spec.rb +167 -0
- data/wavefront-cli.gemspec +3 -4
- metadata +65 -31
- data/.gitlab-ci.yml +0 -16
- data/lib/wavefront-cli/base_write.rb +0 -298
- data/lib/wavefront-cli/commands/report.rb +0 -37
- data/lib/wavefront-cli/display/printer/base.rb +0 -24
- data/lib/wavefront-cli/display/report.rb +0 -17
- data/lib/wavefront-cli/report.rb +0 -18
- data/spec/wavefront-cli/display/printer/base_spec.rb +0 -20
|
@@ -1,57 +1,34 @@
|
|
|
1
|
-
require_relative '
|
|
1
|
+
require_relative '../../stdlib/array'
|
|
2
2
|
|
|
3
3
|
module WavefrontDisplayPrinter
|
|
4
4
|
#
|
|
5
5
|
# Print things which are per-row. The terse listings, primarily
|
|
6
6
|
#
|
|
7
|
-
class Terse
|
|
8
|
-
attr_reader :data, :
|
|
7
|
+
class Terse
|
|
8
|
+
attr_reader :data, :fmt
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@
|
|
10
|
+
# @param data [Hash] data to display, from a response object
|
|
11
|
+
# @param keys [Array[Symbol]] keys to display, in order
|
|
12
|
+
#
|
|
13
|
+
def initialize(data, keys)
|
|
14
|
+
@data = stringify(data, keys)
|
|
15
|
+
@fmt = format_string(data, keys)
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def format_string
|
|
20
|
-
return '%s' if keys.length == 1
|
|
21
|
-
lk = longest_keys
|
|
22
|
-
keys.each_with_object('') { |k, out| out.<< "%-#{lk[k]}s " }
|
|
18
|
+
def format_string(data, keys)
|
|
19
|
+
keys.map { |k| "%-#{data.longest_value_of(k)}<#{k}>s" }.join(' ')
|
|
23
20
|
end
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
#
|
|
28
|
-
# @return [Hash] with the same keys as :keys and Integer values
|
|
29
|
-
#
|
|
30
|
-
# rubocop:disable Metrics/AbcSize
|
|
31
|
-
def longest_keys
|
|
32
|
-
keys.each_with_object(Hash[*keys.map { |k| [k, 0] }.flatten]) \
|
|
33
|
-
do |k, aggr|
|
|
34
|
-
data.each do |obj|
|
|
35
|
-
next unless obj.key?(k)
|
|
36
|
-
val = obj[k]
|
|
37
|
-
val = val.join(', ') if val.is_a?(Array)
|
|
38
|
-
aggr[k] = val.size if val.size > aggr[k]
|
|
39
|
-
end
|
|
40
|
-
end
|
|
22
|
+
def stringify(data, keys)
|
|
23
|
+
data.map { |e| e.tap { keys.each { |k| e[k] = to_list(e[k]) } } }
|
|
41
24
|
end
|
|
42
|
-
# rubocop:enable Metrics/AbcSize
|
|
43
25
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def prep_output
|
|
51
|
-
data.each_with_object([]) do |o, aggr|
|
|
52
|
-
args = keys.map { |k| o[k].is_a?(Array) ? o[k].join(', ') : o[k] }
|
|
53
|
-
aggr.<< format(fmt_string, *args).rstrip
|
|
54
|
-
end
|
|
26
|
+
def to_list(thing)
|
|
27
|
+
thing.is_a?(Array) ? thing.join(', ') : thing
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_s
|
|
31
|
+
data.map { |e| format(fmt, e).rstrip }.join("\n")
|
|
55
32
|
end
|
|
56
33
|
end
|
|
57
34
|
end
|
|
@@ -22,6 +22,12 @@ module WavefrontDisplay
|
|
|
22
22
|
end
|
|
23
23
|
# rubocop:enable Metrics/AbcSize
|
|
24
24
|
|
|
25
|
+
# Prioritizing keys does not make sense in this context
|
|
26
|
+
#
|
|
27
|
+
def prioritize_keys(data, _keys)
|
|
28
|
+
data
|
|
29
|
+
end
|
|
30
|
+
|
|
25
31
|
def mk_timeseries(data)
|
|
26
32
|
return [] unless data.key?(:timeseries)
|
|
27
33
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
|
|
3
|
+
module WavefrontDisplay
|
|
4
|
+
#
|
|
5
|
+
# Format human-readable output for external links.
|
|
6
|
+
#
|
|
7
|
+
class Settings < Base
|
|
8
|
+
def do_list_permissions
|
|
9
|
+
options[:long] ? long_output : multicolumn(:groupName)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def do_list_usergroups
|
|
13
|
+
if options[:long]
|
|
14
|
+
readable_time_arr(:createdEpochMillis)
|
|
15
|
+
long_output
|
|
16
|
+
else
|
|
17
|
+
multicolumn(:id, :name)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -2,21 +2,28 @@ require_relative 'base'
|
|
|
2
2
|
|
|
3
3
|
module WavefrontDisplay
|
|
4
4
|
# Format human-readable output when writing points.
|
|
5
|
+
# In this context data is a Hash of the form
|
|
6
|
+
# { sent: 1, rejected: 0, unsent: 0 }
|
|
5
7
|
#
|
|
6
8
|
class Write < Base
|
|
7
|
-
|
|
9
|
+
attr_reader :not_sent
|
|
10
|
+
|
|
8
11
|
def do_point
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
@not_sent = data['rejected'] + data['unsent']
|
|
13
|
+
report unless nothing_to_say?
|
|
14
|
+
exit not_sent.zero? ? 0 : 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def nothing_to_say?
|
|
18
|
+
options[:quiet] || not_sent.positive?
|
|
11
19
|
end
|
|
12
|
-
# rubocop:enable Metrics/AbcSize
|
|
13
20
|
|
|
14
21
|
def do_file
|
|
15
22
|
do_point
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
def report
|
|
19
|
-
%
|
|
26
|
+
%w[sent rejected unsent].each do |k|
|
|
20
27
|
puts format(' %12s %d', k.to_s, data[k])
|
|
21
28
|
end
|
|
22
29
|
end
|
data/lib/wavefront-cli/event.rb
CHANGED
|
@@ -68,7 +68,7 @@ module WavefrontCli
|
|
|
68
68
|
abort "No locally stored event matches '#{id}'." unless ev
|
|
69
69
|
|
|
70
70
|
res = wf.close(ev)
|
|
71
|
-
ev_file.unlink if ev_file
|
|
71
|
+
ev_file.unlink if ev_file&.exist? && res.status.code == 200
|
|
72
72
|
res
|
|
73
73
|
end
|
|
74
74
|
# rubocop:enable Metrics/AbcSize
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
|
|
3
|
+
module WavefrontCli
|
|
4
|
+
#
|
|
5
|
+
# CLI coverage for the v2 'settings' API.
|
|
6
|
+
#
|
|
7
|
+
class Settings < WavefrontCli::Base
|
|
8
|
+
def do_list_permissions
|
|
9
|
+
wf.permissions
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def do_show_preferences
|
|
13
|
+
wf.preferences
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def do_default_usergroups
|
|
17
|
+
wf.default_user_groups
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def do_update
|
|
21
|
+
body = options[:'<key=value>'].each_with_object({}) do |o, a|
|
|
22
|
+
k, v = o.split('=', 2)
|
|
23
|
+
next unless v && !v.empty?
|
|
24
|
+
|
|
25
|
+
if %w[invitePermissions defaultUserGroups].include?(k)
|
|
26
|
+
v = v.include?(',') ? v.split(',') : [v]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
a[k] = v
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
pp body
|
|
33
|
+
|
|
34
|
+
wf.update_preferences(body)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Extensions to stdlib's Array
|
|
2
|
+
#
|
|
3
|
+
class Array
|
|
4
|
+
# @return [Integer] the length of the longest string or symbol in
|
|
5
|
+
# an array
|
|
6
|
+
#
|
|
7
|
+
def max_length
|
|
8
|
+
return 0 if empty?
|
|
9
|
+
map(&:to_s).map(&:length).max
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @return [Integer] the length of the longest value in an array of
|
|
13
|
+
# hashes with the given key
|
|
14
|
+
#
|
|
15
|
+
# @param key [String, Symbol] key to search for
|
|
16
|
+
#
|
|
17
|
+
def longest_value_of(key)
|
|
18
|
+
map { |v| v[key] }.max_length
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -54,6 +54,14 @@ class String
|
|
|
54
54
|
tr('^', ' ')
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# Fold long value lines in two-column output. The returned string
|
|
58
|
+
# is appended to a key, so the first line is not indented.
|
|
59
|
+
#
|
|
60
|
+
def value_fold(indent = 0, twidth = TW)
|
|
61
|
+
max_line_length = twidth - indent - 4
|
|
62
|
+
scan_line(max_line_length).join("\n" + ' ' * indent)
|
|
63
|
+
end
|
|
64
|
+
|
|
57
65
|
# @param width [Integer] length of longest string (width of
|
|
58
66
|
# terminal less some margin)
|
|
59
67
|
# @return [Array] original string chunked into an array width
|
data/lib/wavefront-cli/user.rb
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
WF_CLI_VERSION = '
|
|
1
|
+
WF_CLI_VERSION = '3.0.0'.freeze
|
data/lib/wavefront-cli/write.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
require 'wavefront-sdk/support/mixins'
|
|
2
|
+
require_relative 'base'
|
|
2
3
|
|
|
3
4
|
module WavefrontCli
|
|
4
5
|
#
|
|
@@ -6,14 +7,39 @@ module WavefrontCli
|
|
|
6
7
|
# as Report, but has to do a couple of things differently, as it
|
|
7
8
|
# speaks to a proxy rather than to the API.
|
|
8
9
|
#
|
|
9
|
-
class Write <
|
|
10
|
+
class Write < Base
|
|
11
|
+
attr_reader :fmt
|
|
12
|
+
include Wavefront::Mixins
|
|
13
|
+
SPLIT_PATTERN = /\s(?=(?:[^"]|"[^"]*")*$)/
|
|
14
|
+
|
|
15
|
+
# rubocop:disable Metrics/AbcSize
|
|
16
|
+
def do_point
|
|
17
|
+
p = { path: options[:'<metric>'],
|
|
18
|
+
value: options[:'<value>'].delete('\\').to_f }
|
|
19
|
+
|
|
20
|
+
tags = tags_to_hash(options[:tag])
|
|
21
|
+
|
|
22
|
+
p[:tags] = tags unless tags.empty?
|
|
23
|
+
p[:source] = options[:host] if options[:host]
|
|
24
|
+
p[:ts] = parse_time(options[:time]) if options[:time]
|
|
25
|
+
send_point(p)
|
|
26
|
+
end
|
|
27
|
+
# rubocop:enable Metrics/AbcSize
|
|
28
|
+
|
|
29
|
+
def do_file
|
|
30
|
+
valid_format?(options[:infileformat])
|
|
31
|
+
setup_fmt(options[:infileformat] || 'tmv')
|
|
32
|
+
process_input(options[:'<file>'])
|
|
33
|
+
end
|
|
34
|
+
|
|
10
35
|
# rubocop:disable Metrics/AbcSize
|
|
11
36
|
def do_distribution
|
|
12
37
|
p = { path: options[:'<metric>'],
|
|
13
38
|
interval: options[:interval] || 'M',
|
|
14
|
-
value: mk_dist
|
|
15
|
-
tags: tags_to_hash(options[:tag]) }
|
|
39
|
+
value: mk_dist }
|
|
16
40
|
|
|
41
|
+
tags = tags_to_hash(options[:tag])
|
|
42
|
+
p[:tags] = tags unless tags.empty?
|
|
17
43
|
p[:source] = options[:host] if options[:host]
|
|
18
44
|
p[:ts] = parse_time(options[:time]) if options[:time]
|
|
19
45
|
send_point(p)
|
|
@@ -44,7 +70,7 @@ module WavefrontCli
|
|
|
44
70
|
|
|
45
71
|
def distribution?
|
|
46
72
|
return true if options[:distribution]
|
|
47
|
-
options[:infileformat]
|
|
73
|
+
options[:infileformat]&.include?('d')
|
|
48
74
|
end
|
|
49
75
|
|
|
50
76
|
def mk_creds
|
|
@@ -72,11 +98,10 @@ module WavefrontCli
|
|
|
72
98
|
end
|
|
73
99
|
|
|
74
100
|
def validate_opts_file
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
end
|
|
101
|
+
return true if options[:metric] || options[:infileformat]&.include?('m')
|
|
102
|
+
|
|
103
|
+
raise(WavefrontCli::Exception::InsufficientData,
|
|
104
|
+
"Supply a metric path in the file or with '-m'.")
|
|
80
105
|
end
|
|
81
106
|
|
|
82
107
|
def open_connection
|
|
@@ -86,5 +111,280 @@ module WavefrontCli
|
|
|
86
111
|
def close_connection
|
|
87
112
|
wf.close
|
|
88
113
|
end
|
|
114
|
+
|
|
115
|
+
def send_point(point)
|
|
116
|
+
call_write(point)
|
|
117
|
+
rescue Wavefront::Exception::InvalidEndpoint
|
|
118
|
+
abort format("Could not connect to proxy '%s:%s'.",
|
|
119
|
+
options[:proxy], options[:port])
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Read the input, from a file or from STDIN, and turn each line
|
|
123
|
+
# into Wavefront points.
|
|
124
|
+
#
|
|
125
|
+
def process_input(file)
|
|
126
|
+
if file == '-'
|
|
127
|
+
read_stdin
|
|
128
|
+
else
|
|
129
|
+
call_write(
|
|
130
|
+
process_input_file(load_data(Pathname.new(file)).split("\n"))
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @param data [Array[String]] array of lines
|
|
136
|
+
#
|
|
137
|
+
def process_input_file(data)
|
|
138
|
+
data.each_with_object([]) do |l, a|
|
|
139
|
+
begin
|
|
140
|
+
a.<< process_line(l)
|
|
141
|
+
rescue WavefrontCli::Exception::UnparseableInput => e
|
|
142
|
+
puts "Bad input. #{e.message}."
|
|
143
|
+
next
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A wrapper which lets us send normal points, deltas, or
|
|
149
|
+
# distributions
|
|
150
|
+
#
|
|
151
|
+
def call_write(data, openclose = true)
|
|
152
|
+
if options[:delta]
|
|
153
|
+
wf.write_delta(data, openclose)
|
|
154
|
+
else
|
|
155
|
+
wf.write(data, openclose)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Read from standard in and stream points through an open
|
|
160
|
+
# socket. If the user hits ctrl-c, close the socket and exit
|
|
161
|
+
# politely.
|
|
162
|
+
#
|
|
163
|
+
def read_stdin
|
|
164
|
+
open_connection
|
|
165
|
+
STDIN.each_line { |l| call_write(process_line(l.strip), false) }
|
|
166
|
+
close_connection
|
|
167
|
+
rescue SystemExit, Interrupt
|
|
168
|
+
puts 'ctrl-c. Exiting.'
|
|
169
|
+
wf.close
|
|
170
|
+
exit 0
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Find and return the value in a chunked line of input
|
|
174
|
+
#
|
|
175
|
+
# param chunks [Array] a chunked line of input from #process_line
|
|
176
|
+
# return [Float] the value
|
|
177
|
+
# raise TypeError if field does not exist
|
|
178
|
+
# raise Wavefront::Exception::InvalidValue if it's not a value
|
|
179
|
+
#
|
|
180
|
+
def extract_value(chunks)
|
|
181
|
+
if fmt.include?('v')
|
|
182
|
+
v = chunks[fmt.index('v')]
|
|
183
|
+
v.to_f
|
|
184
|
+
else
|
|
185
|
+
raw = chunks[fmt.index('d')].split(',')
|
|
186
|
+
xpanded = expand_dist(raw)
|
|
187
|
+
wf.mk_distribution(xpanded)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# We will let users write a distribution as '1 1 1' or '3x1' or
|
|
192
|
+
# even a mix of the two
|
|
193
|
+
#
|
|
194
|
+
def expand_dist(dist)
|
|
195
|
+
dist.map do |v|
|
|
196
|
+
if v.is_a?(String) && v.include?('x')
|
|
197
|
+
x, val = v.split('x', 2)
|
|
198
|
+
Array.new(x.to_i, val.to_f)
|
|
199
|
+
else
|
|
200
|
+
v.to_f
|
|
201
|
+
end
|
|
202
|
+
end.flatten
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Find and return the source in a chunked line of input.
|
|
206
|
+
#
|
|
207
|
+
# @param chunks [Array] a chunked line of input from #process_line
|
|
208
|
+
# @return [Float] the timestamp, if it is there, or the current
|
|
209
|
+
# UTC time if it is not.
|
|
210
|
+
#
|
|
211
|
+
def extract_ts(chunks)
|
|
212
|
+
ts = chunks[fmt.index('t')]
|
|
213
|
+
return parse_time(ts) if valid_timestamp?(ts)
|
|
214
|
+
rescue TypeError
|
|
215
|
+
Time.now.utc.to_i
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# @param chunks [Array] an input line broken into tokens. The
|
|
219
|
+
# final token will be a space-separated list of point tags.
|
|
220
|
+
# @return [Hash] of k = v tags.
|
|
221
|
+
#
|
|
222
|
+
def extract_tags(chunks)
|
|
223
|
+
tags_to_hash(chunks.last.split(SPLIT_PATTERN))
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Find and return the metric path in a chunked line of input.
|
|
227
|
+
# The path can be in the data, or passed as an option, or both.
|
|
228
|
+
# If the latter, then we assume the option is a prefix, and
|
|
229
|
+
# concatenate the value in the data.
|
|
230
|
+
#
|
|
231
|
+
# param chunks [Array] a chunked line of input from #process_line
|
|
232
|
+
# return [String] the metric path
|
|
233
|
+
# raise TypeError if field does not exist
|
|
234
|
+
#
|
|
235
|
+
def extract_path(chunks)
|
|
236
|
+
m = chunks[fmt.index('m')]
|
|
237
|
+
options[:metric] ? [options[:metric], m].join('.') : m
|
|
238
|
+
rescue TypeError
|
|
239
|
+
return options[:metric] if options[:metric]
|
|
240
|
+
raise
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Find and return the source in a chunked line of input.
|
|
244
|
+
#
|
|
245
|
+
# param chunks [Array] a chunked line of input from #process_line
|
|
246
|
+
# return [String] the source, if it is there, or if not, the
|
|
247
|
+
# value passed through by -H, or the local hostname.
|
|
248
|
+
#
|
|
249
|
+
def extract_source(chunks)
|
|
250
|
+
chunks[fmt.index('s')]
|
|
251
|
+
rescue TypeError
|
|
252
|
+
options[:source] || Socket.gethostname
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Process a line of input, as described by the format string
|
|
256
|
+
# held in @fmt. Produces a hash suitable for the SDK to send on.
|
|
257
|
+
#
|
|
258
|
+
# We let the user define most of the fields, but anything beyond
|
|
259
|
+
# what they define is always assumed to be point tags. This is
|
|
260
|
+
# because you can have arbitrarily many of those for each point.
|
|
261
|
+
#
|
|
262
|
+
# @param line [String] a line of an input file
|
|
263
|
+
# @return [Hash]
|
|
264
|
+
# @raise WavefrontCli::Exception::UnparseableInput if the line
|
|
265
|
+
# doesn't look right
|
|
266
|
+
#
|
|
267
|
+
# rubocop:disable Metrics/AbcSize
|
|
268
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
269
|
+
def process_line(line)
|
|
270
|
+
return true if line.empty?
|
|
271
|
+
chunks = line.split(SPLIT_PATTERN, fmt.length)
|
|
272
|
+
enough_fields?(line) # can raise exception
|
|
273
|
+
|
|
274
|
+
point = { path: extract_path(chunks),
|
|
275
|
+
value: extract_value(chunks) }
|
|
276
|
+
|
|
277
|
+
tags = line_tags(chunks)
|
|
278
|
+
|
|
279
|
+
point.tap do |p|
|
|
280
|
+
p[:tags] = tags unless tags.empty?
|
|
281
|
+
p[:ts] = extract_ts(chunks) if fmt.include?('t')
|
|
282
|
+
p[:source] = extract_source(chunks) if fmt.include?('s')
|
|
283
|
+
p[:interval] = options[:interval] || 'm' if fmt.include?('d')
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
287
|
+
# rubocop:enable Metrics/AbcSize
|
|
288
|
+
|
|
289
|
+
# We can get tags from the file, from the -T option, or both.
|
|
290
|
+
# Merge them, making the -T win if there is a collision.
|
|
291
|
+
#
|
|
292
|
+
def line_tags(chunks)
|
|
293
|
+
file_tags = fmt.last == 'T' ? extract_tags(chunks) : {}
|
|
294
|
+
opt_tags = tags_to_hash(options[:tag]) || {}
|
|
295
|
+
file_tags.merge(opt_tags)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Takes an array of key=value tags (as produced by docopt) and
|
|
299
|
+
# turns it into a hash of key: value tags. Anything not of the
|
|
300
|
+
# form key=val is dropped. If key or value are quoted, we
|
|
301
|
+
# remove the quotes.
|
|
302
|
+
#
|
|
303
|
+
# @param tags [Array[String]]
|
|
304
|
+
# @return [Hash] of k: v tags
|
|
305
|
+
#
|
|
306
|
+
def tags_to_hash(tags)
|
|
307
|
+
return nil unless tags
|
|
308
|
+
|
|
309
|
+
[tags].flatten.each_with_object({}) do |t, ret|
|
|
310
|
+
k, v = t.split('=', 2)
|
|
311
|
+
k.gsub!(/^["']|["']$/, '')
|
|
312
|
+
ret[k.to_sym] = v.to_s.gsub(/^["']|["']$/, '') if v
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# The format string must contain values. They can be single
|
|
317
|
+
# values or distributions. So we must have 'v' xor 'd'. It must
|
|
318
|
+
# not contain anything other than 'm', 't', 'T', 's', 'd', or
|
|
319
|
+
# 'v', and the 'T', if there, must be at the end. No letter must
|
|
320
|
+
# appear more than once.
|
|
321
|
+
#
|
|
322
|
+
# @param fmt [String] format of input file
|
|
323
|
+
#
|
|
324
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
325
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
326
|
+
# rubocop:disable Metrics/AbcSize
|
|
327
|
+
def valid_format?(fmt)
|
|
328
|
+
err = if fmt.include?('v') && fmt.include?('d')
|
|
329
|
+
"'v' and 'd' are mutually exclusive"
|
|
330
|
+
elsif !fmt.include?('v') && !fmt.include?('d')
|
|
331
|
+
"format string must include 'v' or 'd'"
|
|
332
|
+
elsif !fmt.match(/^[dmstTv]+$/)
|
|
333
|
+
'unsupported field in format string'
|
|
334
|
+
elsif fmt != fmt.squeeze
|
|
335
|
+
'repeated field in format string'
|
|
336
|
+
elsif fmt.include?('T') && !fmt.end_with?('T')
|
|
337
|
+
"if used, 'T' must come at end of format string"
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
return true if err.nil?
|
|
341
|
+
|
|
342
|
+
raise(WavefrontCli::Exception::UnparseableInput, err)
|
|
343
|
+
end
|
|
344
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
345
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
346
|
+
# rubocop:enable Metrics/AbcSize
|
|
347
|
+
|
|
348
|
+
# Make sure we have the right number of columns, according to
|
|
349
|
+
# the format string. We want to take every precaution we can to
|
|
350
|
+
# stop users accidentally polluting their metric namespace with
|
|
351
|
+
# junk.
|
|
352
|
+
#
|
|
353
|
+
# @param line [String] input line
|
|
354
|
+
# @return [True] if the number of fields is correct
|
|
355
|
+
# @raise WavefrontCli::Exception::UnparseableInput if there
|
|
356
|
+
# are not the right number of fields.
|
|
357
|
+
#
|
|
358
|
+
def enough_fields?(line)
|
|
359
|
+
ncols = line.split(SPLIT_PATTERN).length
|
|
360
|
+
return true if fmt.include?('T') && ncols >= fmt.length
|
|
361
|
+
return true if ncols == fmt.length
|
|
362
|
+
raise(WavefrontCli::Exception::UnparseableInput,
|
|
363
|
+
format('Expected %s fields, got %s', fmt.length, ncols))
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Although the SDK does value checking, we'll add another layer
|
|
367
|
+
# of input checking here. See if the time looks valid. We'll
|
|
368
|
+
# assume anything before 2000/01/01 or after a year from now is
|
|
369
|
+
# wrong. Arbitrary, but there has to be a cut-off somewhere.
|
|
370
|
+
# @param timestamp [String, Integer] epoch timestamp
|
|
371
|
+
# @return [Bool]
|
|
372
|
+
#
|
|
373
|
+
def valid_timestamp?(timestamp)
|
|
374
|
+
(timestamp.is_a?(Integer) ||
|
|
375
|
+
timestamp.is_a?(String) && timestamp.match(/^\d+$/)) &&
|
|
376
|
+
timestamp.to_i > 946_684_800 &&
|
|
377
|
+
timestamp.to_i < (Time.now.to_i + 31_557_600)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def setup_fmt(fmt)
|
|
381
|
+
@fmt = fmt.split('')
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def load_data(file)
|
|
385
|
+
IO.read(file)
|
|
386
|
+
rescue StandardError
|
|
387
|
+
raise WavefrontCli::Exception::FileNotFound
|
|
388
|
+
end
|
|
89
389
|
end
|
|
90
390
|
end
|