log_sense 1.4.0 → 1.4.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.
@@ -4,27 +4,26 @@ require 'erb'
4
4
  require 'ostruct'
5
5
 
6
6
  module LogSense
7
+ #
8
+ # Emit Data
9
+ #
7
10
  module Emitter
8
-
9
- #
10
- # Emit Data
11
- #
12
11
  def self.emit data = {}, options = {}
13
- @input_format = options[:input_format] || "apache"
14
- @output_format = options[:output_format] || "html"
12
+ @input_format = options[:input_format] || 'apache'
13
+ @output_format = options[:output_format] || 'html'
15
14
 
16
15
  # for the ERB binding
16
+ @reports = method("#{@input_format}_report_specification".to_sym).call(data)
17
17
  @data = data
18
18
  @options = options
19
19
 
20
20
  # determine the main template to read
21
- @template = File.join(File.dirname(__FILE__), "templates", "#{@input_format}.#{@output_format}.erb")
21
+ @template = File.join(File.dirname(__FILE__), 'templates', "#{@input_format}.#{@output_format}.erb")
22
22
  erb_template = File.read @template
23
-
24
23
  output = ERB.new(erb_template).result(binding)
25
24
 
26
25
  if options[:output_file]
27
- file = File.open options[:output_file], "w"
26
+ file = File.open options[:output_file], 'w'
28
27
  file.write output
29
28
  file.close
30
29
  else
@@ -32,33 +31,467 @@ module LogSense
32
31
  end
33
32
  end
34
33
 
35
- private
34
+ private_class_method
36
35
 
37
36
  def self.render(template, vars)
38
- @template = File.join(File.dirname(__FILE__), "templates", "_#{template}")
37
+ @template = File.join(File.dirname(__FILE__), 'templates', "_#{template}")
39
38
  erb_template = File.read @template
40
39
  ERB.new(erb_template).result(OpenStruct.new(vars).instance_eval { binding })
41
40
  end
42
41
 
43
42
  def self.escape_javascript(string)
44
43
  js_escape_map = {
45
- "<script" => "&lt;script",
46
- "</script" => "&lt;/script",
47
- "<" => "&lt;",
48
- "</" => '&lt;\/',
49
- "\\" => "\\\\",
50
- "\r\n" => '\\r\\n',
51
- "\n" => '\\n',
52
- "\r" => '\\r',
44
+ '<' => '&lt;',
45
+ '</' => '&lt;\/',
46
+ '\\' => '\\\\',
47
+ '\r\n' => '\\r\\n',
48
+ '\n' => '\\n',
49
+ '\r' => '\\r',
53
50
  '"' => ' \\"',
54
51
  "'" => " \\'",
55
- "`" => " \\`",
56
- "$" => " \\$"
52
+ '`' => ' \\`',
53
+ '$' => ' \\$'
57
54
  }
58
55
  js_escape_map.each do |k, v|
59
56
  string = string.gsub(k, v)
60
57
  end
61
58
  string
62
59
  end
60
+
61
+ def self.slugify(string)
62
+ (string.start_with?(/[0-9]/) ? 'slug-' : '') + string.downcase.gsub(' ', '-')
63
+ end
64
+
65
+ def self.process(value)
66
+ klass = value.class
67
+ [Integer, Float].include?(klass) ? value : escape_javascript(value || '')
68
+ end
69
+
70
+ # limit width of special columns, that is, URL, Path, and Description
71
+ # - data: array of arrays
72
+ # - heading: array with column names
73
+ # - width width to set
74
+ def self.shorten(data, heading, width)
75
+ # indexes of columns which have to be shortened
76
+ to_shorten = %w[URL Description Path].map { |x| heading.index x }.compact
77
+ if width.nil? || to_shorten.empty? || data[0].nil?
78
+ data
79
+ else
80
+ table_columns = data[0].size
81
+ data.map { |x|
82
+ (0..table_columns - 1).each.map { |col|
83
+ should_shorten = x[col] && x[col].size > width - 3 && to_shorten.include?(col)
84
+ should_shorten ? "#{x[col][0..(width - 3)]}..." : x[col]
85
+ }
86
+ }
87
+ end
88
+ end
89
+
90
+ #
91
+ # Specification of the reports to generate
92
+ # Array of hashes with the following information:
93
+ # - title: report_title
94
+ # header: header of tabular data
95
+ # rows: data to show
96
+ # column_alignment: specification of column alignments (works for txt reports)
97
+ # vega_spec: specifications for Vega output
98
+ # datatable_options: specific options for datatable
99
+ def self.apache_report_specification(data)
100
+ [
101
+ { title: 'Daily Distribution',
102
+ header: %w[Day DOW Hits Visits Size],
103
+ column_alignment: %i[left left right right right],
104
+ rows: data[:daily_distribution],
105
+ vega_spec: {
106
+ 'layer': [
107
+ {
108
+ 'mark': {
109
+ 'type': 'line',
110
+ 'point': {
111
+ 'filled': false,
112
+ 'fill': 'white'
113
+ }
114
+ },
115
+ 'encoding': {
116
+ 'y': {'field': 'Hits', 'type': 'quantitative'}
117
+ }
118
+ },
119
+ {
120
+ 'mark': {
121
+ 'type': 'text',
122
+ 'color': '#3E5772',
123
+ 'align': 'middle',
124
+ 'baseline': 'top',
125
+ 'dx': -10,
126
+ 'yOffset': -15
127
+ },
128
+ 'encoding': {
129
+ 'text': {'field': 'Hits', 'type': 'quantitative'},
130
+ 'y': {'field': 'Hits', 'type': 'quantitative'}
131
+ }
132
+ },
133
+
134
+ {
135
+ 'mark': {
136
+ 'type': 'line',
137
+ 'color': '#A52A2A',
138
+ 'point': {
139
+ 'color': '#A52A2A',
140
+ 'filled': false,
141
+ 'fill': 'white',
142
+ }
143
+ },
144
+ 'encoding': {
145
+ 'y': {'field': 'Visits', 'type': 'quantitative'}
146
+ }
147
+ },
148
+
149
+ {
150
+ 'mark': {
151
+ 'type': 'text',
152
+ 'color': '#A52A2A',
153
+ 'align': 'middle',
154
+ 'baseline': 'top',
155
+ 'dx': -10,
156
+ 'yOffset': -15
157
+ },
158
+ 'encoding': {
159
+ 'text': {'field': 'Visits', 'type': 'quantitative'},
160
+ 'y': {'field': 'Visits', 'type': 'quantitative'}
161
+ }
162
+ },
163
+
164
+ ],
165
+ 'encoding': {
166
+ 'x': {'field': 'Day', 'type': 'temporal'},
167
+ }
168
+ }
169
+
170
+ },
171
+ { title: 'Time Distribution',
172
+ header: %w[Hour Hits Visits Size],
173
+ column_alignment: %i[left right right right],
174
+ rows: data[:time_distribution],
175
+ vega_spec: {
176
+ 'layer': [
177
+ {
178
+ 'mark': 'bar'
179
+ },
180
+ {
181
+ 'mark': {
182
+ 'type': 'text',
183
+ 'align': 'middle',
184
+ 'baseline': 'top',
185
+ 'dx': -10,
186
+ 'yOffset': -15
187
+ },
188
+ 'encoding': {
189
+ 'text': {'field': 'Hits', 'type': 'quantitative'},
190
+ 'y': {'field': 'Hits', 'type': 'quantitative'}
191
+ }
192
+ },
193
+ ],
194
+ 'encoding': {
195
+ 'x': {'field': 'Hour', 'type': 'nominal'},
196
+ 'y': {'field': 'Hits', 'type': 'quantitative'}
197
+ }
198
+ }
199
+ },
200
+ {
201
+ title: '20_ and 30_ on HTML pages',
202
+ header: %w[Path Hits Visits Size Status],
203
+ column_alignment: %i[left right right right right],
204
+ rows: data[:most_requested_pages],
205
+ datatable_options: 'columnDefs: [{ width: \'40%\', targets: 0 } ]'
206
+ },
207
+ {
208
+ title: '20_ and 30_ on other resources',
209
+ header: %w[Path Hits Visits Size Status],
210
+ column_alignment: %i[left right right right right],
211
+ rows: data[:most_requested_resources],
212
+ datatable_options: 'columnDefs: [{ width: \'40%\', targets: 0 } ]'
213
+ },
214
+ {
215
+ title: '40_ and 50_x on HTML pages',
216
+ header: %w[Path Hits Visits Status],
217
+ column_alignment: %i[left right right right],
218
+ rows: data[:missed_pages],
219
+ datatable_options: 'columnDefs: [{ width: \'40%\', targets: 0 } ]'
220
+ },
221
+ {
222
+ title: '40_ and 50_ on other resources',
223
+ header: %w[Path Hits Visits Status],
224
+ column_alignment: %i[left right right right],
225
+ rows: data[:missed_resources],
226
+ datatable_options: 'columnDefs: [{ width: \'40%\', targets: 0 } ]'
227
+ },
228
+ {
229
+ title: 'Statuses',
230
+ header: %w[Status Count],
231
+ column_alignment: %i[left right],
232
+ rows: data[:statuses],
233
+ vega_spec: {
234
+ 'mark': 'bar',
235
+ 'encoding': {
236
+ 'x': {'field': 'Status', 'type': 'nominal'},
237
+ 'y': {'field': 'Count', 'type': 'quantitative'}
238
+ }
239
+ }
240
+ },
241
+ {
242
+ title: 'Daily Statuses',
243
+ header: %w[Date S_2xx S_3xx S_4xx],
244
+ column_alignment: %i[left right right right],
245
+ rows: data[:statuses_by_day],
246
+ vega_spec: {
247
+ 'transform': [ {'fold': ['S_2xx', 'S_3xx', 'S_4xx' ] }],
248
+ 'mark': 'bar',
249
+ 'encoding': {
250
+ 'x': {
251
+ 'field': 'Date',
252
+ 'type': 'ordinal',
253
+ 'timeUnit': 'day',
254
+ },
255
+ 'y': {
256
+ 'aggregate': 'sum',
257
+ 'field': 'value',
258
+ 'type': 'quantitative'
259
+ },
260
+ 'color': {
261
+ 'field': 'key',
262
+ 'type': 'nominal',
263
+ 'scale': {
264
+ 'domain': ['S_2xx', 'S_3xx', 'S_4xx'],
265
+ 'range': ['#228b22', '#ff8c00', '#a52a2a']
266
+ },
267
+ }
268
+ }
269
+ }
270
+ },
271
+ { title: 'Browsers',
272
+ header: %w[Browser Hits Visits Size],
273
+ column_alignment: %i[left right right right],
274
+ rows: data[:browsers],
275
+ vega_spec: {
276
+ 'layer': [
277
+ { 'mark': 'bar' },
278
+ {
279
+ 'mark': {
280
+ 'type': 'text',
281
+ 'align': 'middle',
282
+ 'baseline': 'top',
283
+ 'dx': -10,
284
+ 'yOffset': -15
285
+ },
286
+ 'encoding': {
287
+ 'text': {'field': 'Hits', 'type': 'quantitative'},
288
+ }
289
+ },
290
+ ],
291
+ 'encoding': {
292
+ 'x': {'field': 'Browser', 'type': 'nominal'},
293
+ 'y': {'field': 'Hits', 'type': 'quantitative'}
294
+ }
295
+ }
296
+ },
297
+ { title: 'Platforms',
298
+ header: %w[Platform Hits Visits Size],
299
+ column_alignment: %i[left right right right],
300
+ rows: data[:platforms],
301
+ vega_spec: {
302
+ 'layer': [
303
+ { 'mark': 'bar' },
304
+ {
305
+ 'mark': {
306
+ 'type': 'text',
307
+ 'align': 'middle',
308
+ 'baseline': 'top',
309
+ 'dx': -10,
310
+ 'yOffset': -15
311
+ },
312
+ 'encoding': {
313
+ 'text': {'field': 'Hits', 'type': 'quantitative'},
314
+ }
315
+ },
316
+ ],
317
+ 'encoding': {
318
+ 'x': {'field': 'Platform', 'type': 'nominal'},
319
+ 'y': {'field': 'Hits', 'type': 'quantitative'}
320
+ }
321
+ }
322
+ },
323
+ {
324
+ title: 'IPs',
325
+ header: %w[IPs Hits Visits Size Country],
326
+ column_alignment: %i[left right right right left],
327
+ rows: data[:ips]
328
+ },
329
+ {
330
+ title: 'Referers',
331
+ header: %w[Referers Hits Visits Size],
332
+ column_alignment: %i[left right right right],
333
+ rows: data[:referers],
334
+ col: 'small-12 cell'
335
+ },
336
+ ]
337
+ end
338
+
339
+ def self.rails_report_specification(data)
340
+ [
341
+ {
342
+ title: "Daily Distribution",
343
+ header: %w[Day DOW Hits],
344
+ column_alignment: %i[left left right],
345
+ rows: data[:daily_distribution],
346
+ vega_spec: {
347
+ "encoding": {
348
+ "x": {"field": "Day", "type": "temporal"},
349
+ "y": {"field": "Hits", "type": "quantitative"}
350
+ },
351
+ "layer": [
352
+ {
353
+ "mark": {
354
+ "type": "line",
355
+ "point": {
356
+ "filled": false,
357
+ "fill": "white"
358
+ }
359
+ }
360
+ },
361
+ {
362
+ "mark": {
363
+ "type": "text",
364
+ "align": "left",
365
+ "baseline": "middle",
366
+ "dx": 5
367
+ },
368
+ "encoding": {
369
+ "text": {"field": "Hits", "type": "quantitative"}
370
+ }
371
+ }
372
+ ]
373
+ }
374
+ },
375
+ {
376
+ title: "Time Distribution",
377
+ header: %w[Hour Hits],
378
+ column_alignment: %i[left right],
379
+ rows: data[:time_distribution],
380
+ vega_spec: {
381
+ "layer": [
382
+ {
383
+ "mark": "bar",
384
+ },
385
+ {
386
+ "mark": {
387
+ "type": "text",
388
+ "align": "middle",
389
+ "baseline": "top",
390
+ "dx": -10,
391
+ "yOffset": -15
392
+ },
393
+ "encoding": {
394
+ "text": {"field": "Hits", "type": "quantitative"}
395
+ }
396
+ }
397
+ ],
398
+ "encoding": {
399
+ "x": {"field": "Hour", "type": "nominal"},
400
+ "y": {"field": "Hits", "type": "quantitative"}
401
+ }
402
+ }
403
+ },
404
+ {
405
+ title: "Statuses",
406
+ header: %w[Status Count],
407
+ column_alignment: %i[left right],
408
+ rows: data[:statuses],
409
+ vega_spec: {
410
+ "layer": [
411
+ {
412
+ "mark": "bar"
413
+ },
414
+ {
415
+ "mark": {
416
+ "type": "text",
417
+ "align": "left",
418
+ "baseline": "top",
419
+ "dx": -10,
420
+ "yOffset": -20
421
+ },
422
+ "encoding": {
423
+ "text": {"field": "Count", "type": "quantitative"}
424
+ }
425
+ }
426
+ ],
427
+ "encoding": {
428
+ "x": {"field": "Status", "type": "nominal"},
429
+ "y": {"field": "Count", "type": "quantitative"}
430
+ }
431
+ }
432
+ },
433
+ {
434
+ title: "Rails Performance",
435
+ header: %w[Controller Hits Min Avg Max],
436
+ column_alignment: %i[left right right right right],
437
+ rows: data[:performance],
438
+ vega_spec: {
439
+ "layer": [
440
+ {
441
+ "mark": {
442
+ "type": "point",
443
+ "name": "data_points"
444
+ }
445
+ },
446
+ {
447
+ "mark": {
448
+ "name": "label",
449
+ "type": "text",
450
+ "align": "left",
451
+ "baseline": "middle",
452
+ "dx": 5,
453
+ "yOffset": 0
454
+ },
455
+ "encoding": {
456
+ "text": {"field": "Controller"},
457
+ "fontSize": {"value": 8}
458
+ },
459
+ },
460
+ ],
461
+ "encoding": {
462
+ "x": {"field": "Avg", "type": "quantitative"},
463
+ "y": {"field": "Hits", "type": "quantitative"}
464
+ },
465
+ }
466
+ },
467
+ {
468
+ title: "Fatal Events",
469
+ header: %w[Date IP URL Description Log ID],
470
+ column_alignment: %i[left left left left left],
471
+ rows: data[:fatal],
472
+ col: "small-12 cell"
473
+ },
474
+ {
475
+ title: "Internal Server Errors",
476
+ header: %w[Date Status IP URL Description Log ID],
477
+ column_alignment: %i[left left left left left left],
478
+ rows: data[:internal_server_error],
479
+ col: "small-12 cell"
480
+ },
481
+ {
482
+ title: "Errors",
483
+ header: %w[Log ID Context Description Count],
484
+ column_alignment: %i[left left left left],
485
+ rows: data[:error],
486
+ col: "small-12 cell"
487
+ },
488
+ {
489
+ title: "IPs",
490
+ header: %w[IPs Hits Country],
491
+ column_alignment: %i[left right left],
492
+ rows: data[:ips]
493
+ }
494
+ ]
495
+ end
63
496
  end
64
497
  end
@@ -7,46 +7,46 @@ module LogSense
7
7
  #
8
8
  # parse command line options
9
9
  #
10
- def self.parse options
10
+ def self.parse(options)
11
11
  limit = 900
12
12
  args = {}
13
13
 
14
14
  opt_parser = OptionParser.new do |opts|
15
- opts.banner = "Usage: log_sense [options] [logfile]"
15
+ opts.banner = 'Usage: log_sense [options] [logfile ...]'
16
16
 
17
- opts.on("-tTITLE", "--title=TITLE", String, "Title to use in the report") do |n|
17
+ opts.on('-tTITLE', '--title=TITLE', String, 'Title to use in the report') do |n|
18
18
  args[:title] = n
19
19
  end
20
20
 
21
- opts.on("-fFORMAT", "--input-format=FORMAT", String, "Input format (either rails or apache)") do |n|
21
+ opts.on('-fFORMAT', '--input-format=FORMAT', String, 'Input format (either rails or apache)') do |n|
22
22
  args[:input_format] = n
23
23
  end
24
24
 
25
- opts.on("-iINPUT_FILE", "--input-file=INPUT_FILE", String, "Input file") do |n|
26
- args[:input_file] = n
27
- end
28
-
29
- opts.on("-tFORMAT", "--output-format=FORMAT", String, "Output format: html, org, txt, sqlite. See below for available formats") do |n|
25
+ opts.on('-tFORMAT', '--output-format=FORMAT', String, 'Output format: html, org, txt, sqlite. See below for available formats') do |n|
30
26
  args[:output_format] = n
31
27
  end
32
28
 
33
- opts.on("-oOUTPUT_FILE", "--output-file=OUTPUT_FILE", String, "Output file") do |n|
29
+ opts.on('-oOUTPUT_FILE', '--output-file=OUTPUT_FILE', String, 'Output file') do |n|
34
30
  args[:output_file] = n
35
31
  end
36
32
 
37
- opts.on("-bDATE", "--begin=DATE", Date, "Consider entries after or on DATE") do |n|
33
+ opts.on('-bDATE', '--begin=DATE', Date, 'Consider entries after or on DATE') do |n|
38
34
  args[:from_date] = n
39
35
  end
40
36
 
41
- opts.on("-eDATE", "--end=DATE", Date, "Consider entries before or on DATE") do |n|
37
+ opts.on('-eDATE', '--end=DATE', Date, 'Consider entries before or on DATE') do |n|
42
38
  args[:to_date] = n
43
39
  end
44
40
 
45
- opts.on("-lN", "--limit=N", Integer, "Number of entries to show (defaults to #{limit})") do |n|
41
+ opts.on('-lN', '--limit=N', Integer, "Limit to the N most requested resources (defaults to #{limit})") do |n|
46
42
  args[:limit] = n
47
43
  end
48
44
 
49
- opts.on("-cPOLICY", "--crawlers=POLICY", String, "Decide what to do with crawlers (applies to Apache Logs)") do |n|
45
+ opts.on('-wWIDTH', '--width=WIDTH', Integer, 'Maximum width of URL and description columns in text reports') do |n|
46
+ args[:width] = n
47
+ end
48
+
49
+ opts.on('-cPOLICY', '--crawlers=POLICY', String, 'Decide what to do with crawlers (applies to Apache Logs)') do |n|
50
50
  case n
51
51
  when 'only'
52
52
  args[:only_crawlers] = true
@@ -55,30 +55,30 @@ module LogSense
55
55
  end
56
56
  end
57
57
 
58
- opts.on("-ns", "--no-selfpoll", "Ignore self poll entries (requests from ::1; applies to Apache Logs)") do
58
+ opts.on('-ns', '--no-selfpoll', 'Ignore self poll entries (requests from ::1; applies to Apache Logs)') do
59
59
  args[:no_selfpoll] = true
60
60
  end
61
61
 
62
- opts.on("-v", "--version", "Prints version information") do
62
+ opts.on('-v', '--version', 'Prints version information') do
63
63
  puts "log_sense version #{LogSense::VERSION}"
64
- puts "Copyright (C) 2021 Shair.Tech"
65
- puts "Distributed under the terms of the MIT license"
64
+ puts 'Copyright (C) 2021 Shair.Tech'
65
+ puts 'Distributed under the terms of the MIT license'
66
66
  exit
67
67
  end
68
68
 
69
- opts.on("-h", "--help", "Prints this help") do
69
+ opts.on('-h', '--help', 'Prints this help') do
70
70
  puts opts
71
- puts ""
71
+ puts ''
72
72
  puts "This is version #{LogSense::VERSION}"
73
73
 
74
- puts ""
75
- puts "Output formats"
76
- pathname = File.join(File.dirname(__FILE__), "templates", "*")
77
- templates = Dir.glob(pathname).select { |x| ! File.basename(x).start_with? /_|#/ and ! File.basename(x).end_with? "~" }
78
- components = templates.map { |x| File.basename(x).split "." }.group_by { |x| x[0] }
74
+ puts ''
75
+ puts 'Output formats'
76
+ pathname = File.join(File.dirname(__FILE__), 'templates', '*')
77
+ templates = Dir.glob(pathname).select { |x| !File.basename(x).start_with?(/_|#/) && !File.basename(x).end_with?('~') }
78
+ components = templates.map { |x| File.basename(x).split '.' }.group_by { |x| x[0] }
79
79
  components.each do |k, vs|
80
80
  puts "#{k} parsing can produce the following outputs:"
81
- puts " - sqlite"
81
+ puts ' - sqlite'
82
82
  vs.each do |v|
83
83
  puts " - #{v[1]}"
84
84
  end
@@ -91,13 +91,13 @@ module LogSense
91
91
  opt_parser.parse!(options)
92
92
 
93
93
  args[:limit] ||= limit
94
- args[:input_format] ||= "apache"
95
- args[:output_format] ||= "html"
94
+ args[:input_format] ||= 'apache'
95
+ args[:output_format] ||= 'html'
96
96
  args[:ignore_crawlers] ||= false
97
97
  args[:only_crawlers] ||= false
98
98
  args[:no_selfpoll] ||= false
99
99
 
100
- return args
100
+ args
101
101
  end
102
102
  end
103
103
  end
@@ -19,9 +19,10 @@ module LogSense
19
19
  @last_day = last_day_s&.first&.first ? Date.parse(last_day_s[0][0]) : nil
20
20
 
21
21
  @total_days = 0
22
- if @first_day and @last_day
23
- @total_days = (@last_day - @first_day).to_i
24
- end
22
+ @total_days = (@last_day - @first_day).to_i if @first_day && @last_day
23
+
24
+ # TODO should also look into Error
25
+ @source_files = db.execute "SELECT distinct(source_file) from Event"
25
26
 
26
27
  @log_size = db.execute "SELECT count(started_at) from Event"
27
28
  @log_size = @log_size[0][0]