MuranoCLI 3.0.8 → 3.1.0.beta.1

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.
@@ -15,248 +15,643 @@ require 'MrMurano/Logs'
15
15
  require 'MrMurano/ReCommander'
16
16
  require 'MrMurano/Solution'
17
17
 
18
- # FIXME: (landonb): MUR-3081: Remove old http code for v3.1.0. Search: LOGS_USE_HTTP.
19
- LOGS_USE_HTTP = true
20
-
21
- def command_logs(c)
22
- cmd_add_logs_meta(c)
23
- # Add global solution flag: --type [application|product].
24
- cmd_add_solntype_pickers(c, exclude_all: true)
25
- cmd_add_logs_options(c)
26
- c.action do |args, options|
27
- c.verify_arg_count!(args)
28
- logs_action(options)
18
+ # Because Ruby 2.0 does not support quoted keys, e.g., { '$eq': 'value' }.
19
+ # rubocop:disable Style/HashSyntax
20
+
21
+ class LogsCmd
22
+ include MrMurano::Verbose
23
+
24
+ LOG_EMITTER_TYPES = %i[
25
+ script
26
+ call
27
+ event
28
+ config
29
+ service
30
+ ].freeze
31
+
32
+ LOG_SEVERITIES = %i[
33
+ emergency
34
+ alert
35
+ critical
36
+ error
37
+ warning
38
+ notice
39
+ informational
40
+ debug
41
+ ].freeze
42
+
43
+ # (lb): Ideally, we'd use +/- and not +/:, but rb-commander (or is it
44
+ # OptionParser?) double-parses things that look like switches. E.g.,
45
+ # `murano logs --types -call` would set options.types to ["-call"]
46
+ # but would also set options.config to "all". Just one more reason
47
+ # I do not think rb-commander should call itself a "complete solution".
48
+ # (Note also we cannot use '!' instead of '-', because Bash.)
49
+ # Another option would be to use the "no-" option, e.g., "--[no-]types",
50
+ # but then what do you do with the sindle character '-T' option?
51
+ EXCLUDE_INDICATOR = ':'
52
+ INCLUDE_INDICATOR = '+'
53
+
54
+ def initialize
55
+ @filter_severity = []
56
+ @filter_types = []
57
+ @filter_events = []
58
+ @filter_endpoints = []
29
59
  end
30
- end
31
60
 
32
- def cmd_add_logs_meta(c)
33
- c.syntax = %(murano logs [--options])
34
- c.summary = %(Get the logs for a solution)
35
- if LOGS_USE_HTTP
36
- c.description = %(Get the logs for a solution.)
37
- else
38
- cmd_add_logs_help(c)
61
+ def command_logs(cmd)
62
+ cmd_add_logs_meta(cmd)
63
+ # Add global solution flag: --type [application|product].
64
+ cmd_add_solntype_pickers(cmd, exclude_all: true)
65
+ cmd_add_logs_options(cmd)
66
+ cmd_add_format_options(cmd)
67
+ cmd_add_filter_options(cmd)
68
+ cmd.action do |args, options|
69
+ @options = options
70
+ cmd.verify_arg_count!(args)
71
+ logs_action
72
+ end
39
73
  end
40
- end
41
74
 
42
- def cmd_add_logs_help(c)
43
- c.description = %(
44
- Get the logs for a solution.
45
-
46
- Each log record contains a number of fields, including the following.
47
-
48
- Severity
49
- ================================================================
50
- The severity of the log message, as defined by rsyslog standard.
51
-
52
- ID | Name | Description
53
- -- | ------------- | -----------------------------------------
54
- 0 | Emergency | System is unusable
55
- 1 | Alert | Action must be taken immediately
56
- 2 | Critical | Critical conditions
57
- 3 | Error | Error conditions
58
- 4 | Warning | Warning conditions
59
- 5 | Notice | Normal but significant condition
60
- 6 | Informational | Informational messages
61
- 7 | Debug | Debug-level messages
62
-
63
- Type
64
- ================================================================
65
- The type (emitter system) of the message.
66
-
67
- Name | Description
68
- ------- | ----------------------------------------------------
69
- Script | Pegasus-Engine: on User Lua “print()” function call
70
- Call | Dispatcher: On service calls from Lua.
71
- Event | Dispatcher: On event trigger from services
72
- Config | Pegasus-API: On solution configuration change or
73
- | used service deprecation warning.
74
- Service | Services generated & transmitted to Dispatcher.
75
-
76
- Message
77
- ================================================================
78
- Message can be up to 64kb containing plain text describing a log
79
- of the event
80
-
81
- Service
82
- ================================================================
83
- The service via which the event name is coming or the service of
84
- which the function is called.
85
-
86
- Event
87
- ================================================================
88
- Depending on the type:
89
-
90
- Event, Script => Event name
91
- Call => operationId
92
-
93
- Tracking ID
94
- ================================================================
95
- End to end Murano processing id.
96
- Used to group logs together for one endpoint request.
97
- ).strip
98
- end
75
+ def cmd_add_logs_meta(cmd)
76
+ cmd.syntax = %(murano logs [--options])
77
+ cmd.summary = %(Get the logs for a solution)
78
+ cmd_add_help(cmd)
79
+ cmd_add_examples(cmd)
80
+ end
99
81
 
100
- def cmd_add_logs_options(c)
101
- c.option '-f', '--follow', %(Follow logs from server)
102
- c.option '-r', '--retry', %(Always retry the connection) unless LOGS_USE_HTTP
103
- c.option '--[no-]localtime', %(Adjust Timestamps to be in local time)
104
- c.option '--[no-]pretty', %(Reformat JSON blobs in logs.)
105
- c.option '--raw', %(Don't do any formating of the log data)
106
- return if LOGS_USE_HTTP
107
- c.option '--tracking', %(Include start of the Murano Tracking ID)
108
- c.option '--tracking-full', %(Include the full Murano Tracking ID)
109
- c.option '--http', %(Use HTTP connection [deprecated; will be removed])
110
- end
82
+ def cmd_add_help(cmd)
83
+ cmd.description = %(
84
+ Get the logs for a solution.
85
+
86
+ Each log record contains a number of fields, including the following.
87
+
88
+ Severity
89
+ ================================================================
90
+ The severity of the log message, as defined by rsyslog standard.
91
+
92
+ ID | Name | Description
93
+ -- | ------------- | -----------------------------------------
94
+ 0 | Emergency | System is unusable
95
+ 1 | Alert | Action must be taken immediately
96
+ 2 | Critical | Critical conditions
97
+ 3 | Error | Error conditions
98
+ 4 | Warning | Warning conditions
99
+ 5 | Notice | Normal but significant condition
100
+ 6 | Informational | Informational messages
101
+ 7 | Debug | Debug-level messages
102
+
103
+ Type
104
+ ================================================================
105
+ The type (emitter system) of the message.
106
+
107
+ Name | Description
108
+ ------- | ----------------------------------------------------
109
+ Script | Script Engine: When User Lua script calls `print()`
110
+ Call | Dispatcher: On service calls from Lua
111
+ Event | Dispatcher: On event trigger from services
112
+ Config | API: On solution configuration change, or
113
+ | used service deprecation warning
114
+ Service | Services generated & transmitted to Dispatcher
115
+
116
+
117
+ Message
118
+ ================================================================
119
+ Message can be up to 64kb containing plain text describing a log
120
+ of the event.
121
+
122
+ Service
123
+ ================================================================
124
+ The service via which the event name is coming or the service of
125
+ which the function is called.
126
+
127
+ Event
128
+ ================================================================
129
+ Depending on the type:
130
+
131
+ Event, Script => Event name
132
+ Call => operationId
133
+
134
+ Tracking ID
135
+ ================================================================
136
+ End to end Murano processing id.
137
+ Used to group logs together for one endpoint request.
138
+ ).strip
139
+ end
111
140
 
112
- def logs_action(options)
113
- cmd_default_logs_options(options)
114
- cmd_defaults_solntype_pickers(options, :application)
115
- sol = cmd_get_sol!(options)
116
- logs_display(sol, options)
117
- end
141
+ # NOTE (landonb): The Service & Script Debug Log RFC mentions 'subject',
142
+ # but I've never seen it in any debug log reply.
143
+ # The Subject line can be used for a short summary.
144
+ # It should be shorter than 80 characters.
118
145
 
119
- def cmd_default_logs_options(options)
120
- options.default(
121
- follow: false,
122
- retry: false,
123
- pretty: true,
124
- localtime: true,
125
- raw: false,
126
- type: :application,
127
- )
128
- end
146
+ def cmd_add_examples(cmd)
147
+ cmd.example %(
148
+ View the last 100 product log entries
149
+ ).strip, 'murano logs'
129
150
 
130
- def cmd_get_sol!(options)
131
- if options.type == :application
132
- MrMurano::Application.new
133
- elsif options.type == :product
134
- MrMurano::Product.new
135
- else
136
- MrMurano::Verbose.error "Unknown --type specified: #{options.type}"
137
- exit 1
151
+ cmd.example %(
152
+ Stream the application logs, including the last 100 records
153
+ ).strip, 'murano logs --follow'
154
+
155
+ cmd.example %(
156
+ Stream the logs generated by 'device2' events
157
+ ).strip, 'murano logs --follow --event device2'
158
+
159
+ cmd.example %(
160
+ Stream the logs generated by the types, 'event' and 'script'
161
+ ).strip, 'murano logs --follow --types event,script'
162
+
163
+ cmd.example %(
164
+ Stream the logs generated by the types, 'call' and 'config'
165
+ ).strip, 'murano logs --follow --types call -T config'
166
+
167
+ cmd.example %(
168
+ Exclude the logs generated by the 'script' type
169
+ ).strip, 'murano logs --follow -T :script'
170
+
171
+ cmd.example %(
172
+ Show last 100 logs with any severity level expect DEBUG
173
+ ).strip, 'murano logs --severity 0-6'
174
+
175
+ cmd.example %(
176
+ Stream only the logs with the DEBUG severity level
177
+ ).strip, 'murano logs --follow -V --severity deB'
178
+
179
+ cmd.example %(
180
+ Stream logs with the severity levels ALERT, CRITICAL, WARNING, and DEBUG
181
+ ).strip, 'murano logs --follow -V -l 1-2,WARN,7'
138
182
  end
139
- end
140
183
 
141
- def logs_display(sol, options)
142
- if !options.follow
143
- logs_once(sol, options)
144
- else
145
- logs_follow(sol, options)
184
+ def cmd_add_logs_options(cmd)
185
+ cmd.option '-f', '--[no-]follow', %(Follow logs from server)
186
+ cmd.option '-r', '--retry', %(Always retry the connection)
187
+ cmd.option(
188
+ '-i', '--[no-]insensitive',
189
+ %(Use case-insensitive matching (default: true))
190
+ )
191
+ cmd.option '-N', '--limit LIMIT', Integer, %(Retrieve this many existing logs at start of command (only works with --no-follow))
146
192
  end
147
- end
148
193
 
149
- def logs_once(sol, options)
150
- ret = sol.get('/logs')
151
- if ret.is_a?(Hash) && ret.key?(:items)
152
- ret[:items].reverse.each do |line|
153
- if options.raw
154
- puts line
155
- else
156
- puts MrMurano::Pretties.MakePrettyLogsV1(line, options)
194
+ def cmd_add_format_options(cmd)
195
+ cmd_add_format_options_localtime(cmd)
196
+ cmd_add_format_options_pretty(cmd)
197
+ cmd_add_format_options_raw(cmd)
198
+ cmd_add_format_options_message_only(cmd)
199
+ cmd_add_format_options_one_line(cmd)
200
+ cmd_add_format_options_align_columns(cmd)
201
+ cmd_add_format_options_indent_body(cmd)
202
+ cmd_add_format_options_separators(cmd)
203
+ cmd_add_format_options_include_tracking(cmd)
204
+ cmd_add_format_options_sprintf(cmd)
205
+ end
206
+
207
+ def cmd_add_format_options_localtime(cmd)
208
+ cmd.option '--[no-]localtime', %(Adjust Timestamps to be in local time)
209
+ end
210
+
211
+ def cmd_add_format_options_pretty(cmd)
212
+ cmd.option '--[no-]pretty', %(Reformat JSON blobs in logs)
213
+ end
214
+
215
+ def cmd_add_format_options_raw(cmd)
216
+ cmd.option '--raw', %(Do not format the log data)
217
+ end
218
+
219
+ def cmd_add_format_options_message_only(cmd)
220
+ cmd.option '-o', '--message-only', %(Show only headers and the 'print' message)
221
+ end
222
+
223
+ def cmd_add_format_options_one_line(cmd)
224
+ cmd.option '--one-line', %(Squeeze each log entry onto one line (wrapping as necessary))
225
+ end
226
+
227
+ def cmd_add_format_options_align_columns(cmd)
228
+ cmd.option '--[no-]align', %(Align columns in formatted output)
229
+ end
230
+
231
+ def cmd_add_format_options_indent_body(cmd)
232
+ cmd.option '--[no-]indent', %(Indent body content in formatted output)
233
+ end
234
+
235
+ def cmd_add_format_options_separators(cmd)
236
+ cmd.option '--[no-]separators', %(Indent body content in formatted output)
237
+ end
238
+
239
+ def cmd_add_format_options_include_tracking(cmd)
240
+ cmd.option '--[no-]tracking', %(Include tracking ID)
241
+ end
242
+
243
+ def cmd_add_format_options_sprintf(cmd)
244
+ cmd.option '--sprintf FORMAT', %(Specify timestamp format (default: '%Y-%m-%d %H:%M:%S'))
245
+ end
246
+
247
+ def cmd_add_filter_options(cmd)
248
+ # Common log fields.
249
+ cmd_add_filter_option_severity(cmd)
250
+ cmd_add_filter_option_type(cmd)
251
+ cmd_add_filter_option_message(cmd)
252
+ cmd_add_filter_option_service(cmd)
253
+ cmd_add_filter_option_event(cmd)
254
+ # Skipping: timestamp filter
255
+ # Skipping: tracking_id filter
256
+ # Type-specific fields in data.
257
+ cmd_add_filter_option_endpoint(cmd)
258
+ # Skipping: module filter
259
+ # Skipping: elapsed time filter (i.e., could do { elapsed: { $gt: 10 } })
260
+ end
261
+
262
+ def cmd_add_filter_option_severity(cmd)
263
+ cmd.option(
264
+ '-l', '--severity [NAME|LEVEL|RANGE[,NAME|LEVEL|RANGE...]]', Array,
265
+ %(
266
+ Only show log entries of this severity.
267
+ May be specified by name, value, or range, e.g., WARN, 3, 1-4.
268
+ #{LOG_SEVERITIES.map.with_index { |s, i| "#{s}(#{i})" }.join(' ')}
269
+ ).strip
270
+ ) do |value|
271
+ @filter_severity.push value
272
+ end
273
+ end
274
+
275
+ def cmd_add_filter_option_type(cmd)
276
+ emitter_type_help = %(
277
+ Filter log entries by type (emitter system) of message.
278
+ EMITTERS is 1 or more comma-separated types:
279
+ #{LOG_EMITTER_TYPES.map(&:to_s)}
280
+ Use a "#{INCLUDE_INDICATOR}" or "#{EXCLUDE_INDICATOR}" prefix to include or exclude types, respectively.
281
+ ).strip
282
+ cmd.option('-T EMITTERS', '--types EMITTERS', Array, emitter_type_help) do |values|
283
+ # This seems a little roundabout, but rb-commander only keeps last value.
284
+ @filter_types.push values
285
+ values.map do |val|
286
+ val.sub(/^[#{INCLUDE_INDICATOR}#{EXCLUDE_INDICATOR}]/, '')
157
287
  end
158
288
  end
159
- else
160
- sol.error "Could not get logs: #{ret}"
289
+ end
290
+
291
+ def cmd_add_filter_option_message(cmd)
292
+ cmd.option '-m', '--message GLOB', %(
293
+ Filter log entries by the message contents
294
+ ).strip
295
+ end
296
+
297
+ def cmd_add_filter_option_service(cmd)
298
+ cmd.option '-s', '--service GLOB', %(
299
+ Filter log entries by the originating service
300
+ ).strip
301
+ end
302
+
303
+ def cmd_add_filter_option_event(cmd)
304
+ cmd.option(
305
+ '-E', '--event GLOB', Array,
306
+ %(Filter log entries by the event)
307
+ ) do |value|
308
+ @filter_events.push value
309
+ end
310
+ end
311
+
312
+ def cmd_add_filter_option_endpoint(cmd)
313
+ cmd.option(
314
+ '-e', '--endpoint ENDPOINT',
315
+ %(Filter log entries by the endpoint (ENDPOINT is VERB:PATH))
316
+ ) do |value|
317
+ @filter_endpoints.push value
318
+ end
319
+ end
320
+
321
+ def logs_action
322
+ cmd_default_logs_options
323
+ cmd_verify_logs_options!
324
+ cmd_defaults_solntype_pickers(@options, :application)
325
+ @query = assemble_query
326
+ verbose %(query: #{@query})
327
+ sol = cmd_get_sol!
328
+ logs_display(sol)
329
+ end
330
+
331
+ def cmd_default_logs_options
332
+ @options.default(
333
+ type: :application,
334
+ follow: false,
335
+ retry: false,
336
+ insensitive: true,
337
+ limit: nil,
338
+ localtime: true,
339
+ pretty: true,
340
+ raw: false,
341
+ message_only: false,
342
+ one_line: false,
343
+ tracking: false,
344
+ sprintf: '%Y-%m-%d %H:%M:%S',
345
+ align: false,
346
+ indent: false,
347
+ separators: false,
348
+ severity: nil,
349
+ types: [],
350
+ message: nil,
351
+ service: nil,
352
+ event: nil,
353
+ endpoint: nil,
354
+ )
355
+ end
356
+
357
+ def cmd_verify_logs_options!
358
+ n_formatting = 0
359
+ n_formatting += 1 if @options.raw
360
+ n_formatting += 1 if @options.message_only
361
+ n_formatting += 1 if @options.one_line
362
+ # Global options should really be checked elsewhere. Oh, well.
363
+ n_formatting += 1 if @options.json
364
+ n_formatting += 1 if @options.yaml
365
+ n_formatting += 1 if @options.pp
366
+ return unless n_formatting > 1
367
+ format_options = '--raw, --message-only, --one-line, --json, --yaml, or --pp'
368
+ warn "Try using just one of #{format_options}, but not two or more."
161
369
  exit 1
162
370
  end
163
- end
164
371
 
165
- def logs_follow(sol, options)
166
- if !LOGS_USE_HTTP && !options.http
167
- logs_follow_wss(sol, options)
168
- else
169
- logs_follow_http(sol, options)
372
+ def cmd_get_sol!
373
+ if @options.type == :application
374
+ MrMurano::Application.new
375
+ elsif @options.type == :product
376
+ MrMurano::Product.new
377
+ else
378
+ error "Unknown --type specified: #{@options.type}"
379
+ exit 1
380
+ end
381
+ end
382
+
383
+ def assemble_query
384
+ query_parts = {}
385
+ assemble_query_severity(query_parts)
386
+ assemble_query_types_array(query_parts)
387
+ assemble_query_message(query_parts)
388
+ assemble_query_service(query_parts)
389
+ assemble_query_event(query_parts)
390
+ assemble_query_endpoint(query_parts)
391
+ # Assemble and return actual query string.
392
+ assemble_query_string(query_parts)
170
393
  end
171
- end
172
394
 
173
- # FIXME: (landonb): MUR-3081: Remove old http code for v3.1.0. Search: LOGS_USE_HTTP.
174
- def logs_follow_http(sol, options)
175
- # Open a lasting connection and continually feed MakePrettyLogsV1().
176
- sol.get('/logs?polling=true') do |request, http|
177
- request['Accept-Encoding'] = 'None'
178
- http.request(request) do |response|
179
- remainder = ''
180
- response.read_body do |chunk|
181
- chunk = remainder + chunk unless remainder.empty?
182
-
183
- # For all complete JSON blobs, make them pretty.
184
- chunk.gsub!(/\{(?>[^}{]+|\g<0>)*\}/m) do |m|
185
- if options.raw
186
- puts m
395
+ def assemble_query_severity(query_parts)
396
+ filter_severity = @filter_severity.flatten
397
+ return if filter_severity.empty?
398
+ indices = []
399
+ filter_severity.each do |sev|
400
+ index = sev if sev =~ /^[0-9]$/
401
+ index = LOG_SEVERITIES.find_index { |s| s.to_s =~ /^#{sev.downcase}/ } unless index
402
+ if index
403
+ indices.push index.to_i
404
+ else
405
+ parts = /^([0-9])-([0-9])$/.match(sev)
406
+ if !parts.nil?
407
+ start = parts[1].to_i
408
+ finis = parts[2].to_i
409
+ if start < finis
410
+ more_indices = (start..finis).to_a
187
411
  else
188
- begin
189
- js = JSON.parse(
190
- m,
191
- allow_nan: true,
192
- symbolize_names: true,
193
- create_additions: false,
194
- )
195
- puts MrMurano::Pretties.MakePrettyLogsV1(js, options)
196
- rescue StandardError
197
- sol.error '=== JSON parse error, showing raw instead ==='
198
- puts m
199
- end
412
+ more_indices = (finis..start).to_a
200
413
  end
201
- '' #remove (we're kinda abusing gsub here.)
414
+ indices += more_indices
415
+ else
416
+ warning "Invalid severity: #{sev}"
417
+ exit 1
418
+ end
419
+ end
420
+ end
421
+ query_parts['severity'] = { '$in': indices }
422
+ end
423
+
424
+ def assemble_query_types_array(query_parts)
425
+ assemble_in_or_nin_query(query_parts, 'type', @filter_types.flatten) do |type|
426
+ index = LOG_EMITTER_TYPES.find_index { |s| s.to_s =~ /^#{type.downcase}/ }
427
+ if index
428
+ LOG_EMITTER_TYPES[index].to_s
429
+ else
430
+ warning "Invalid emitter type: #{type}"
431
+ exit 1
432
+ end
433
+ end
434
+ end
435
+
436
+ def assemble_query_message(query_parts)
437
+ assemble_string_search_one(query_parts, 'message', @options.message, regex: true)
438
+ end
439
+
440
+ def assemble_query_service(query_parts)
441
+ assemble_string_search_one(query_parts, 'service', @options.service)
442
+ end
443
+
444
+ def assemble_query_event(query_parts)
445
+ assemble_string_search_many(query_parts, 'event', @filter_events)
446
+ end
447
+
448
+ def assemble_query_endpoint(query_parts)
449
+ terms = @filter_endpoints.map { |endp| format_endpoint(endp) }
450
+ # FIXME: (lb): MUR-5446: Still unresolved: How to query endpoints.
451
+ # The RFC says to use 'endpoint', but I've also been told to use
452
+ # 'data.section.' Neither work for me.
453
+ assemble_string_search_many(query_parts, 'data.section', terms)
454
+ end
455
+
456
+ def format_endpoint(endp)
457
+ # E.g., ?query={"data.section":"get_/set"}
458
+ # The format the user is most likely to use is with a colon, but
459
+ # we can let the user be a little sloppy, too, e.g., "get /set".
460
+ parts = endp.split(' ', 2)
461
+ parts = endp.split(':', 2) unless parts.length > 1
462
+ parts.join('_').downcase
463
+ end
464
+
465
+ def assemble_string_search_one(query_parts, field, value, regex: false)
466
+ return if value.to_s.empty?
467
+ if !regex
468
+ # Note that some options support strict equality, e.g.,
469
+ # { 'key': { '$eq': 'value' } }
470
+ # but post-processed keys like 'data.section' do not.
471
+ # So we just do normal equality, e.g.,
472
+ # { 'key': 'value' }
473
+ query_parts[field] = value
474
+ else
475
+ query_parts[field] = { :'$regex' => value }
476
+ query_parts[field][:'$options'] = 'i' if @options.insensitive
477
+ end
478
+ end
479
+
480
+ def assemble_string_search_many(query_parts, field, arr_of_arrs)
481
+ terms = arr_of_arrs.flatten
482
+ return if terms.empty?
483
+ if terms.length == 1
484
+ assemble_string_search_one(query_parts, field, terms[0])
485
+ else
486
+ assemble_in_or_nin_query(query_parts, field, terms)
487
+ end
488
+ end
489
+
490
+ def assemble_in_or_nin_query(query_parts, field, terms, &block)
491
+ return if terms.empty?
492
+ exclude = term_indicates_exclude?(terms[0])
493
+ resolved_terms = []
494
+ terms.each do |term|
495
+ process_query_term(term, resolved_terms, exclude, field, terms, &block)
496
+ end
497
+ return if resolved_terms.empty?
498
+ if !exclude
499
+ operator = '$in'
500
+ else
501
+ operator = '$nin'
502
+ end
503
+ query_parts[field] = { "#{operator}": resolved_terms }
504
+ end
505
+
506
+ def term_indicates_exclude?(term)
507
+ if term.start_with? EXCLUDE_INDICATOR
508
+ true
509
+ else
510
+ false
511
+ end
512
+ end
513
+
514
+ def process_query_term(term, resolved_terms, exclude, field, terms)
515
+ verify_term_plus_minux_prefix!(term, exclude, field, terms)
516
+ term = term.sub(/^[#{INCLUDE_INDICATOR}#{EXCLUDE_INDICATOR}]/, '')
517
+ term = yield term if block_given?
518
+ resolved_terms.push term
519
+ end
520
+
521
+ def verify_term_plus_minux_prefix!(term, exclude, field, terms)
522
+ return unless term =~ /^[#{INCLUDE_INDICATOR}#{EXCLUDE_INDICATOR}]/
523
+ return unless (
524
+ (!exclude && term.start_with?(EXCLUDE_INDICATOR)) ||
525
+ (exclude && term.start_with?(INCLUDE_INDICATOR))
526
+ )
527
+ warning(
528
+ %(You cannot mix + and ! for "#{field}": #{terms.join(',')})
529
+ )
530
+ exit 1
531
+ end
532
+
533
+ def assemble_query_string(query_parts)
534
+ if query_parts.empty?
535
+ ''
536
+ else
537
+ # (lb): I tried escaping parts of the query but it broke things.
538
+ # So I'm assuming we don't need CGI.escape or URI.encode_www_form.
539
+ query_parts.to_json
540
+ end
541
+ end
542
+
543
+ def logs_display(sol)
544
+ if !@options.follow
545
+ logs_once(sol)
546
+ else
547
+ logs_follow(sol)
548
+ end
549
+ end
550
+
551
+ def logs_once(sol)
552
+ ret = sol.get("/logs#{query_string}")
553
+ if ret.is_a?(Hash) && ret.key?(:items)
554
+ ret[:items].reverse.each do |line|
555
+ if pretty_printing
556
+ print_pretty(line)
557
+ else
558
+ print_raw(line)
202
559
  end
560
+ end
561
+ else
562
+ sol.error "Could not get logs: #{ret}"
563
+ exit 1
564
+ end
565
+ end
566
+
567
+ def query_string
568
+ # NOTE (lb): Unsure whether we need to encode parts of the query,
569
+ # possibly with CGI.escape. Note that http.get calls
570
+ # URI.encode_www_form(query), which the server will accept,
571
+ # but it will not produce any results.
572
+ params = []
573
+ params += ["query=#{@query}"] unless @query.empty?
574
+ params += ["limit=#{@options.limit}"] unless @options.limit.nil?
575
+ querys = params.join('&')
576
+ querys = "?#{querys}" unless querys.empty?
577
+ querys
578
+ end
203
579
 
204
- # Is there an incomplete one?
205
- chunk.match(/(\{.*$)/m) do |mat|
206
- remainder = mat[1]
580
+ # LATER/2017-12-14 (landonb): Show logs from all associated solutions.
581
+ # We'll have to wire all the WebSockets from within the EM.run block.
582
+ def logs_follow(sol)
583
+ formatter = fetch_formatter
584
+ keep_running = true
585
+ while keep_running
586
+ keep_running = @options.retry
587
+ logs = MrMurano::Logs::Follow.new(@query, @options.limit)
588
+ logs.run_event_loop(sol) do |line|
589
+ log_entry = parse_logs_line(line)
590
+ if log_entry[:statusCode] == 400
591
+ warning "Query error: #{log_entry}"
592
+ else
593
+ formatter.call(log_entry) unless log_entry.nil?
207
594
  end
208
595
  end
209
596
  end
210
597
  end
211
- # rubocop:disable Lint/HandleExceptions: Do not suppress exceptions.
212
- rescue Interrupt => _
213
- end
214
598
 
215
- # LATER/2017-12-14 (landonb): Show logs from all associated solutions.
216
- # We'll have to wire all the WebSockets from within the EM.run block.
217
- def logs_follow_wss(sol, options)
218
- formatter = get_formatter(options)
219
- keep_running = true
220
- while keep_running
221
- keep_running = options.retry
222
- logs = MrMurano::Logs::Follow.new
223
- logs.run_event_loop(sol) do |line|
224
- log_entry = parse_logs_line(line)
225
- formatter.call(log_entry, options) unless log_entry.nil?
599
+ def parse_logs_line(line)
600
+ log_entry = JSON.parse(line)
601
+ elevate_hash(log_entry)
602
+ rescue StandardError => err
603
+ warning "Not JSON: #{err} / #{line}"
604
+ nil
605
+ end
606
+
607
+ def fetch_formatter
608
+ if pretty_printing
609
+ method(:print_pretty)
610
+ else
611
+ method(:print_raw)
226
612
  end
227
613
  end
228
- end
229
614
 
230
- def parse_logs_line(line)
231
- log_entry = JSON.parse(line)
232
- elevate_hash(log_entry)
233
- rescue StandardError => err
234
- MrMurano::Verbose.warning "Not JSON: #{err} / #{line}"
235
- nil
236
- end
615
+ def pretty_printing
616
+ !@options.raw && ($cfg['tool.outformat'] == 'best')
617
+ end
237
618
 
238
- def get_formatter(options)
239
- if options.raw
240
- method(:print_raw)
241
- else
242
- method(:print_pretty)
619
+ def print_raw(line)
620
+ outf line
243
621
  end
244
- end
245
622
 
246
- def print_raw(line, _options={})
247
- puts line
623
+ def print_pretty(line)
624
+ determine_format(line) if @log_format_is_v2.nil?
625
+ if @log_format_is_v2
626
+ puts MrMurano::Pretties.MakePrettyLogsV2(line, @options)
627
+ else
628
+ puts MrMurano::Pretties.MakePrettyLogsV1(line, @options)
629
+ end
630
+ rescue StandardError => err
631
+ error "Failed to parse log: #{err} / #{line}"
632
+ raise
633
+ end
634
+
635
+ def determine_format(line)
636
+ # FIXME/2018-01-05 (landonb): On bizapi-dev, I see new-format logs,
637
+ # but on bizapi-staging, I see old school format. So deal with it.
638
+ # Logs V1 have 4 entries: type, timestamp, subject, data
639
+ # Logs V2 have lots more entries, including type, timestamp and data.
640
+ # We could use presence of subject to distinguish; or we could check
641
+ # timestamp, which is seconds in V1 but msecs in V2; or we could check
642
+ # data, which is string in V1, and Hash in V2.
643
+ @log_format_is_v2 = !(line[:data].is_a? String)
644
+ end
248
645
  end
249
646
 
250
- def print_pretty(line, options={})
251
- puts MrMurano::Pretties.MakePrettyLogsV2(line, options)
252
- rescue StandardError => err
253
- MrMurano::Verbose.error "Failed to parse log: #{err} / #{line}"
254
- raise
647
+ def wire_cmd_logs
648
+ logs_cmd = LogsCmd.new
649
+ command(:logs) { |cmd| logs_cmd.command_logs(cmd) }
650
+ alias_command 'logs application', 'logs', '--type', 'application'
651
+ alias_command 'logs product', 'logs', '--type', 'product'
652
+ alias_command 'application logs', 'logs', '--type', 'application'
653
+ alias_command 'product logs', 'logs', '--type', 'product'
255
654
  end
256
655
 
257
- command :logs, &method(:command_logs)
258
- alias_command 'logs application', 'logs', '--type', 'application'
259
- alias_command 'logs product', 'logs', '--type', 'product'
260
- alias_command 'application logs', 'logs', '--type', 'application'
261
- alias_command 'product logs', 'logs', '--type', 'product'
656
+ wire_cmd_logs
262
657