MuranoCLI 3.0.8 → 3.1.0.beta.1

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