log_sense 1.5.2 → 1.6.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.org +27 -0
- data/Gemfile.lock +6 -4
- data/README.org +108 -34
- data/Rakefile +6 -6
- data/exe/log_sense +110 -39
- data/ip_locations/dbip-country-lite.sqlite3 +0 -0
- data/lib/log_sense/aggregator.rb +191 -0
- data/lib/log_sense/apache_aggregator.rb +122 -0
- data/lib/log_sense/apache_log_line_parser.rb +23 -21
- data/lib/log_sense/apache_log_parser.rb +15 -12
- data/lib/log_sense/apache_report_shaper.rb +309 -0
- data/lib/log_sense/emitter.rb +55 -553
- data/lib/log_sense/ip_locator.rb +24 -12
- data/lib/log_sense/options_checker.rb +24 -0
- data/lib/log_sense/options_parser.rb +81 -51
- data/lib/log_sense/rails_aggregator.rb +69 -0
- data/lib/log_sense/rails_log_parser.rb +82 -68
- data/lib/log_sense/rails_report_shaper.rb +183 -0
- data/lib/log_sense/report_shaper.rb +105 -0
- data/lib/log_sense/templates/_cdn_links.html.erb +11 -0
- data/lib/log_sense/templates/_command_invocation.html.erb +4 -0
- data/lib/log_sense/templates/_log_structure.html.erb +7 -1
- data/lib/log_sense/templates/_output_table.html.erb +6 -2
- data/lib/log_sense/templates/_rails.css.erb +7 -0
- data/lib/log_sense/templates/_summary.html.erb +9 -7
- data/lib/log_sense/templates/_summary.txt.erb +2 -2
- data/lib/log_sense/templates/{rails.html.erb → report_html.erb} +19 -37
- data/lib/log_sense/templates/{apache.txt.erb → report_txt.erb} +1 -1
- data/lib/log_sense/version.rb +1 -1
- data/lib/log_sense.rb +19 -9
- data/log_sense.gemspec +1 -1
- data/{apache-screenshot.png → screenshots/apache-screenshot.png} +0 -0
- data/screenshots/rails-screenshot.png +0 -0
- metadata +17 -11
- data/lib/log_sense/apache_data_cruncher.rb +0 -147
- data/lib/log_sense/rails_data_cruncher.rb +0 -141
- data/lib/log_sense/templates/apache.html.erb +0 -115
- data/lib/log_sense/templates/rails.txt.erb +0 -22
data/lib/log_sense/emitter.rb
CHANGED
@@ -1,43 +1,51 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
module LogSense
|
7
|
-
WORDS_SEPARATOR = ' · '
|
2
|
+
require "terminal-table"
|
3
|
+
require "json"
|
4
|
+
require "erb"
|
5
|
+
require "ostruct"
|
8
6
|
|
7
|
+
module LogSense
|
9
8
|
#
|
10
9
|
# Emit Data
|
11
10
|
#
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
@output_format = options[:output_format] || 'html'
|
11
|
+
class Emitter
|
12
|
+
CDN_CSS = [
|
13
|
+
"https://cdnjs.cloudflare.com/ajax/libs/foundicons/3.0.0/foundation-icons.min.css",
|
14
|
+
"https://cdn.jsdelivr.net/npm/foundation-sites@6.7.5/dist/css/foundation.min.css",
|
15
|
+
"https://cdn.datatables.net/v/zf/dt-1.11.3/datatables.min.css"
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
CDN_JS = [
|
19
|
+
"https://code.jquery.com/jquery-3.6.2.min.js",
|
20
|
+
"https://cdn.datatables.net/v/zf/dt-1.13.1/datatables.min.js",
|
21
|
+
"https://cdn.jsdelivr.net/npm/foundation-sites@6.7.5/dist/js/foundation.min.js",
|
22
|
+
"https://cdn.jsdelivr.net/npm/vega@5.22.1",
|
23
|
+
"https://cdn.jsdelivr.net/npm/vega-lite@5.6.0",
|
24
|
+
"https://cdn.jsdelivr.net/npm/vega-embed@6.21.0"
|
25
|
+
].freeze
|
28
26
|
|
29
|
-
|
30
|
-
|
27
|
+
def self.emit(reports = {}, data = {}, options = {})
|
28
|
+
# These are used in templates
|
29
|
+
@reports = reports
|
31
30
|
@data = data
|
32
31
|
@options = options
|
32
|
+
@report_title = options[:input_format].capitalize
|
33
|
+
@format_specific_css = "#{@options[:input_format]}.css.erb"
|
33
34
|
|
34
|
-
#
|
35
|
-
|
36
|
-
|
35
|
+
# Chooses template and destination
|
36
|
+
output_format = @options[:output_format]
|
37
|
+
output_file = @options[:output_file]
|
38
|
+
|
39
|
+
# read template and compile
|
40
|
+
template = File.join(File.dirname(__FILE__),
|
41
|
+
"templates",
|
42
|
+
"report_#{output_format}.erb")
|
43
|
+
erb_template = File.read template
|
37
44
|
output = ERB.new(erb_template, trim_mode: "-").result(binding)
|
38
45
|
|
39
|
-
|
40
|
-
|
46
|
+
# output
|
47
|
+
if output_file
|
48
|
+
file = File.open output_file, "w"
|
41
49
|
file.write output
|
42
50
|
file.close
|
43
51
|
else
|
@@ -46,41 +54,42 @@ module LogSense
|
|
46
54
|
end
|
47
55
|
|
48
56
|
#
|
49
|
-
#
|
57
|
+
# These are used in templates
|
50
58
|
#
|
59
|
+
|
51
60
|
def self.render(template, vars = {})
|
52
|
-
@template = File.join(File.dirname(__FILE__),
|
53
|
-
|
54
|
-
|
55
|
-
.
|
61
|
+
@template = File.join(File.dirname(__FILE__), "templates", "_#{template}")
|
62
|
+
if File.exist? @template
|
63
|
+
erb_template = File.read @template
|
64
|
+
ERB.new(erb_template, trim_mode: "-")
|
65
|
+
.result(OpenStruct.new(vars).instance_eval { binding })
|
66
|
+
end
|
56
67
|
end
|
57
68
|
|
58
69
|
def self.escape_javascript(string)
|
59
70
|
js_escape_map = {
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
'\r' => '\\r',
|
65
|
-
'\\' => ' \\\\',
|
71
|
+
#"&" => "&",
|
72
|
+
#"%" => "%",
|
73
|
+
"<" => "<",
|
74
|
+
"\\" => "\",
|
66
75
|
'"' => ' \\"',
|
67
76
|
"'" => " \\'",
|
68
|
-
|
69
|
-
|
77
|
+
"`" => " \\`",
|
78
|
+
"$" => " \\$"
|
70
79
|
}
|
71
|
-
js_escape_map.each do |
|
72
|
-
string = string.gsub(
|
80
|
+
js_escape_map.each do |match, replace|
|
81
|
+
string = string.gsub(match, replace)
|
73
82
|
end
|
74
83
|
string
|
75
84
|
end
|
76
85
|
|
77
86
|
def self.slugify(string)
|
78
|
-
(string.start_with?(/[0-9]/) ?
|
87
|
+
(string.start_with?(/[0-9]/) ? "slug-" : "") + string.downcase.gsub(" ", "-")
|
79
88
|
end
|
80
89
|
|
81
90
|
def self.process(value)
|
82
91
|
klass = value.class
|
83
|
-
[Integer, Float].include?(klass) ? value : escape_javascript(value ||
|
92
|
+
[Integer, Float].include?(klass) ? value : escape_javascript(value || "")
|
84
93
|
end
|
85
94
|
|
86
95
|
# limit width of special columns, that is, those in keywords
|
@@ -118,512 +127,5 @@ module LogSense
|
|
118
127
|
end
|
119
128
|
end
|
120
129
|
end
|
121
|
-
|
122
|
-
#
|
123
|
-
# Specification of the reports to generate
|
124
|
-
# Array of hashes with the following information:
|
125
|
-
# - title: report_title
|
126
|
-
# header: header of tabular data
|
127
|
-
# rows: data to show
|
128
|
-
# column_alignment: specification of column alignments (works for txt reports)
|
129
|
-
# vega_spec: specifications for Vega output
|
130
|
-
# datatable_options: specific options for datatable
|
131
|
-
def self.apache_report_specification(data = {})
|
132
|
-
[
|
133
|
-
{
|
134
|
-
title: 'Daily Distribution',
|
135
|
-
header: %w[Day DOW Hits Visits Size],
|
136
|
-
column_alignment: %i[left left right right right],
|
137
|
-
rows: data[:daily_distribution],
|
138
|
-
vega_spec: {
|
139
|
-
'layer': [
|
140
|
-
{
|
141
|
-
'mark': {
|
142
|
-
'type': 'line',
|
143
|
-
'point': {
|
144
|
-
'filled': false,
|
145
|
-
'fill': 'white'
|
146
|
-
}
|
147
|
-
},
|
148
|
-
'encoding': {
|
149
|
-
'y': {'field': 'Hits', 'type': 'quantitative'}
|
150
|
-
}
|
151
|
-
},
|
152
|
-
{
|
153
|
-
'mark': {
|
154
|
-
'type': 'text',
|
155
|
-
'color': '#3E5772',
|
156
|
-
'align': 'middle',
|
157
|
-
'baseline': 'top',
|
158
|
-
'dx': -10,
|
159
|
-
'yOffset': -15
|
160
|
-
},
|
161
|
-
'encoding': {
|
162
|
-
'text': {'field': 'Hits', 'type': 'quantitative'},
|
163
|
-
'y': {'field': 'Hits', 'type': 'quantitative'}
|
164
|
-
}
|
165
|
-
},
|
166
|
-
|
167
|
-
{
|
168
|
-
'mark': {
|
169
|
-
'type': 'line',
|
170
|
-
'color': '#A52A2A',
|
171
|
-
'point': {
|
172
|
-
'color': '#A52A2A',
|
173
|
-
'filled': false,
|
174
|
-
'fill': 'white',
|
175
|
-
}
|
176
|
-
},
|
177
|
-
'encoding': {
|
178
|
-
'y': {'field': 'Visits', 'type': 'quantitative'}
|
179
|
-
}
|
180
|
-
},
|
181
|
-
|
182
|
-
{
|
183
|
-
'mark': {
|
184
|
-
'type': 'text',
|
185
|
-
'color': '#A52A2A',
|
186
|
-
'align': 'middle',
|
187
|
-
'baseline': 'top',
|
188
|
-
'dx': -10,
|
189
|
-
'yOffset': -15
|
190
|
-
},
|
191
|
-
'encoding': {
|
192
|
-
'text': {'field': 'Visits', 'type': 'quantitative'},
|
193
|
-
'y': {'field': 'Visits', 'type': 'quantitative'}
|
194
|
-
}
|
195
|
-
},
|
196
|
-
|
197
|
-
],
|
198
|
-
'encoding': {
|
199
|
-
'x': {'field': 'Day', 'type': 'temporal'},
|
200
|
-
}
|
201
|
-
}
|
202
|
-
},
|
203
|
-
{
|
204
|
-
title: 'Time Distribution',
|
205
|
-
header: %w[Hour Hits Visits Size],
|
206
|
-
column_alignment: %i[left right right right],
|
207
|
-
rows: data[:time_distribution],
|
208
|
-
vega_spec: {
|
209
|
-
'layer': [
|
210
|
-
{
|
211
|
-
'mark': 'bar'
|
212
|
-
},
|
213
|
-
{
|
214
|
-
'mark': {
|
215
|
-
'type': 'text',
|
216
|
-
'align': 'middle',
|
217
|
-
'baseline': 'top',
|
218
|
-
'dx': -10,
|
219
|
-
'yOffset': -15
|
220
|
-
},
|
221
|
-
'encoding': {
|
222
|
-
'text': {'field': 'Hits', 'type': 'quantitative'},
|
223
|
-
'y': {'field': 'Hits', 'type': 'quantitative'}
|
224
|
-
}
|
225
|
-
},
|
226
|
-
],
|
227
|
-
'encoding': {
|
228
|
-
'x': {'field': 'Hour', 'type': 'nominal'},
|
229
|
-
'y': {'field': 'Hits', 'type': 'quantitative'}
|
230
|
-
}
|
231
|
-
}
|
232
|
-
},
|
233
|
-
{
|
234
|
-
title: '20_ and 30_ on HTML pages',
|
235
|
-
header: %w[Path Hits Visits Size Status],
|
236
|
-
column_alignment: %i[left right right right right],
|
237
|
-
rows: data[:most_requested_pages],
|
238
|
-
datatable_options: 'columnDefs: [{ width: \'40%\', targets: 0 }, { width: \'15%\', targets: [1, 2, 3, 4] }], dataRender: true'
|
239
|
-
},
|
240
|
-
{
|
241
|
-
title: '20_ and 30_ on other resources',
|
242
|
-
header: %w[Path Hits Visits Size Status],
|
243
|
-
column_alignment: %i[left right right right right],
|
244
|
-
rows: data[:most_requested_resources],
|
245
|
-
datatable_options: 'columnDefs: [{ width: \'40%\', targets: 0 }, { width: \'15%\', targets: [1, 2, 3, 4] }], dataRender: true'
|
246
|
-
},
|
247
|
-
{
|
248
|
-
title: '40_ and 50_x on HTML pages',
|
249
|
-
header: %w[Path Hits Visits Status],
|
250
|
-
column_alignment: %i[left right right right],
|
251
|
-
rows: data[:missed_pages],
|
252
|
-
datatable_options: 'columnDefs: [{ width: \'40%\', targets: 0 }, { width: \'20%\', targets: [1, 2, 3] }], dataRender: true'
|
253
|
-
},
|
254
|
-
{
|
255
|
-
title: '40_ and 50_ on other resources',
|
256
|
-
header: %w[Path Hits Visits Status],
|
257
|
-
column_alignment: %i[left right right right],
|
258
|
-
rows: data[:missed_resources],
|
259
|
-
datatable_options: 'columnDefs: [{ width: \'40%\', targets: 0 }, { width: \'20%\', targets: [1, 2, 3] }], dataRender: true'
|
260
|
-
},
|
261
|
-
{
|
262
|
-
title: '40_ and 50_x on HTML pages by IP',
|
263
|
-
header: %w[IP Hits Paths],
|
264
|
-
column_alignment: %i[left right left],
|
265
|
-
# Value is something along the line of:
|
266
|
-
# [["66.249.79.93", "/adolfo/notes/calendar/2014/11/16.html", "404"],
|
267
|
-
# ["66.249.79.93", "/adolfo/website-specification/generate-xml-sitemap.org.html", "404"]]
|
268
|
-
rows: data[:missed_pages_by_ip]&.group_by { |x| x[0] }&.map { |k, v|
|
269
|
-
[
|
270
|
-
k,
|
271
|
-
v.size,
|
272
|
-
v.map { |x| x[1] }.join(WORDS_SEPARATOR)
|
273
|
-
]
|
274
|
-
}&.sort { |x, y| y[1] <=> x[1] }
|
275
|
-
},
|
276
|
-
{
|
277
|
-
title: '40_ and 50_ on other resources by IP',
|
278
|
-
header: %w[IP Hits Paths],
|
279
|
-
column_alignment: %i[left right left],
|
280
|
-
# Value is something along the line of:
|
281
|
-
# [["66.249.79.93", "/adolfo/notes/calendar/2014/11/16.html", "404"],
|
282
|
-
# ["66.249.79.93", "/adolfo/website-specification/generate-xml-sitemap.org.html", "404"]]
|
283
|
-
rows: data[:missed_resources_by_ip]&.group_by { |x| x[0] }&.map { |k, v|
|
284
|
-
[
|
285
|
-
k,
|
286
|
-
v.size,
|
287
|
-
v.map { |x| x[1] }.join(WORDS_SEPARATOR)
|
288
|
-
]
|
289
|
-
}&.sort { |x, y| y[1] <=> x[1] }
|
290
|
-
},
|
291
|
-
{
|
292
|
-
title: 'Statuses',
|
293
|
-
header: %w[Status Count],
|
294
|
-
column_alignment: %i[left right],
|
295
|
-
rows: data[:statuses],
|
296
|
-
vega_spec: {
|
297
|
-
'mark': 'bar',
|
298
|
-
'encoding': {
|
299
|
-
'x': {'field': 'Status', 'type': 'nominal'},
|
300
|
-
'y': {'field': 'Count', 'type': 'quantitative'}
|
301
|
-
}
|
302
|
-
}
|
303
|
-
},
|
304
|
-
{
|
305
|
-
title: 'Daily Statuses',
|
306
|
-
header: %w[Date S_2xx S_3xx S_4xx],
|
307
|
-
column_alignment: %i[left right right right],
|
308
|
-
rows: data[:statuses_by_day],
|
309
|
-
vega_spec: {
|
310
|
-
'transform': [ {'fold': ['S_2xx', 'S_3xx', 'S_4xx' ] }],
|
311
|
-
'mark': 'bar',
|
312
|
-
'encoding': {
|
313
|
-
'x': {
|
314
|
-
'field': 'Date',
|
315
|
-
'type': 'ordinal',
|
316
|
-
'timeUnit': 'day',
|
317
|
-
},
|
318
|
-
'y': {
|
319
|
-
'aggregate': 'sum',
|
320
|
-
'field': 'value',
|
321
|
-
'type': 'quantitative'
|
322
|
-
},
|
323
|
-
'color': {
|
324
|
-
'field': 'key',
|
325
|
-
'type': 'nominal',
|
326
|
-
'scale': {
|
327
|
-
'domain': ['S_2xx', 'S_3xx', 'S_4xx'],
|
328
|
-
'range': ['#228b22', '#ff8c00', '#a52a2a']
|
329
|
-
},
|
330
|
-
}
|
331
|
-
}
|
332
|
-
}
|
333
|
-
},
|
334
|
-
{
|
335
|
-
title: 'Browsers',
|
336
|
-
header: %w[Browser Hits Visits Size],
|
337
|
-
column_alignment: %i[left right right right],
|
338
|
-
rows: data[:browsers],
|
339
|
-
vega_spec: {
|
340
|
-
'layer': [
|
341
|
-
{ 'mark': 'bar' },
|
342
|
-
{
|
343
|
-
'mark': {
|
344
|
-
'type': 'text',
|
345
|
-
'align': 'middle',
|
346
|
-
'baseline': 'top',
|
347
|
-
'dx': -10,
|
348
|
-
'yOffset': -15
|
349
|
-
},
|
350
|
-
'encoding': {
|
351
|
-
'text': {'field': 'Hits', 'type': 'quantitative'},
|
352
|
-
}
|
353
|
-
},
|
354
|
-
],
|
355
|
-
'encoding': {
|
356
|
-
'x': {'field': 'Browser', 'type': 'nominal'},
|
357
|
-
'y': {'field': 'Hits', 'type': 'quantitative'}
|
358
|
-
}
|
359
|
-
}
|
360
|
-
},
|
361
|
-
{
|
362
|
-
title: 'Platforms',
|
363
|
-
header: %w[Platform Hits Visits Size],
|
364
|
-
column_alignment: %i[left right right right],
|
365
|
-
rows: data[:platforms],
|
366
|
-
vega_spec: {
|
367
|
-
'layer': [
|
368
|
-
{ 'mark': 'bar' },
|
369
|
-
{
|
370
|
-
'mark': {
|
371
|
-
'type': 'text',
|
372
|
-
'align': 'middle',
|
373
|
-
'baseline': 'top',
|
374
|
-
'dx': -10,
|
375
|
-
'yOffset': -15
|
376
|
-
},
|
377
|
-
'encoding': {
|
378
|
-
'text': {'field': 'Hits', 'type': 'quantitative'},
|
379
|
-
}
|
380
|
-
},
|
381
|
-
],
|
382
|
-
'encoding': {
|
383
|
-
'x': {'field': 'Platform', 'type': 'nominal'},
|
384
|
-
'y': {'field': 'Hits', 'type': 'quantitative'}
|
385
|
-
}
|
386
|
-
}
|
387
|
-
},
|
388
|
-
{
|
389
|
-
title: 'IPs',
|
390
|
-
header: %w[IP Hits Visits Size Country],
|
391
|
-
column_alignment: %i[left right right right left],
|
392
|
-
rows: data[:ips]
|
393
|
-
},
|
394
|
-
{
|
395
|
-
title: 'Countries',
|
396
|
-
header: ["Country", "Hits", "Visits", "IPs", "IP List"],
|
397
|
-
column_alignment: %i[left right right right left],
|
398
|
-
rows: data[:countries]&.map { |k, v|
|
399
|
-
[
|
400
|
-
k,
|
401
|
-
v.map { |x| x[1] }.inject(&:+),
|
402
|
-
v.map { |x| x[2] }.inject(&:+),
|
403
|
-
v.map { |x| x[0] }.size,
|
404
|
-
v.map { |x| x[0] }.join(WORDS_SEPARATOR)
|
405
|
-
]
|
406
|
-
}&.sort { |x, y| y[3] <=> x[3] }
|
407
|
-
},
|
408
|
-
{
|
409
|
-
title: 'Combined Platform Data',
|
410
|
-
header: %w[ Browser OS IP Hits Size],
|
411
|
-
column_alignment: %i[left left left right right],
|
412
|
-
col: 'small-12 cell',
|
413
|
-
rows: data[:combined_platforms],
|
414
|
-
},
|
415
|
-
{
|
416
|
-
title: 'Referers',
|
417
|
-
header: %w[Referers Hits Visits Size],
|
418
|
-
column_alignment: %i[left right right right],
|
419
|
-
datatable_options: 'columnDefs: [{ width: \'50%\', targets: 0 } ], dataRender: true',
|
420
|
-
rows: data[:referers],
|
421
|
-
col: 'small-12 cell'
|
422
|
-
},
|
423
|
-
{
|
424
|
-
title: 'Streaks',
|
425
|
-
report: :html,
|
426
|
-
header: ['IP', 'Date', 'Total HTML', 'Total Other', 'HTML', 'Other'],
|
427
|
-
column_alignment: %i[left left right right left left],
|
428
|
-
datatable_options: 'columnDefs: [{ width: \'30%\', targets: [4, 5] }, { width: \'10%\', targets: [0, 1, 2, 3]} ], dataRender: true',
|
429
|
-
rows: data[:streaks]&.group_by { |x| [x[0], x[1]] }&.map do |k, v|
|
430
|
-
[
|
431
|
-
k[0],
|
432
|
-
k[1],
|
433
|
-
v.map { |x| x[2] }.compact.select { |x| x.match(/\.html?$/) }.size,
|
434
|
-
v.map { |x| x[2] }.compact.reject { |x| x.match(/\.html?$/) }.size,
|
435
|
-
v.map { |x| x[2] }.compact.select { |x| x.match(/\.html?$/) }.join(WORDS_SEPARATOR),
|
436
|
-
v.map { |x| x[2] }.compact.reject { |x| x.match(/\.html?$/) }.join(WORDS_SEPARATOR)
|
437
|
-
]
|
438
|
-
end,
|
439
|
-
col: 'small-12 cell'
|
440
|
-
}
|
441
|
-
]
|
442
|
-
end
|
443
|
-
|
444
|
-
def self.rails_report_specification(data = {})
|
445
|
-
[
|
446
|
-
{
|
447
|
-
title: "Daily Distribution",
|
448
|
-
header: %w[Day DOW Hits],
|
449
|
-
column_alignment: %i[left left right],
|
450
|
-
rows: data[:daily_distribution],
|
451
|
-
vega_spec: {
|
452
|
-
"encoding": {
|
453
|
-
"x": {"field": "Day", "type": "temporal"},
|
454
|
-
"y": {"field": "Hits", "type": "quantitative"}
|
455
|
-
},
|
456
|
-
"layer": [
|
457
|
-
{
|
458
|
-
"mark": {
|
459
|
-
"type": "line",
|
460
|
-
"point": {
|
461
|
-
"filled": false,
|
462
|
-
"fill": "white"
|
463
|
-
}
|
464
|
-
}
|
465
|
-
},
|
466
|
-
{
|
467
|
-
"mark": {
|
468
|
-
"type": "text",
|
469
|
-
"align": "left",
|
470
|
-
"baseline": "middle",
|
471
|
-
"dx": 5
|
472
|
-
},
|
473
|
-
"encoding": {
|
474
|
-
"text": {"field": "Hits", "type": "quantitative"}
|
475
|
-
}
|
476
|
-
}
|
477
|
-
]
|
478
|
-
}
|
479
|
-
},
|
480
|
-
{
|
481
|
-
title: "Time Distribution",
|
482
|
-
header: %w[Hour Hits],
|
483
|
-
column_alignment: %i[left right],
|
484
|
-
rows: data[:time_distribution],
|
485
|
-
vega_spec: {
|
486
|
-
"layer": [
|
487
|
-
{
|
488
|
-
"mark": "bar",
|
489
|
-
},
|
490
|
-
{
|
491
|
-
"mark": {
|
492
|
-
"type": "text",
|
493
|
-
"align": "middle",
|
494
|
-
"baseline": "top",
|
495
|
-
"dx": -10,
|
496
|
-
"yOffset": -15
|
497
|
-
},
|
498
|
-
"encoding": {
|
499
|
-
"text": {"field": "Hits", "type": "quantitative"}
|
500
|
-
}
|
501
|
-
}
|
502
|
-
],
|
503
|
-
"encoding": {
|
504
|
-
"x": {"field": "Hour", "type": "nominal"},
|
505
|
-
"y": {"field": "Hits", "type": "quantitative"}
|
506
|
-
}
|
507
|
-
}
|
508
|
-
},
|
509
|
-
{
|
510
|
-
title: "Statuses",
|
511
|
-
header: %w[Status Count],
|
512
|
-
column_alignment: %i[left right],
|
513
|
-
rows: data[:statuses],
|
514
|
-
vega_spec: {
|
515
|
-
"layer": [
|
516
|
-
{
|
517
|
-
"mark": "bar"
|
518
|
-
},
|
519
|
-
{
|
520
|
-
"mark": {
|
521
|
-
"type": "text",
|
522
|
-
"align": "left",
|
523
|
-
"baseline": "top",
|
524
|
-
"dx": -10,
|
525
|
-
"yOffset": -20
|
526
|
-
},
|
527
|
-
"encoding": {
|
528
|
-
"text": {"field": "Count", "type": "quantitative"}
|
529
|
-
}
|
530
|
-
}
|
531
|
-
],
|
532
|
-
"encoding": {
|
533
|
-
"x": {"field": "Status", "type": "nominal"},
|
534
|
-
"y": {"field": "Count", "type": "quantitative"}
|
535
|
-
}
|
536
|
-
}
|
537
|
-
},
|
538
|
-
{
|
539
|
-
title: "Rails Performance",
|
540
|
-
header: %w[Controller Hits Min Avg Max],
|
541
|
-
column_alignment: %i[left right right right right],
|
542
|
-
rows: data[:performance],
|
543
|
-
vega_spec: {
|
544
|
-
"layer": [
|
545
|
-
{
|
546
|
-
"mark": {
|
547
|
-
"type": "point",
|
548
|
-
"name": "data_points"
|
549
|
-
}
|
550
|
-
},
|
551
|
-
{
|
552
|
-
"mark": {
|
553
|
-
"name": "label",
|
554
|
-
"type": "text",
|
555
|
-
"align": "left",
|
556
|
-
"baseline": "middle",
|
557
|
-
"dx": 5,
|
558
|
-
"yOffset": 0
|
559
|
-
},
|
560
|
-
"encoding": {
|
561
|
-
"text": {"field": "Controller"},
|
562
|
-
"fontSize": {"value": 8}
|
563
|
-
},
|
564
|
-
},
|
565
|
-
],
|
566
|
-
"encoding": {
|
567
|
-
"x": {"field": "Avg", "type": "quantitative"},
|
568
|
-
"y": {"field": "Hits", "type": "quantitative"}
|
569
|
-
},
|
570
|
-
}
|
571
|
-
},
|
572
|
-
{
|
573
|
-
title: "Fatal Events",
|
574
|
-
header: %w[Date IP URL Description Log ID],
|
575
|
-
column_alignment: %i[left left left left left],
|
576
|
-
rows: data[:fatal],
|
577
|
-
col: 'small-12 cell'
|
578
|
-
},
|
579
|
-
{
|
580
|
-
title: 'Internal Server Errors',
|
581
|
-
header: %w[Date Status IP URL Description Log ID],
|
582
|
-
column_alignment: %i[left left left left left left],
|
583
|
-
rows: data[:internal_server_error],
|
584
|
-
col: 'small-12 cell'
|
585
|
-
},
|
586
|
-
{
|
587
|
-
title: 'Errors',
|
588
|
-
header: %w[Log ID Context Description Count],
|
589
|
-
column_alignment: %i[left left left left],
|
590
|
-
rows: data[:error],
|
591
|
-
col: 'small-12 cell'
|
592
|
-
},
|
593
|
-
{
|
594
|
-
title: 'IPs',
|
595
|
-
header: %w[IPs Hits Country],
|
596
|
-
column_alignment: %i[left right left],
|
597
|
-
rows: data[:ips]
|
598
|
-
},
|
599
|
-
{
|
600
|
-
title: 'Countries',
|
601
|
-
header: %w[Country Hits IPs],
|
602
|
-
column_alignment: %i[left right left],
|
603
|
-
rows: data[:countries]&.map { |k, v|
|
604
|
-
[
|
605
|
-
k,
|
606
|
-
v.map { |x| x[1] }.inject(&:+),
|
607
|
-
v.map { |x| x[0] }.join(WORDS_SEPARATOR)
|
608
|
-
]
|
609
|
-
}&.sort { |x, y| x[0] <=> y[0] }
|
610
|
-
},
|
611
|
-
{
|
612
|
-
title: 'Streaks',
|
613
|
-
report: :html,
|
614
|
-
header: %w[IP Date Total Resources],
|
615
|
-
column_alignment: %i[left left right right left left],
|
616
|
-
rows: data[:streaks]&.group_by { |x| [x[0], x[1]] }&.map do |k, v|
|
617
|
-
[
|
618
|
-
k[0],
|
619
|
-
k[1],
|
620
|
-
v.size,
|
621
|
-
v.map { |x| x[2] }.join(WORDS_SEPARATOR)
|
622
|
-
]
|
623
|
-
end,
|
624
|
-
col: 'small-12 cell'
|
625
|
-
}
|
626
|
-
]
|
627
|
-
end
|
628
130
|
end
|
629
131
|
end
|