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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +7 -0
  4. data/.travis.yml +4 -5
  5. data/HISTORY.md +20 -1
  6. data/README.md +79 -29
  7. data/lib/wavefront-cli/base.rb +26 -2
  8. data/lib/wavefront-cli/commands/alert.rb +10 -10
  9. data/lib/wavefront-cli/commands/base.rb +15 -2
  10. data/lib/wavefront-cli/commands/cloudintegration.rb +3 -3
  11. data/lib/wavefront-cli/commands/config.rb +2 -1
  12. data/lib/wavefront-cli/commands/dashboard.rb +8 -6
  13. data/lib/wavefront-cli/commands/derivedmetric.rb +5 -5
  14. data/lib/wavefront-cli/commands/event.rb +3 -3
  15. data/lib/wavefront-cli/commands/integration.rb +5 -5
  16. data/lib/wavefront-cli/commands/link.rb +3 -3
  17. data/lib/wavefront-cli/commands/message.rb +2 -2
  18. data/lib/wavefront-cli/commands/metric.rb +1 -1
  19. data/lib/wavefront-cli/commands/notificant.rb +3 -3
  20. data/lib/wavefront-cli/commands/proxy.rb +3 -3
  21. data/lib/wavefront-cli/commands/query.rb +3 -3
  22. data/lib/wavefront-cli/commands/savedsearch.rb +3 -3
  23. data/lib/wavefront-cli/commands/settings.rb +22 -0
  24. data/lib/wavefront-cli/commands/source.rb +3 -3
  25. data/lib/wavefront-cli/commands/user.rb +4 -4
  26. data/lib/wavefront-cli/commands/usergroup.rb +5 -8
  27. data/lib/wavefront-cli/commands/webhook.rb +3 -3
  28. data/lib/wavefront-cli/commands/window.rb +3 -3
  29. data/lib/wavefront-cli/commands/write.rb +6 -4
  30. data/lib/wavefront-cli/config.rb +14 -0
  31. data/lib/wavefront-cli/controller.rb +2 -0
  32. data/lib/wavefront-cli/dashboard.rb +133 -14
  33. data/lib/wavefront-cli/display/base.rb +28 -7
  34. data/lib/wavefront-cli/display/dashboard.rb +32 -8
  35. data/lib/wavefront-cli/display/distribution.rb +4 -1
  36. data/lib/wavefront-cli/display/printer/long.rb +149 -140
  37. data/lib/wavefront-cli/display/printer/terse.rb +19 -42
  38. data/lib/wavefront-cli/display/query.rb +6 -0
  39. data/lib/wavefront-cli/display/settings.rb +21 -0
  40. data/lib/wavefront-cli/display/write.rb +12 -5
  41. data/lib/wavefront-cli/event.rb +1 -1
  42. data/lib/wavefront-cli/exception.rb +1 -0
  43. data/lib/wavefront-cli/settings.rb +37 -0
  44. data/lib/wavefront-cli/stdlib/array.rb +20 -0
  45. data/lib/wavefront-cli/stdlib/string.rb +8 -0
  46. data/lib/wavefront-cli/user.rb +1 -1
  47. data/lib/wavefront-cli/version.rb +1 -1
  48. data/lib/wavefront-cli/write.rb +310 -10
  49. data/spec/.rubocop.yml +3 -0
  50. data/spec/spec_helper.rb +81 -1
  51. data/spec/wavefront-cli/alert_spec.rb +1 -0
  52. data/spec/wavefront-cli/cloudintegration_spec.rb +1 -0
  53. data/spec/wavefront-cli/dashboard_spec.rb +47 -4
  54. data/spec/wavefront-cli/derivedmetric_spec.rb +1 -0
  55. data/spec/wavefront-cli/display/base_spec.rb +16 -9
  56. data/spec/wavefront-cli/display/printer/long_spec.rb +75 -106
  57. data/spec/wavefront-cli/display/printer/terse_spec.rb +33 -21
  58. data/spec/wavefront-cli/event_spec.rb +1 -0
  59. data/spec/wavefront-cli/externallink_spec.rb +1 -0
  60. data/spec/wavefront-cli/integration_spec.rb +1 -0
  61. data/spec/wavefront-cli/maintenancewindow_spec.rb +1 -0
  62. data/spec/wavefront-cli/proxy_spec.rb +2 -0
  63. data/spec/wavefront-cli/query_spec.rb +9 -0
  64. data/spec/wavefront-cli/resources/display/alert-human-long +24 -0
  65. data/spec/wavefront-cli/resources/display/alert-input.json +37 -0
  66. data/spec/wavefront-cli/resources/display/alerts-human-terse +9 -0
  67. data/spec/wavefront-cli/resources/display/alerts-input.json +1 -0
  68. data/spec/wavefront-cli/resources/display/user-human-long +19 -0
  69. data/spec/wavefront-cli/resources/display/user-human-long-no_sep +18 -0
  70. data/spec/wavefront-cli/resources/display/user-input.json +1 -0
  71. data/spec/wavefront-cli/resources/responses/README.md +2 -0
  72. data/spec/wavefront-cli/resources/responses/alert-list.json +1 -0
  73. data/spec/wavefront-cli/resources/responses/backups.json +1 -0
  74. data/spec/wavefront-cli/resources/responses/cloudintegration-list.json +1 -0
  75. data/spec/wavefront-cli/resources/responses/dashboard-list.json +1 -0
  76. data/spec/wavefront-cli/resources/responses/derivedmetric-list.json +1 -0
  77. data/spec/wavefront-cli/resources/responses/event-list.json +1 -0
  78. data/spec/wavefront-cli/resources/responses/integration-list.json +1 -0
  79. data/spec/wavefront-cli/resources/responses/link-list.json +1 -0
  80. data/spec/wavefront-cli/resources/responses/notificant-list.json +1 -0
  81. data/spec/wavefront-cli/resources/responses/proxy-list.json +1 -0
  82. data/spec/wavefront-cli/resources/responses/query-cpu.json +1 -0
  83. data/spec/wavefront-cli/resources/responses/savedsearch-list.json +1 -0
  84. data/spec/wavefront-cli/resources/responses/user-list.json +1 -0
  85. data/spec/wavefront-cli/resources/responses/usergroup-list.json +1 -0
  86. data/spec/wavefront-cli/resources/responses/webhook-list.json +1 -0
  87. data/spec/wavefront-cli/resources/responses/window-list.json +1 -0
  88. data/spec/wavefront-cli/savedsearch_spec.rb +1 -0
  89. data/spec/wavefront-cli/settings_spec.rb +19 -0
  90. data/spec/wavefront-cli/stdlib/array_spec.rb +20 -0
  91. data/spec/wavefront-cli/stdlib/string_spec.rb +13 -1
  92. data/spec/wavefront-cli/user_spec.rb +1 -0
  93. data/spec/wavefront-cli/usergroup_spec.rb +1 -0
  94. data/spec/wavefront-cli/webhook_spec.rb +1 -0
  95. data/spec/wavefront-cli/write_spec.rb +167 -0
  96. data/wavefront-cli.gemspec +3 -4
  97. metadata +65 -31
  98. data/.gitlab-ci.yml +0 -16
  99. data/lib/wavefront-cli/base_write.rb +0 -298
  100. data/lib/wavefront-cli/commands/report.rb +0 -37
  101. data/lib/wavefront-cli/display/printer/base.rb +0 -24
  102. data/lib/wavefront-cli/display/report.rb +0 -17
  103. data/lib/wavefront-cli/report.rb +0 -18
  104. data/spec/wavefront-cli/display/printer/base_spec.rb +0 -20
@@ -1,57 +1,34 @@
1
- require_relative 'base'
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 < Base
8
- attr_reader :data, :keys, :fmt_string
7
+ class Terse
8
+ attr_reader :data, :fmt
9
9
 
10
- def initialize(data, *keys)
11
- @data = data
12
- @keys = keys
13
- @fmt_string = format_string.rstrip
14
- @out = prep_output
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
- # @return [String] a Ruby format string for each line
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
- # Find the length of the longest value for each member of @keys,
26
- # in @data.
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
- # Print multiple column output. This method does no word
45
- # wrapping.
46
- #
47
- # @param keys [Symbol] the keys you want in the output. They
48
- # will be printed in the order given.
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
- # rubocop:disable Metrics/AbcSize
9
+ attr_reader :not_sent
10
+
8
11
  def do_point
9
- report unless options[:quiet] || (data[:unsent] + data[:rejected] > 0)
10
- exit(data.rejected.zero? && data.unsent.zero? ? 0 : 1)
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
- %i[sent rejected unsent].each do |k|
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
@@ -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 && ev_file.exist? && res.status.code == 200
71
+ ev_file.unlink if ev_file&.exist? && res.status.code == 200
72
72
  res
73
73
  end
74
74
  # rubocop:enable Metrics/AbcSize
@@ -17,5 +17,6 @@ module WavefrontCli
17
17
  class UnsupportedNoop < RuntimeError; end
18
18
  class UnsupportedOperation < RuntimeError; end
19
19
  class UnsupportedOutput < RuntimeError; end
20
+ class UserGroupNotFound < RuntimeError; end
20
21
  end
21
22
  end
@@ -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
@@ -65,7 +65,7 @@ module WavefrontCli
65
65
  # ourselves.
66
66
  #
67
67
  def extra_validation
68
- options[:'<user>'].each { |u| validate_user(u) }
68
+ options[:'<user>']&.each { |u| validate_user(u) }
69
69
  end
70
70
 
71
71
  def validate_user(user)
@@ -1 +1 @@
1
- WF_CLI_VERSION = '2.18.0'.freeze
1
+ WF_CLI_VERSION = '3.0.0'.freeze
@@ -1,4 +1,5 @@
1
- require_relative 'base_write'
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 < BaseWrite
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] && options[:infileformat].include?('d')
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
- unless options[:metric] || (options.key?(:infileformat) &&
76
- options[:infileformat].include?('m'))
77
- raise(WavefrontCli::Exception::InsufficientData,
78
- "Supply a metric path in the file or with '-m'.")
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