wavefront-cli 2.18.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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