greenhat 0.2.0 → 0.3.3

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/greenhat/accessors/disk.rb +13 -13
  3. data/lib/greenhat/accessors/gitlab.rb +75 -0
  4. data/lib/greenhat/accessors/memory.rb +10 -10
  5. data/lib/greenhat/accessors/process.rb +4 -0
  6. data/lib/greenhat/cli.rb +147 -58
  7. data/lib/greenhat/color.rb +27 -0
  8. data/lib/greenhat/logbot.rb +9 -9
  9. data/lib/greenhat/settings.rb +27 -7
  10. data/lib/greenhat/shell/args.rb +146 -0
  11. data/lib/greenhat/shell/cat.rb +25 -73
  12. data/lib/greenhat/shell/color_string.rb +8 -8
  13. data/lib/greenhat/shell/disk.rb +31 -4
  14. data/lib/greenhat/shell/faststats.rb +103 -56
  15. data/lib/greenhat/shell/field_helper.rb +75 -0
  16. data/lib/greenhat/shell/filter_help.rb +162 -0
  17. data/lib/greenhat/shell/gitlab.rb +61 -2
  18. data/lib/greenhat/shell/help.rb +98 -15
  19. data/lib/greenhat/shell/list.rb +46 -0
  20. data/lib/greenhat/shell/log.rb +118 -104
  21. data/lib/greenhat/shell/page.rb +11 -5
  22. data/lib/greenhat/shell/process.rb +29 -17
  23. data/lib/greenhat/shell/report.rb +37 -47
  24. data/lib/greenhat/shell/shell_helper.rb +661 -0
  25. data/lib/greenhat/shell.rb +23 -9
  26. data/lib/greenhat/thing/file_types.rb +31 -5
  27. data/lib/greenhat/thing/formatters/json_shellwords.rb +0 -3
  28. data/lib/greenhat/thing/formatters/nginx.rb +44 -0
  29. data/lib/greenhat/thing/helpers.rb +4 -4
  30. data/lib/greenhat/thing/kind.rb +9 -2
  31. data/lib/greenhat/thing/spinner.rb +3 -3
  32. data/lib/greenhat/thing.rb +25 -3
  33. data/lib/greenhat/tty/columns.rb +4 -0
  34. data/lib/greenhat/version.rb +1 -1
  35. data/lib/greenhat.rb +15 -14
  36. metadata +38 -18
  37. data/lib/greenhat/shell/filter.rb +0 -128
  38. data/lib/greenhat/shell/helper.rb +0 -584
@@ -1,128 +0,0 @@
1
- module GreenHat
2
- # CLI Helper
3
- module ShellHelper
4
- # Unify Filter / Filter Help
5
- module Filter
6
- # rubocop:disable Metrics/MethodLength
7
- def self.help
8
- puts "\u2500".colorize(:cyan) * 20
9
- puts 'Filter'.colorize(:yellow)
10
- puts "\u2500".colorize(:cyan) * 20
11
-
12
- puts 'Options'.colorize(:blue)
13
- puts '--raw'.colorize(:green)
14
- puts ' Do not use less/paging'
15
- puts
16
-
17
- puts '--page'.colorize(:green)
18
- puts ' Specifically enable or disable paging'
19
- puts ' E.g. --page (default to true), --page=true, --page=false'
20
- puts
21
-
22
- puts '--round'.colorize(:green)
23
- puts ' Attempt to round all integers. Default: 2.'
24
- puts ' E.g. --round, --round=3, --round=0'
25
- puts
26
-
27
- puts '--truncate'.colorize(:green)
28
- puts ' Limit output values. Defaults to 4 lines. 0 to disable'
29
- puts ' E.g. --truncate=100 or --truncate=0'
30
- puts
31
-
32
- puts '--json'.colorize(:green)
33
- puts ' Print output back into JSON'
34
- puts
35
-
36
- puts '--or'.colorize(:green)
37
- puts ' Filters will use OR instead of AND'
38
- puts
39
-
40
- puts '--total'.colorize(:green)
41
- puts ' Print only total count of matching entries'
42
- puts
43
-
44
- puts '--slice'.colorize(:green)
45
- puts ' Extract specific fields from entries (slice multiple with comma)'
46
- puts ' Ex: --slice=path or --slice=path,params'
47
- puts
48
-
49
- puts '--except'.colorize(:green)
50
- puts ' Exclude specific fields (except multiple with comma)'
51
- puts ' Ex: --except=params --except=params,path'
52
- puts
53
-
54
- puts '--stats'.colorize(:green)
55
- puts ' Order/Count occurrances by field'
56
- puts ' Ex: --stats=params --except=params,path'
57
- puts
58
-
59
- puts '--uniq'.colorize(:green)
60
- puts ' Show unique values only'
61
- puts ' Ex: --uniq=params --uniq=params,path'
62
- puts
63
-
64
- puts '--pluck'.colorize(:green)
65
- puts ' Extract values from entries'
66
- puts ' Ex: --pluck=params --pluck=params,path'
67
- puts
68
-
69
- puts '--archive'.colorize(:green)
70
- puts ' Limit to specific archvie name (inclusive). Matching SOS tar.gz name'
71
- puts ' Ex: --archive=dev-gitlab_20210622154626, --archive=202106,202107'
72
- puts
73
-
74
- puts '--sort'.colorize(:green)
75
- puts ' Sort by multiple fields'
76
- puts ' Ex: --sort=duration_s,db_duration_s'
77
- puts
78
-
79
- puts '--reverse'.colorize(:green)
80
- puts ' Reverse all results'
81
- puts ' Ex: --reverse'
82
- puts
83
-
84
- puts '--combine'.colorize(:green)
85
- puts ' Omit archive identifier dividers. Useful with sort or time filters'
86
- puts ' Ex: --combine'
87
- puts
88
-
89
- puts '--start'.colorize(:green)
90
- puts ' Show events after specified time. Filtered by the `time` field'
91
- puts ' Use with `--end` for between selections'
92
- puts ' Ex: log filter --start="2021-06-22 14:44 UTC" --end="2021-06-22 14:45 UTC"'
93
- puts
94
-
95
- puts '--end'.colorize(:green)
96
- puts ' Show events before specified time. Filtered by the `time` field'
97
- puts ' Use with `--start` for between selections'
98
- puts ' Ex: log filter --end="2021-06-22"'
99
- puts
100
-
101
- puts '--truncate'.colorize(:green)
102
- puts ' Truncate field length. On by default (4 rows). Performance issues!'
103
- puts ' Disable with --truncate=0'.colorize(:light_red)
104
- puts ' Ex: --truncate=200, --truncate=2048"'
105
- puts
106
-
107
- puts 'Field Searching'.colorize(:blue)
108
- puts ' --[key]=[value]'
109
- puts ' Search in key for value'
110
- puts ' Example: --path=mirror/pull'
111
- puts
112
-
113
- puts 'Search specific logs'.colorize(:blue)
114
- puts ' Any non dash parameters will be the log list to search from'
115
- puts " Ex: log filter --path=api sidekiq/current (hint: use `#{'ls'.colorize(:yellow)}` for log names"
116
- puts
117
-
118
- puts 'Example Queries'.colorize(:blue)
119
- puts " Also see #{'examples'.colorize(:light_blue)} for even more examples"
120
- puts ' log filter --class=BuildFinishedWorker sidekiq/current --slice=time,message'
121
- puts ' log filter gitlab-rails/api_json.log --slice=ua --uniq=ua --ua=gitlab-runner'
122
-
123
- puts
124
- end
125
- # rubocop:enable Metrics/MethodLength
126
- end
127
- end
128
- end
@@ -1,584 +0,0 @@
1
- module GreenHat
2
- # Common Helpers
3
- # rubocop:disable Metrics/ModuleLength
4
- module ShellHelper
5
- # Generic Parameter Parsing
6
- def self.param_parse(params, skip_args = [])
7
- # Turn Params into Hash
8
- opts = params.flat_map { |param| param_opt_scan(param) }
9
-
10
- # Collect and Remove Args
11
- args = params.each_with_object({}) { |param, obj| param_arg_scan(param, obj) }
12
-
13
- # Move Special Arguments
14
- opts.reject! do |opt|
15
- if param_special_opts.include? opt.field
16
- args[opt.field] = opt.value.split(',').map(&:to_sym)
17
- true
18
- end
19
- end
20
-
21
- # Remove Opts and Args
22
- params.reject! do |param|
23
- opt_field_remove?(opts, param) || arg_field_remove?(args, param)
24
- end
25
-
26
- # Update other user defaults
27
- Settings.default_log_args(args, skip_args)
28
-
29
- [params, opts, args]
30
- end
31
-
32
- def self.opt_field_remove?(opts, param)
33
- opts.any? do |opt|
34
- param.include? "--#{opt.field}#{opt.bang ? '!' : nil}=#{opt.value}"
35
- end
36
- end
37
-
38
- def self.arg_field_remove?(args, param)
39
- args.keys.any? { |field| param.include? "--#{field}" }
40
- end
41
-
42
- # Def Param Scan (Split -- values into keys)
43
- def self.param_opt_scan(param)
44
- param.scan(/--([^=]+)=(.*)/).map do |field, value|
45
- bang = false
46
- if field.include? '!'
47
- field.delete!('!')
48
- bang = true
49
- end
50
-
51
- { field: field.to_sym, value: value, bang: bang }
52
- end
53
- end
54
-
55
- # Params that should be set as Args
56
- # Comma Delimited
57
- # --slice=thing,thing2
58
- # --except=time,params
59
- # --round=2
60
-
61
- def self.param_special_opts
62
- %i[
63
- slice except stats uniq pluck round archive start end sort limit truncate page case
64
- ]
65
- end
66
-
67
- # Parameter Extraction
68
- # Special Opts: --slice, --except
69
- def self.param_arg_scan(param, obj)
70
- # TODO: Why is capture group doing two arrays
71
- param.scan(/--([^=]+)$/).flatten.each do |field|
72
- obj[field.to_sym] = param_arg_defaults(field.to_sym)
73
- end
74
-
75
- obj
76
- end
77
-
78
- # Arg Defaults
79
- def self.param_arg_defaults(field)
80
- case field
81
- when :round then [2]
82
- when :limit then [TTY::Screen.height / 2]
83
- when :truncate then [TTY::Screen.width * 4]
84
- when :page, :case then [:true]
85
- when *param_special_opts then []
86
- else
87
- true
88
- end
89
- end
90
-
91
- # Use File Process for Output
92
- def self.file_output(files)
93
- results = file_process(files) do |file|
94
- [
95
- file.friendly_name,
96
- file.output(false),
97
- "\n"
98
- ]
99
- end
100
-
101
- ShellHelper.show(results.flatten)
102
- end
103
-
104
- def self.file_process(files, &block)
105
- files.map do |file|
106
- next if file.output(false).empty?
107
-
108
- block.call(file)
109
- end.flatten
110
- end
111
-
112
- # Pagination Helper
113
- def self.page(data)
114
- TTY::Pager.page do |pager|
115
- data.flatten.each do |output|
116
- pager.write("\n#{output}") # write line to the pager
117
- end
118
- end
119
- end
120
-
121
- # Show Data / Auto Paginate Helper
122
- def self.show(data, args = {})
123
- # If Block of String
124
- if data.instance_of?(String)
125
- TTY::Pager.page data
126
- return true
127
- end
128
-
129
- # If raw just print out
130
- if args.raw
131
- puts data.join("\n")
132
- return true
133
- end
134
-
135
- # Check if content needs to paged, or if auto_height is off
136
- if Page.skip?(args, data)
137
- puts data.map { |entry| entry_show(args, entry) }.compact.join("\n")
138
- return true
139
- end
140
-
141
- # Default Pager
142
- TTY::Pager.page do |pager|
143
- data.each do |entry|
144
- output = entry_show(args, entry)
145
-
146
- next if output.blank?
147
-
148
- pager.write("\n#{output}") # write line to the pager
149
- end
150
- end
151
- end
152
-
153
- # Entry Shower / Top Level
154
- def self.entry_show(args, entry, key = nil)
155
- LogBot.debug('Entry Show', entry.class) if ENV['DEBUG']
156
- case entry
157
- when Hash then render_table(entry, args)
158
- when Float, Integer, Array
159
- format_table_entry(args, entry, key)
160
- # Ignore Special Formatting for Strings / Usually already formatted
161
- when String
162
- entry
163
- else
164
- LogBot.warn('Shell Show', "Unknown #{entry.class}")
165
- nil
166
- end
167
- end
168
-
169
- # Format Table Entries
170
- def self.format_table_entry(args, entry, key = nil)
171
- formatted_entry = case entry
172
- # Rounding
173
- when Float, Integer || entry.numeric?
174
- args.round ? entry.to_f.round(args.round.first.to_s.to_i).ai : entry.ai
175
-
176
- # General Inspecting
177
- when Hash then entry.ai(ruby19_syntax: true)
178
-
179
- # Arrays often contain Hashes. Dangerous Recursive?
180
- when Array
181
- entry.map { |x| format_table_entry(args, x) }.join("\n")
182
-
183
- when Time
184
- entry.to_s.colorize(:light_white)
185
-
186
- # Default String Formatting
187
- else
188
- StringColor.do(key, entry)
189
- end
190
-
191
- if args[:truncate]
192
- entry_truncate(formatted_entry, args[:truncate].join.to_i)
193
- else
194
- formatted_entry
195
- end
196
- rescue StandardError => e
197
- binding.pry
198
- end
199
-
200
- # Print the Table in a Nice way
201
- def self.render_table(entry, args)
202
- entry = entry.map { |k, v| [k, format_table_entry(args, v, k)] }.to_h
203
- # Pre-format Entry
204
-
205
- table = TTY::Table.new(header: entry.keys, rows: [entry], orientation: :vertical)
206
-
207
- LogBot.debug('Rendering Entries') if ENV['DEBUG']
208
- table.render(:unicode, padding: [0, 1, 0, 1], multiline: true) do |renderer|
209
- renderer.border.style = :cyan
210
-
211
- # renderer.filter = lambda do |val, _row_index, col_index|
212
- # render_table_entry(val, col_index, args)
213
- # end
214
- end
215
-
216
- # LogBot.debug('Finish Render Table') if ENV['DEBUG']
217
- # Fall Back to Amazing Inspect
218
- rescue StandardError => e
219
- if ENV['DEBUG']
220
- LogBot.warn('Table', message: e.message)
221
- ap e.backtrace
222
- end
223
-
224
- [
225
- entry.ai,
226
- ('_' * (TTY::Screen.width / 3)).colorize(:cyan),
227
- "\n"
228
- ].join("\n")
229
- end
230
-
231
- def self.render_table_entry(val, col_index, args)
232
- return val.to_s unless col_index == 1
233
-
234
- format_table_entry(args, val)
235
- end
236
-
237
- # Main Entry Point for Filtering
238
- def self.filter_start(log_list, filter_type, args, opts)
239
- # Convert to Things
240
- logs = ShellHelper.find_things(log_list, args).select(&:processed?)
241
-
242
- # Ignore Archive/Host Dividers
243
- if args[:combine]
244
- results = logs.reject(&:blank?).map(&:data).flatten.compact
245
- ShellHelper.filter(results, filter_type, args, opts)
246
- else
247
- # Iterate and Preserve Archive/Host Index
248
- logs.each_with_object({}) do |log, obj|
249
- # Ignore Empty Results / No Thing
250
- next if log&.blank?
251
-
252
- obj[log.friendly_name] = ShellHelper.filter(log.data, filter_type, args, opts)
253
-
254
- obj
255
- end
256
- end
257
- end
258
-
259
- # Filter Logic
260
- # TODO: Simplify
261
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
262
- def self.filter(data, type = :all?, args = {}, opts = {})
263
- results = data.clone.flatten.compact
264
- results.select! do |row|
265
- opts.send(type) do |opt|
266
- filter_row_key(row, opt, args)
267
- end
268
- end
269
-
270
- # Time Filtering
271
- results = filter_time(results, args) if args[:start] || args[:end]
272
-
273
- # Strip Results if Slice is defined
274
- results = filter_slice(results, args[:slice]) if args[:slice]
275
-
276
- # Strip Results if Except is defined
277
- results = filter_except(results, args[:except]) if args[:except]
278
-
279
- # Remove Blank from either slice or except
280
- results.reject!(&:empty?)
281
-
282
- # Sort
283
- results.sort_by! { |x| x.slice(*args[:sort]).values } if args[:sort]
284
-
285
- # JSON Formatting
286
- results = results.map { |x| Oj.dump(x) } if args[:json]
287
-
288
- # Show Unique Only
289
- results = filter_uniq(results, args[:uniq]) if args.key?(:uniq)
290
-
291
- # Reverse
292
- results.reverse! if args[:reverse]
293
-
294
- # Count occurrences / Skip Results
295
- return filter_stats(results, args[:stats]) if args[:stats]
296
-
297
- # Pluck
298
- results = filter_pluck(results, args[:pluck]) if args.key?(:pluck)
299
-
300
- # Truncate
301
- # filter_truncate(results, args[:truncate].join.to_i) if args.key?(:truncate)
302
-
303
- # Limit
304
- if args[:limit]
305
- results[0..args[:limit].map(&:to_s).join.to_i - 1]
306
- else
307
- results
308
- end
309
- end
310
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
311
-
312
- # Filter Start and End Times
313
- # rubocop:disable Metrics/MethodLength
314
- # TODO: This is a bit icky, simplify/dry
315
- def self.filter_time(results, args)
316
- if args.start
317
- begin
318
- time_start = Time.parse(args.start.first.to_s)
319
-
320
- results.select! do |x|
321
- if x.time
322
- time_start < x.time
323
- else
324
- true
325
- end
326
- end
327
- rescue StandardError
328
- puts 'Unable to Process Start Time Filter'.colorize(:red)
329
- end
330
- end
331
-
332
- if args.end
333
- begin
334
- time_start = Time.parse(args.end.first.to_s)
335
-
336
- results.select! do |x|
337
- if x.time
338
- time_start > x.time
339
- else
340
- true
341
- end
342
- end
343
- rescue StandardError
344
- puts 'Unable to Process End Time Filter'.colorize(:red)
345
- end
346
- end
347
-
348
- results
349
- end
350
- # rubocop:enable Metrics/MethodLength
351
-
352
- def self.filter_except(results, except)
353
- # Avoid Empty Results
354
- if slice.empty?
355
- filter_empty_arg('slice')
356
- return results
357
- end
358
-
359
- results.map { |row| row.except(*except) }
360
- end
361
-
362
- def self.entry_truncate(entry, truncate)
363
- # Ignore if Truncation Off
364
- return entry if truncate.zero?
365
-
366
- # Only truncate large strings
367
- return entry unless entry.instance_of?(String) && entry.size > truncate
368
-
369
- # Include '...' to indicate truncation
370
- "#{entry.to_s[0..truncate]} #{'...'.colorize(:light_blue)}"
371
- end
372
-
373
- def self.filter_slice(results, slice)
374
- # Avoid Empty Results
375
- if slice.empty?
376
- filter_empty_arg('slice')
377
- return results
378
- end
379
-
380
- results.map { |row| row.slice(*slice) }
381
- end
382
-
383
- def self.filter_pluck(results, pluck)
384
- # Avoid Empty Results
385
- if pluck.empty?
386
- filter_empty_arg('pluck')
387
- return results
388
- end
389
-
390
- results.map { |x| x.slice(*pluck).values }.flatten
391
- end
392
-
393
- def self.filter_uniq(results, unique)
394
- # Avoid Empty Results
395
- if unique.empty?
396
- filter_empty_arg('uniq')
397
- return results
398
- end
399
-
400
- unique.map do |field|
401
- results.uniq { |x| x[field] }
402
- end.flatten
403
- end
404
-
405
- def self.filter_stats(results, stats)
406
- # Avoid Empty Results
407
- if stats.empty?
408
- filter_empty_arg('stats')
409
- return results
410
- end
411
-
412
- stats.map do |stat|
413
- occurrences = filter_count_occurrences(results, stat)
414
- occurrences.sort_by(&:last).to_h
415
- end
416
- end
417
-
418
- # Helper to Count occurrences
419
- def self.filter_count_occurrences(results, stat)
420
- results.each_with_object(Hash.new(0)) do |entry, counts|
421
- next unless entry.key? stat
422
-
423
- counts[entry[stat]] += 1
424
-
425
- counts
426
- end
427
- end
428
-
429
- def self.filter_empty_arg(arg)
430
- puts [
431
- 'Ignoring'.colorize(:light_yellow),
432
- "--#{arg}".colorize(:cyan),
433
- 'it requires an argument'.colorize(:red)
434
- ].join(' ')
435
- end
436
-
437
- # Break out filter row logic into separate method
438
- def self.filter_row_key(row, param, args)
439
- # Ignore Other Logic if Field isn't even included
440
- return false unless row.key? param.field
441
-
442
- # Sensitivity Check / Check for Match
443
- included = if args.key?(:case)
444
- row[param.field].to_s.include? param.value.to_s
445
- else
446
- row[param.field].to_s.downcase.include? param.value.to_s.downcase
447
- end
448
-
449
- # Pivot of off include vs exclude
450
- if param.bang
451
- !included
452
- else
453
- included
454
- end
455
- end
456
-
457
- # Total Count Helper
458
- def self.total_count(results)
459
- results.each do |k, v|
460
- puts k
461
- puts "Total: #{v.count.to_s.colorize(:blue)}"
462
- puts
463
- end
464
- end
465
-
466
- # Total Log List Manipulator
467
- def self.prepare_list(log_list, base_list = nil)
468
- base_list ||= GreenHat::ShellHelper::Log.list
469
-
470
- # Assume all
471
- log_list.push '*' if log_list.empty?
472
-
473
- # Map for All
474
- log_list = base_list.map(&:name) if log_list == ['*']
475
-
476
- log_list
477
- end
478
-
479
- # Shortcut find things
480
- def self.find_things(log_list, args = {})
481
- things = log_list.uniq.flat_map do |log|
482
- Thing.where name: log
483
- end
484
-
485
- # Host / Archive
486
- things.select! { |x| x.archive? args.archive } if args.archive
487
-
488
- things
489
- end
490
-
491
- # Main Entry Point for Searching
492
- def self.search_start(log_list, filter_type, args, opts)
493
- # Convert to Things
494
- logs = ShellHelper.find_things(log_list, args)
495
-
496
- logs.each_with_object({}) do |log, obj|
497
- # Ignore Empty Results / No Thing
498
- next if log&.data.blank?
499
-
500
- obj[log.friendly_name] = ShellHelper.search(log.data, filter_type, args, opts)
501
-
502
- obj
503
- end
504
- end
505
-
506
- # Generic Search Helper / String/Regex
507
- def self.search(data, type = :all?, args = {}, opts = {})
508
- results = data.clone.flatten.compact
509
-
510
- results.select! do |row|
511
- opts.send(type) do |opt|
512
- search_row(row, opt)
513
- end
514
- end
515
-
516
- # Strip Results if Slice is defined
517
- results.map! { |row| row.slice(*args[:slice]) } if args[:slice]
518
-
519
- # Strip Results if Except is defined
520
- results.map! { |row| row.except(*args[:except]) } if args[:except]
521
-
522
- # Remove Blank from either slice or except
523
- results.reject!(&:empty?)
524
-
525
- results
526
- end
527
-
528
- # Break out filter row logic into separate method
529
- def self.search_row(row, param)
530
- # Not Included Param
531
- included = row.to_s.include? param.value
532
-
533
- if param.bang
534
- !included
535
- else
536
- included
537
- end
538
- end
539
-
540
- # Color Reader Helper
541
- def self.pastel
542
- @pastel ||= Pastel.new
543
- end
544
-
545
- # Number Helper
546
- # https://gitlab.com/zedtux/human_size_to_number/-/blob/master/lib/human_size_to_number/helper.rb
547
- def self.human_size_to_number(string)
548
- size, unit = string.scan(/(\d*\.?\d+)\s?(Bytes?|KB|MB|GB|TB)/i).first
549
- number = size.to_f
550
-
551
- number = case unit.downcase
552
- when 'byte', 'bytes'
553
- number
554
- when 'kb'
555
- number * 1024
556
- when 'mb'
557
- number * 1024 * 1024
558
- when 'gb'
559
- number * 1024 * 1024 * 1024
560
- when 'tb'
561
- number * 1024 * 1024 * 1024 * 1024
562
- end
563
- number.round
564
- end
565
-
566
- # TODO: Needed?
567
- def self.filter_and(data, params = {})
568
- result = data.clone.flatten.compact
569
- params.each do |k, v|
570
- result.select! do |row|
571
- if row.key? k.to_sym
572
- row[k.to_sym].include? v
573
- else
574
- false
575
- end
576
- end
577
- next
578
- end
579
-
580
- result
581
- end
582
- end
583
- # rubocop:enable Metrics/ModuleLength
584
- end