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