log_sense 1.3.5 → 1.5.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 +46 -0
- data/Gemfile.lock +4 -4
- data/README.org +24 -10
- data/Rakefile +17 -3
- data/exe/log_sense +24 -16
- data/ip_locations/dbip-country-lite.sqlite3 +0 -0
- data/lib/log_sense/apache_data_cruncher.rb +30 -30
- data/lib/log_sense/apache_log_line_parser.rb +12 -13
- data/lib/log_sense/apache_log_parser.rb +44 -36
- data/lib/log_sense/emitter.rb +518 -15
- data/lib/log_sense/ip_locator.rb +26 -19
- data/lib/log_sense/options_parser.rb +35 -30
- data/lib/log_sense/rails_data_cruncher.rb +8 -4
- data/lib/log_sense/rails_log_parser.rb +108 -100
- data/lib/log_sense/templates/_command_invocation.html.erb +0 -4
- data/lib/log_sense/templates/_command_invocation.txt.erb +4 -3
- data/lib/log_sense/templates/_navigation.html.erb +21 -0
- data/lib/log_sense/templates/_output_table.html.erb +2 -7
- data/lib/log_sense/templates/_output_table.txt.erb +14 -0
- data/lib/log_sense/templates/_performance.html.erb +1 -1
- data/lib/log_sense/templates/_performance.txt.erb +8 -5
- data/lib/log_sense/templates/_report_data.html.erb +2 -2
- data/lib/log_sense/templates/_summary.html.erb +6 -1
- data/lib/log_sense/templates/_summary.txt.erb +11 -8
- data/lib/log_sense/templates/_warning.txt.erb +1 -0
- data/lib/log_sense/templates/apache.html.erb +14 -335
- data/lib/log_sense/templates/apache.txt.erb +22 -0
- data/lib/log_sense/templates/rails.html.erb +13 -174
- data/lib/log_sense/templates/rails.txt.erb +10 -60
- data/lib/log_sense/version.rb +1 -1
- metadata +6 -2
@@ -7,46 +7,46 @@ module LogSense
|
|
7
7
|
#
|
8
8
|
# parse command line options
|
9
9
|
#
|
10
|
-
def self.parse
|
11
|
-
limit =
|
10
|
+
def self.parse(options)
|
11
|
+
limit = 900
|
12
12
|
args = {}
|
13
13
|
|
14
14
|
opt_parser = OptionParser.new do |opts|
|
15
|
-
opts.banner =
|
15
|
+
opts.banner = 'Usage: log_sense [options] [logfile ...]'
|
16
16
|
|
17
|
-
opts.on(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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,34 @@ module LogSense
|
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
-
opts.on(
|
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(
|
62
|
+
opts.on('--verbose', 'Inform about progress (prints to STDERR)') do
|
63
|
+
args[:verbose] = true
|
64
|
+
end
|
65
|
+
|
66
|
+
opts.on('-v', '--version', 'Prints version information') do
|
63
67
|
puts "log_sense version #{LogSense::VERSION}"
|
64
|
-
puts
|
65
|
-
puts
|
68
|
+
puts 'Copyright (C) 2021 Shair.Tech'
|
69
|
+
puts 'Distributed under the terms of the MIT license'
|
66
70
|
exit
|
67
71
|
end
|
68
72
|
|
69
|
-
opts.on(
|
73
|
+
opts.on('-h', '--help', 'Prints this help') do
|
70
74
|
puts opts
|
71
|
-
puts
|
75
|
+
puts ''
|
72
76
|
puts "This is version #{LogSense::VERSION}"
|
73
77
|
|
74
|
-
puts
|
75
|
-
puts
|
76
|
-
pathname = File.join(File.dirname(__FILE__),
|
77
|
-
templates = Dir.glob(pathname).select { |x| !
|
78
|
-
components = templates.map { |x| File.basename(x).split
|
78
|
+
puts ''
|
79
|
+
puts 'Output formats'
|
80
|
+
pathname = File.join(File.dirname(__FILE__), 'templates', '*')
|
81
|
+
templates = Dir.glob(pathname).select { |x| !File.basename(x).start_with?(/_|#/) && !File.basename(x).end_with?('~') }
|
82
|
+
components = templates.map { |x| File.basename(x).split '.' }.group_by { |x| x[0] }
|
79
83
|
components.each do |k, vs|
|
80
84
|
puts "#{k} parsing can produce the following outputs:"
|
81
|
-
puts
|
85
|
+
puts ' - sqlite'
|
82
86
|
vs.each do |v|
|
83
87
|
puts " - #{v[1]}"
|
84
88
|
end
|
@@ -91,13 +95,14 @@ module LogSense
|
|
91
95
|
opt_parser.parse!(options)
|
92
96
|
|
93
97
|
args[:limit] ||= limit
|
94
|
-
args[:input_format] ||=
|
95
|
-
args[:output_format] ||=
|
98
|
+
args[:input_format] ||= 'apache'
|
99
|
+
args[:output_format] ||= 'html'
|
96
100
|
args[:ignore_crawlers] ||= false
|
97
101
|
args[:only_crawlers] ||= false
|
98
102
|
args[:no_selfpoll] ||= false
|
103
|
+
args[:verbose] ||= false
|
99
104
|
|
100
|
-
|
105
|
+
args
|
101
106
|
end
|
102
107
|
end
|
103
108
|
end
|
@@ -7,7 +7,7 @@ module LogSense
|
|
7
7
|
# @ variables are automatically put in the returned data
|
8
8
|
#
|
9
9
|
|
10
|
-
def self.crunch db, options = { limit:
|
10
|
+
def self.crunch db, options = { limit: 900 }
|
11
11
|
first_day_s = db.execute "SELECT started_at from Event where started_at not NULL order by started_at limit 1"
|
12
12
|
# we could use ended_at to cover the full activity period, but I prefer started_at
|
13
13
|
# with the meaning that the monitor event initiation
|
@@ -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
|
23
|
-
|
24
|
-
|
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]
|
@@ -103,6 +104,9 @@ module LogSense
|
|
103
104
|
|
104
105
|
@ips = db.execute "SELECT ip, count(ip) from Event where #{filter} group by ip order by count(ip) desc limit #{options[:limit]}"
|
105
106
|
|
107
|
+
@streaks = db.execute 'SELECT ip, substr(started_at, 1, 10), url from Event order by ip, started_at'
|
108
|
+
data = {}
|
109
|
+
|
106
110
|
@performance = db.execute "SELECT distinct(controller), count(controller), printf(\"%.2f\", min(duration_total_ms)), printf(\"%.2f\", avg(duration_total_ms)), printf(\"%.2f\", max(duration_total_ms)) from Event group by controller order by controller"
|
107
111
|
|
108
112
|
@fatal = db.execute ("SELECT strftime(\"%Y-%m-%d %H:%M\", started_at), ip, url, error.description, event.log_id FROM Event JOIN Error ON event.log_id == error.log_id WHERE exit_status == 'F'") || [[]]
|
@@ -2,10 +2,8 @@ require 'sqlite3'
|
|
2
2
|
|
3
3
|
module LogSense
|
4
4
|
module RailsLogParser
|
5
|
-
def self.parse
|
6
|
-
|
7
|
-
|
8
|
-
db = SQLite3::Database.new ":memory:"
|
5
|
+
def self.parse(streams, options = {})
|
6
|
+
db = SQLite3::Database.new ':memory:'
|
9
7
|
db.execute 'CREATE TABLE IF NOT EXISTS Event(
|
10
8
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
11
9
|
exit_status TEXT,
|
@@ -17,12 +15,14 @@ module LogSense
|
|
17
15
|
url TEXT,
|
18
16
|
controller TEXT,
|
19
17
|
html_verb TEXT,
|
20
|
-
status INTEGER,
|
18
|
+
status INTEGER,
|
21
19
|
duration_total_ms FLOAT,
|
22
20
|
duration_views_ms FLOAT,
|
23
21
|
duration_ar_ms FLOAT,
|
24
22
|
allocations INTEGER,
|
25
|
-
comment TEXT
|
23
|
+
comment TEXT,
|
24
|
+
source_file TEXT,
|
25
|
+
line_number INTEGER
|
26
26
|
)'
|
27
27
|
|
28
28
|
ins = db.prepare("insert into Event(
|
@@ -35,30 +35,34 @@ module LogSense
|
|
35
35
|
url,
|
36
36
|
controller,
|
37
37
|
html_verb,
|
38
|
-
status,
|
38
|
+
status,
|
39
39
|
duration_total_ms,
|
40
40
|
duration_views_ms,
|
41
41
|
duration_ar_ms,
|
42
42
|
allocations,
|
43
|
-
comment
|
43
|
+
comment,
|
44
|
+
source_file,
|
45
|
+
line_number
|
44
46
|
)
|
45
|
-
values (#{Array.new(
|
47
|
+
values (#{Array.new(17, '?').join(', ')})")
|
46
48
|
|
47
|
-
|
48
49
|
db.execute 'CREATE TABLE IF NOT EXISTS Error(
|
49
50
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
50
51
|
log_id TEXT,
|
51
52
|
context TEXT,
|
52
|
-
description TEXT
|
53
|
+
description TEXT,
|
54
|
+
filename TEXT,
|
55
|
+
line_number INTEGER
|
53
56
|
)'
|
54
57
|
|
55
58
|
ins_error = db.prepare("insert into Error(
|
56
59
|
log_id,
|
57
60
|
context,
|
58
|
-
description
|
61
|
+
description,
|
62
|
+
filename,
|
63
|
+
line_number
|
59
64
|
)
|
60
|
-
values (?, ?, ?)")
|
61
|
-
|
65
|
+
values (?, ?, ?, ?, ?)")
|
62
66
|
|
63
67
|
# requests in the log might be interleaved.
|
64
68
|
#
|
@@ -79,94 +83,101 @@ module LogSense
|
|
79
83
|
# and they appears in the order shown above: started, processing, ...
|
80
84
|
#
|
81
85
|
# Different requests might be interleaved, of course
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
data = self.match_and_process_error line
|
88
|
-
if data
|
89
|
-
ins_error.execute(data[:log_id], data[:context], data[:description])
|
90
|
-
next
|
91
|
-
end
|
92
|
-
|
93
|
-
data = self.match_and_process_start line
|
94
|
-
if data
|
95
|
-
id = data[:log_id]
|
96
|
-
pending[id] = data.merge (pending[id] || {})
|
97
|
-
next
|
98
|
-
end
|
99
|
-
|
100
|
-
data = self.match_and_process_processing_by line
|
101
|
-
if data
|
102
|
-
id = data[:log_id]
|
103
|
-
pending[id] = data.merge (pending[id] || {})
|
104
|
-
next
|
105
|
-
end
|
106
|
-
|
107
|
-
data = self.match_and_process_fatal line
|
108
|
-
if data
|
109
|
-
id = data[:log_id]
|
110
|
-
# it might as well be that the first event started before
|
111
|
-
# the log. With this, we make sure we add only events whose
|
112
|
-
# start was logged and parsed
|
113
|
-
if pending[id]
|
114
|
-
event = data.merge (pending[id] || {})
|
86
|
+
#
|
87
|
+
streams.each do |stream|
|
88
|
+
stream.readlines.each_with_index do |line, line_number|
|
89
|
+
filename = stream == $stdin ? "stdin" : stream.path
|
115
90
|
|
116
|
-
|
117
|
-
|
118
|
-
event[:started_at],
|
119
|
-
event[:ended_at],
|
120
|
-
event[:log_id],
|
121
|
-
event[:ip],
|
122
|
-
unique_visitor_id(event),
|
123
|
-
event[:url],
|
124
|
-
event[:controller],
|
125
|
-
event[:html_verb],
|
126
|
-
event[:status],
|
127
|
-
event[:duration_total_ms],
|
128
|
-
event[:duration_views_ms],
|
129
|
-
event[:duration_ar_ms],
|
130
|
-
event[:allocations],
|
131
|
-
event[:comment]
|
132
|
-
)
|
91
|
+
# I and F for completed requests, [ is for error messages
|
92
|
+
next if line[0] != 'I' and line[0] != 'F' and line[0] != '['
|
133
93
|
|
134
|
-
|
94
|
+
data = match_and_process_error line
|
95
|
+
if data
|
96
|
+
ins_error.execute(data[:log_id], data[:context], data[:description], filename, line_number)
|
97
|
+
next
|
98
|
+
end
|
99
|
+
|
100
|
+
data = match_and_process_start line
|
101
|
+
if data
|
102
|
+
id = data[:log_id]
|
103
|
+
pending[id] = data.merge(pending[id] || {})
|
104
|
+
next
|
135
105
|
end
|
136
|
-
end
|
137
|
-
|
138
|
-
data = self.match_and_process_completed line
|
139
|
-
if data
|
140
|
-
id = data[:log_id]
|
141
106
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
107
|
+
data = match_and_process_processing_by line
|
108
|
+
if data
|
109
|
+
id = data[:log_id]
|
110
|
+
pending[id] = data.merge(pending[id] || {})
|
111
|
+
next
|
112
|
+
end
|
147
113
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
event[
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
114
|
+
data = match_and_process_fatal line
|
115
|
+
if data
|
116
|
+
id = data[:log_id]
|
117
|
+
# it might as well be that the first event started before
|
118
|
+
# the log. With this, we make sure we add only events whose
|
119
|
+
# start was logged and parsed
|
120
|
+
if pending[id]
|
121
|
+
event = data.merge(pending[id] || {})
|
122
|
+
|
123
|
+
ins.execute(
|
124
|
+
event[:exit_status],
|
125
|
+
event[:started_at],
|
126
|
+
event[:ended_at],
|
127
|
+
event[:log_id],
|
128
|
+
event[:ip],
|
129
|
+
unique_visitor_id(event),
|
130
|
+
event[:url],
|
131
|
+
event[:controller],
|
132
|
+
event[:html_verb],
|
133
|
+
event[:status],
|
134
|
+
event[:duration_total_ms],
|
135
|
+
event[:duration_views_ms],
|
136
|
+
event[:duration_ar_ms],
|
137
|
+
event[:allocations],
|
138
|
+
event[:comment],
|
139
|
+
filename,
|
140
|
+
line_number
|
141
|
+
)
|
142
|
+
|
143
|
+
pending.delete(id)
|
144
|
+
end
|
145
|
+
end
|
165
146
|
|
166
|
-
|
147
|
+
data = self.match_and_process_completed line
|
148
|
+
if data
|
149
|
+
id = data[:log_id]
|
150
|
+
|
151
|
+
# it might as well be that the first event started before
|
152
|
+
# the log. With this, we make sure we add only events whose
|
153
|
+
# start was logged and parsed
|
154
|
+
if pending[id]
|
155
|
+
event = data.merge (pending[id] || {})
|
156
|
+
|
157
|
+
ins.execute(
|
158
|
+
event[:exit_status],
|
159
|
+
event[:started_at],
|
160
|
+
event[:ended_at],
|
161
|
+
event[:log_id],
|
162
|
+
event[:ip],
|
163
|
+
unique_visitor_id(event),
|
164
|
+
event[:url],
|
165
|
+
event[:controller],
|
166
|
+
event[:html_verb],
|
167
|
+
event[:status],
|
168
|
+
event[:duration_total_ms],
|
169
|
+
event[:duration_views_ms],
|
170
|
+
event[:duration_ar_ms],
|
171
|
+
event[:allocations],
|
172
|
+
event[:comment],
|
173
|
+
filename,
|
174
|
+
line_number
|
175
|
+
)
|
176
|
+
|
177
|
+
pending.delete(id)
|
178
|
+
end
|
167
179
|
end
|
168
180
|
end
|
169
|
-
|
170
181
|
end
|
171
182
|
|
172
183
|
db
|
@@ -226,7 +237,7 @@ module LogSense
|
|
226
237
|
# I, [2021-12-06T14:28:19.736545 #2804090] INFO -- : [34091cb5-3e7b-4042-aaf8-6c6510d3f14c] Completed 500 Internal Server Error in 66ms (ActiveRecord: 8.0ms | Allocations: 24885)
|
227
238
|
COMPLETED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{ID}\] Completed #{STATUS} #{STATUS_IN_WORDS} in (?<total>#{MSECS})ms \((Views: (?<views>#{MSECS})ms \| )?ActiveRecord: (?<arec>#{MSECS})ms( \| Allocations: (?<alloc>[0-9]+))?\)/
|
228
239
|
|
229
|
-
def self.match_and_process_completed
|
240
|
+
def self.match_and_process_completed(line)
|
230
241
|
matchdata = (COMPLETED_REGEXP.match line)
|
231
242
|
# exit_status = matchdata[:status].to_i == 500 ? "E" : "I"
|
232
243
|
if matchdata
|
@@ -267,7 +278,7 @@ module LogSense
|
|
267
278
|
# F, [2021-12-04T00:34:05.839269 #2735058] FATAL -- : [3a16162e-a6a5-435e-a9d8-c4df5dc0f728] actionpack (5.2.4.4) lib/action_dispatch/middleware/debug_exceptions.rb:65:in `call'
|
268
279
|
FATAL_REGEXP = /F, \[#{TIMESTAMP} #[0-9]+\] FATAL -- : \[#{ID}\] (?<comment>.*)$/
|
269
280
|
|
270
|
-
def self.match_and_process_fatal
|
281
|
+
def self.match_and_process_fatal(line)
|
271
282
|
matchdata = FATAL_REGEXP.match line
|
272
283
|
if matchdata
|
273
284
|
{
|
@@ -281,11 +292,8 @@ module LogSense
|
|
281
292
|
end
|
282
293
|
|
283
294
|
# generate a unique visitor id from an event
|
284
|
-
def self.unique_visitor_id
|
295
|
+
def self.unique_visitor_id(event)
|
285
296
|
"#{DateTime.parse(event[:started_at] || event[:ended_at] || "1970-01-01").strftime("%Y-%m-%d")} #{event[:ip]}"
|
286
297
|
end
|
287
|
-
|
288
298
|
end
|
289
|
-
|
290
299
|
end
|
291
|
-
|
@@ -4,10 +4,6 @@
|
|
4
4
|
<th>CLI Command</th>
|
5
5
|
<td><code><%= data[:command] %></code></td>
|
6
6
|
</tr>
|
7
|
-
<tr>
|
8
|
-
<th>Input file</th>
|
9
|
-
<td><code><%= (data[:log_file] || "stdin") %></code></td>
|
10
|
-
</tr>
|
11
7
|
<tr>
|
12
8
|
<th>Ignore crawlers</th>
|
13
9
|
<td><code><%= options[:ignore_crawlers] %></code></td></tr>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<nav>
|
2
|
+
<h2>Navigation</h2>
|
3
|
+
<ul class="no-bullet">
|
4
|
+
<% (["Summary", "Log Structure"] +
|
5
|
+
menus +
|
6
|
+
["Command Invocation", "Performance"]).each do |item| %>
|
7
|
+
<li class="nav-item">
|
8
|
+
<a href="#<%= Emitter::slugify item %>" data-close>
|
9
|
+
<%= item %>
|
10
|
+
</a>
|
11
|
+
</li>
|
12
|
+
<% end %>
|
13
|
+
</ul>
|
14
|
+
|
15
|
+
<p>
|
16
|
+
Generated by
|
17
|
+
<a href="https://github.com/avillafiorita/log_sense">LogSense</a> <br />
|
18
|
+
on <%= DateTime.now.strftime("%Y-%m-%d %H:%M") %>.<br />
|
19
|
+
<a href='https://db-ip.com'>IP Geolocation by DB-IP</a>
|
20
|
+
</p>
|
21
|
+
</nav>
|
@@ -1,9 +1,3 @@
|
|
1
|
-
<%
|
2
|
-
def slugify string
|
3
|
-
string.downcase.gsub(/ +/, '-')
|
4
|
-
end
|
5
|
-
%>
|
6
|
-
|
7
1
|
<table id="table-<%= index %>" class="table unstriped">
|
8
2
|
<thead>
|
9
3
|
<tr>
|
@@ -19,9 +13,10 @@ end
|
|
19
13
|
$(document).ready(function(){
|
20
14
|
$('#table-<%= index %>').dataTable({
|
21
15
|
data: data_<%= index %>,
|
16
|
+
<%= report[:datatable_options] + "," if report[:datatable_options] %>
|
22
17
|
columns: [
|
23
18
|
<% report[:header].each do |header| %>
|
24
|
-
{ data: '<%= header %>', className: '<%= slugify(header) %>' },
|
19
|
+
{ data: '<%= header %>', className: '<%= Emitter::slugify(header) %>' },
|
25
20
|
<% end %>
|
26
21
|
]
|
27
22
|
});
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<%=
|
2
|
+
# shortens long URLs and long descriptions
|
3
|
+
shortened = Emitter::shorten(report[:rows], report[:header], data[:width])
|
4
|
+
|
5
|
+
# build and style the table
|
6
|
+
table = Terminal::Table.new headings: report[:header], rows: shortened
|
7
|
+
table.style = { border_i: "|" }
|
8
|
+
columns = report[:header].size - 1
|
9
|
+
(0..columns).map do |i|
|
10
|
+
table.align_column(i, report[:column_alignment][i] || :left)
|
11
|
+
end
|
12
|
+
# return it
|
13
|
+
table
|
14
|
+
%>
|
@@ -15,7 +15,7 @@
|
|
15
15
|
<%= data[:log_size] %> <span class="stats-list-label">Events</span>
|
16
16
|
</li>
|
17
17
|
<li class="stats-list-positive">
|
18
|
-
|
18
|
+
<%= "%.2f" % (data[:log_size] / data[:duration]) %>
|
19
19
|
<span class="stats-list-label">Parsed Events/sec</span>
|
20
20
|
</li>
|
21
21
|
</ul>
|
@@ -1,9 +1,12 @@
|
|
1
1
|
<%=
|
2
|
-
table = Terminal::Table.new rows: [
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
2
|
+
table = Terminal::Table.new rows: [
|
3
|
+
["Analysis started at", data[:started_at].to_s ],
|
4
|
+
["Analysis ended at", data[:ended_at].to_s ],
|
5
|
+
["Duration", "%02d:%02d" % [data[:duration] / 60, data[:duration] % 60] ],
|
6
|
+
["Events", "%9d" % data[:log_size] ],
|
7
|
+
["Parsed events/sec", "%.2f" % (data[:log_size] / data[:duration]) ]
|
8
|
+
]
|
9
|
+
table.style = { border_i: "|" }
|
7
10
|
table.align_column(2, :right)
|
8
11
|
table
|
9
12
|
%>
|
@@ -1,10 +1,10 @@
|
|
1
1
|
<script>
|
2
|
-
/* this is used both by Vega and DataTable
|
2
|
+
/* this is used both by Vega and DataTable for <%= report[:title] %>*/
|
3
3
|
data_<%= index %> = [
|
4
4
|
<% report[:rows].each do |row| %>
|
5
5
|
{
|
6
6
|
<% report[:header].each_with_index do |h, i| %>
|
7
|
-
"<%= h %>": <%=
|
7
|
+
"<%= h %>": "<%= Emitter::process row[i] %>",
|
8
8
|
<% end %>
|
9
9
|
},
|
10
10
|
<% end %>
|
@@ -17,7 +17,12 @@
|
|
17
17
|
<%= data[:total_unique_visits] %> <span class="stats-list-label">Unique Visits</span>
|
18
18
|
</li>
|
19
19
|
<li class="stats-list-negative">
|
20
|
-
|
20
|
+
<% days = data[:last_day_in_analysis] - data[:first_day_in_analysis] %>
|
21
|
+
<%= days > 0 ? "%d" % (data[:total_unique_visits] / days) : "N/A" %>
|
21
22
|
<span class="stats-list-label">Unique Visits / Day</span>
|
22
23
|
</li>
|
24
|
+
<li class="stats-list-negative">
|
25
|
+
<%= data[:total_unique_visits] != 0 ? data[:total_hits] / data[:total_unique_visits] : "N/A" %>
|
26
|
+
<span class="stats-list-label">Hits / Unique Visitor</span>
|
27
|
+
</li>
|
23
28
|
</ul>
|
@@ -1,10 +1,13 @@
|
|
1
1
|
<%=
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
2
|
+
table = Terminal::Table.new rows: [
|
3
|
+
["From", data[:first_day_in_analysis]],
|
4
|
+
["To", data[:last_day_in_analysis]],
|
5
|
+
["Days", data[:total_days_in_analysis]],
|
6
|
+
["Hits", data[:total_hits]],
|
7
|
+
["Unique Visits", data[:total_unique_visits]],
|
8
|
+
["Unique Visits / Day", data[:total_days_in_analysis] > 0 ? "%d" % (data[:total_unique_visits] / data[:total_days_in_analysis]) : "N/A"],
|
9
|
+
["Hits/Unique Visitor", data[:total_unique_visits] != 0 ? data[:total_hits] / data[:total_unique_visits] : "N/A"]
|
10
|
+
]
|
11
|
+
table.style = { border_i: "|" }
|
12
|
+
table
|
10
13
|
%>
|
@@ -0,0 +1 @@
|
|
1
|
+
>>>> URLs IN THIS FILE ARE NOT SANITIZED AND MIGHT BE UNSAFE TO OPEN <<<<
|