log_sense 1.1.1 → 1.2.2
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 +6 -19
- data/README.org +23 -20
- data/lib/log_sense/rails_data_cruncher.rb +11 -2
- data/lib/log_sense/rails_log_parser.rb +52 -60
- data/lib/log_sense/templates/rails.html.erb +360 -0
- data/lib/log_sense/templates/rails.txt.erb +21 -9
- data/lib/log_sense/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72d2fbdba26d6fa20cef0fc82c16ecd29eeb135ece6178804a9737d6bc95e838
|
4
|
+
data.tar.gz: 5ac8a548f0f2ac3db060ac4375d4756539a5262b95b9aebc0fc496e6e0003caf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0bdecd45808f1257cb7aeaa97d2cf13cf0e62fbde74365c351a2e5440784e99bbfa29271746db66a5dba2bb5e1ae5891aaac4cb982f37ef8072d9e079e3b032a
|
7
|
+
data.tar.gz: d88cca775be4a8828ebaecec757cee87e52338f36784c3a59d6ab8b703328982ecff71233640560f832c4b1f9d7fa4c299a47344d3b79139f518404e02f32380
|
data/CHANGELOG.org
CHANGED
@@ -2,26 +2,13 @@
|
|
2
2
|
#+AUTHOR: Adolfo Villafiorita
|
3
3
|
#+STARTUP: showall
|
4
4
|
|
5
|
-
*
|
5
|
+
* Changes in log_sense 1.1.2
|
6
|
+
<2021-12-17 Fri>
|
6
7
|
|
7
|
-
|
8
|
+
- Added Rails Log HTML output
|
8
9
|
|
9
|
-
|
10
|
+
* Changes in log_sense 1.1.1 and earlier
|
11
|
+
<2021-12-17 Fri>
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
** Documentation
|
14
|
-
|
15
|
-
** Code
|
16
|
-
|
17
|
-
|
18
|
-
* Version 1.0.0
|
19
|
-
|
20
|
-
** New Functions and Changes
|
21
|
-
|
22
|
-
** Fixes
|
23
|
-
|
24
|
-
** Documentation
|
25
|
-
|
26
|
-
** Code
|
13
|
+
- In the Git commit messages (not very informative, I am afraid).
|
27
14
|
|
data/README.org
CHANGED
@@ -14,21 +14,15 @@ and [[https://umami.is/][Umami]], focusing on privacy and data-ownership: the da
|
|
14
14
|
generated by LogSense is stored on your computer and owned by
|
15
15
|
you (like it should be).
|
16
16
|
|
17
|
-
LogSense is also inspired by static websites generators
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
installation headaches.
|
17
|
+
LogSense is also inspired by *static websites generators*: statistics
|
18
|
+
are generated from the command line and accessed as static HTML files.
|
19
|
+
By generating static resources, LogSense significantly reduces the
|
20
|
+
attack surface of your webserver and installation headaches.
|
22
21
|
|
23
22
|
We have, for instance, a cron job running on our servers, generating
|
24
23
|
statistics at night. The generated files are then made available on a
|
25
24
|
private area on the web.
|
26
25
|
|
27
|
-
Statistics are generated from Apache log formats in the =combined=
|
28
|
-
format and from Rails logs. Reports are tailored, but not limited, to
|
29
|
-
web servers serving static websites. No need to install Java Script
|
30
|
-
code on your websites, no cookies installed, no user tracking.
|
31
|
-
|
32
26
|
LogSense reports the following data:
|
33
27
|
|
34
28
|
- Visitors, hits, unique visitors, bandwidth used
|
@@ -62,20 +56,29 @@ LogSense generates HTML, txt (Org Mode), and SQLite outputs.
|
|
62
56
|
|
63
57
|
#+RESULTS:
|
64
58
|
#+begin_example
|
65
|
-
Usage:
|
66
|
-
|
59
|
+
Usage: log_sense [options] [logfile]
|
60
|
+
--title=TITLE Title to use in the report
|
61
|
+
-f, --input-format=FORMAT Input format (either rails or apache)
|
62
|
+
-i, --input-file=INPUT_FILE Input file
|
63
|
+
-t, --output-format=FORMAT Output format: html, org, txt, sqlite. See below for available formats
|
64
|
+
-o, --output-file=OUTPUT_FILE Output file
|
67
65
|
-b, --begin=DATE Consider entries after or on DATE
|
68
66
|
-e, --end=DATE Consider entries before or on DATE
|
69
|
-
-
|
70
|
-
-
|
71
|
-
|
72
|
-
-u, --prefix=PREFIX Prefix to add to all plots (used to run multiple analyses in the same dir)
|
73
|
-
-w, --suffix=SUFFIX Suffix to add to all plots (used to run multiple analyses in the same dir)
|
74
|
-
-c, --code-export=WHAT Control :export directive in Org Mode code blocks (code, results, *both*, none)
|
75
|
-
-f, --format=FORMAT Output format: html, org, sqlite. Defaults to org mode
|
67
|
+
-l, --limit=N Number of entries to show (defaults to 30)
|
68
|
+
-c, --crawlers=POLICY Decide what to do with crawlers (applies to Apache Logs)
|
69
|
+
-n, --no-selfpolls Ignore self poll entries (requests from ::1; applies to Apache Logs)
|
76
70
|
-v, --version Prints version information
|
77
71
|
-h, --help Prints this help
|
78
|
-
|
72
|
+
|
73
|
+
This is version 1.1.1
|
74
|
+
|
75
|
+
Output formats
|
76
|
+
apache parsing can produce the following outputs:
|
77
|
+
- sqlite
|
78
|
+
- html
|
79
|
+
rails parsing can produce the following outputs:
|
80
|
+
- sqlite
|
81
|
+
- txt
|
79
82
|
#+end_example
|
80
83
|
|
81
84
|
* Change Log
|
@@ -26,6 +26,10 @@ module LogSense
|
|
26
26
|
@log_size = db.execute "SELECT count(started_at) from Event"
|
27
27
|
@log_size = @log_size[0][0]
|
28
28
|
|
29
|
+
# TODO: I should make the names of events/size/etc uniform betweeen Apache and Rails Logs
|
30
|
+
# SAME AS ABOVE
|
31
|
+
@total_hits = @log_size
|
32
|
+
|
29
33
|
# SAME AS ABOVE (but log_size is wrong in the case of Rails
|
30
34
|
# logs, since an event takes more than one line)
|
31
35
|
@events = db.execute "SELECT count(started_at) from Event"
|
@@ -88,11 +92,12 @@ module LogSense
|
|
88
92
|
|
89
93
|
@statuses = db.execute "SELECT status, count(status) from Event where #{filter} group by status order by status"
|
90
94
|
|
95
|
+
@by_day_5xx = db.execute "SELECT date(started_at), count(started_at) from Event where substr(status, 1,1) == '5' and #{filter} group by date(started_at)"
|
91
96
|
@by_day_4xx = db.execute "SELECT date(started_at), count(started_at) from Event where substr(status, 1,1) == '4' and #{filter} group by date(started_at)"
|
92
97
|
@by_day_3xx = db.execute "SELECT date(started_at), count(started_at) from Event where substr(status, 1,1) == '3' and #{filter} group by date(started_at)"
|
93
98
|
@by_day_2xx = db.execute "SELECT date(started_at), count(started_at) from Event where substr(status, 1,1) == '2' and #{filter} group by date(started_at)"
|
94
99
|
|
95
|
-
@statuses_by_day = (@by_day_2xx + @by_day_3xx + @by_day_4xx).group_by { |x| x[0] }.to_a.map { |x|
|
100
|
+
@statuses_by_day = (@by_day_2xx + @by_day_3xx + @by_day_4xx + @by_day_5xx).group_by { |x| x[0] }.to_a.map { |x|
|
96
101
|
[x[0], x[1].map { |y| y[1] }].flatten
|
97
102
|
}
|
98
103
|
|
@@ -100,8 +105,12 @@ module LogSense
|
|
100
105
|
|
101
106
|
@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"
|
102
107
|
|
103
|
-
@fatal = db.execute "SELECT strftime(\"%Y-%m-%d %H:%M\", started_at), ip, url, log_id FROM Event WHERE exit_status == 'F'"
|
108
|
+
@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'") || [[]]
|
109
|
+
|
110
|
+
@internal_server_error = (db.execute "SELECT strftime(\"%Y-%m-%d %H:%M\", started_at), status, ip, url, error.description, event.log_id FROM Event JOIN Error ON event.log_id == error.log_id WHERE status is 500") || [[]]
|
104
111
|
|
112
|
+
@error = (db.execute "SELECT log_id, context, description, count(log_id) from Error GROUP BY description") || [[]]
|
113
|
+
|
105
114
|
data = {}
|
106
115
|
self.instance_variables.each do |variable|
|
107
116
|
var_as_symbol = variable.to_s[1..-1].to_sym
|
@@ -24,7 +24,7 @@ module LogSense
|
|
24
24
|
allocations INTEGER,
|
25
25
|
comment TEXT
|
26
26
|
)'
|
27
|
-
|
27
|
+
|
28
28
|
ins = db.prepare("insert into Event(
|
29
29
|
exit_status,
|
30
30
|
started_at,
|
@@ -44,6 +44,22 @@ module LogSense
|
|
44
44
|
)
|
45
45
|
values (#{Array.new(15, '?').join(', ')})")
|
46
46
|
|
47
|
+
|
48
|
+
db.execute 'CREATE TABLE IF NOT EXISTS Error(
|
49
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
50
|
+
log_id TEXT,
|
51
|
+
context TEXT,
|
52
|
+
description TEXT
|
53
|
+
)'
|
54
|
+
|
55
|
+
ins_error = db.prepare("insert into Error(
|
56
|
+
log_id,
|
57
|
+
context,
|
58
|
+
description
|
59
|
+
)
|
60
|
+
values (?, ?, ?)")
|
61
|
+
|
62
|
+
|
47
63
|
# requests in the log might be interleaved.
|
48
64
|
#
|
49
65
|
# We use the 'pending' variable to progressively store data
|
@@ -65,8 +81,14 @@ module LogSense
|
|
65
81
|
# Different requests might be interleaved, of course
|
66
82
|
|
67
83
|
File.readlines(filename).each do |line|
|
68
|
-
#
|
69
|
-
next if line[0] != 'I' and line[0] != 'F'
|
84
|
+
# I and F for completed requests, [ is for error messages
|
85
|
+
next if line[0] != 'I' and line[0] != 'F' and line[0] != '['
|
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
|
70
92
|
|
71
93
|
data = self.match_and_process_start line
|
72
94
|
if data
|
@@ -145,39 +167,6 @@ module LogSense
|
|
145
167
|
end
|
146
168
|
end
|
147
169
|
|
148
|
-
|
149
|
-
data = self.match_and_process_completed_no_alloc line
|
150
|
-
if data
|
151
|
-
id = data[:log_id]
|
152
|
-
|
153
|
-
# it might as well be that the first event started before
|
154
|
-
# the log. With this, we make sure we add only events whose
|
155
|
-
# start was logged and parsed
|
156
|
-
if pending[id]
|
157
|
-
event = data.merge (pending[id] || {})
|
158
|
-
|
159
|
-
ins.execute(
|
160
|
-
event[:exit_status],
|
161
|
-
event[:started_at],
|
162
|
-
event[:ended_at],
|
163
|
-
event[:log_id],
|
164
|
-
event[:ip],
|
165
|
-
"#{DateTime.parse(event[:ended_at]).strftime("%Y-%m-%d")} #{event[:ip]}",
|
166
|
-
event[:url],
|
167
|
-
event[:controller],
|
168
|
-
event[:html_verb],
|
169
|
-
event[:status],
|
170
|
-
event[:duration_total_ms],
|
171
|
-
event[:duration_views_ms],
|
172
|
-
event[:duration_ar_ms],
|
173
|
-
event[:allocations],
|
174
|
-
event[:comment]
|
175
|
-
)
|
176
|
-
|
177
|
-
pending.delete(id)
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
170
|
end
|
182
171
|
|
183
172
|
db
|
@@ -189,8 +178,30 @@ module LogSense
|
|
189
178
|
URL = /(?<url>[^"]+)/
|
190
179
|
IP = /(?<ip>[0-9.]+)/
|
191
180
|
STATUS = /(?<status>[0-9]+)/
|
181
|
+
STATUS_IN_WORDS = /(OK|Unauthorized|Found|Internal Server Error|Bad Request|Method Not Allowed|Request Timeout|Not Implemented|Bad Gateway|Service Unavailable)/
|
192
182
|
MSECS = /[0-9.]+/
|
193
183
|
|
184
|
+
# Error Messages
|
185
|
+
# [584cffcc-f1fd-4b5c-bb8b-b89621bd4921] ActionController::RoutingError (No route matches [GET] "/assets/foundation-icons.svg"):
|
186
|
+
# [fd8df8b5-83c9-48b5-a056-e5026e31bd5e] ActionView::Template::Error (undefined method `all_my_ancestor' for nil:NilClass):
|
187
|
+
# [d17ed55c-f5f1-442a-a9d6-3035ab91adf0] ActionView::Template::Error (undefined method `volunteer_for' for #<DonationsController:0x007f4864c564b8>
|
188
|
+
EXCEPTION = /[A-Za-z_0-9:]+(Error)?/
|
189
|
+
ERROR_REGEXP = /^\[#{ID}\] (?<context>#{EXCEPTION}) \((?<description>(#{EXCEPTION})?.*)\):/
|
190
|
+
|
191
|
+
def self.match_and_process_error line
|
192
|
+
matchdata = ERROR_REGEXP.match line
|
193
|
+
if matchdata
|
194
|
+
{
|
195
|
+
log_id: matchdata[:id],
|
196
|
+
context: matchdata[:context],
|
197
|
+
description: matchdata[:description]
|
198
|
+
}
|
199
|
+
else
|
200
|
+
nil
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
|
194
205
|
# I, [2021-10-19T08:16:34.343858 #10477] INFO -- : [67103c0d-455d-4fe8-951e-87e97628cb66] Started GET "/grow/people/471" for 217.77.80.35 at 2021-10-19 08:16:34 +0000
|
195
206
|
STARTED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{ID}\] Started #{VERB} "#{URL}" for #{IP} at/
|
196
207
|
|
@@ -209,11 +220,15 @@ module LogSense
|
|
209
220
|
end
|
210
221
|
end
|
211
222
|
|
223
|
+
# TODO: Add regexps for the performance data (Views ...). We have three cases (view, active records, allocations), (views, active records), (active records, allocations)
|
212
224
|
# I, [2021-10-19T08:16:34.712331 #10477] INFO -- : [67103c0d-455d-4fe8-951e-87e97628cb66] Completed 200 OK in 367ms (Views: 216.7ms | ActiveRecord: 141.3ms | Allocations: 168792)
|
213
|
-
|
225
|
+
# I, [2021-12-09T16:53:52.657727 #2735058] INFO -- : [0064e403-9eb2-439d-8fe1-a334c86f5532] Completed 200 OK in 13ms (Views: 11.1ms | ActiveRecord: 1.2ms)
|
226
|
+
# 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
|
+
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]+))?\)/
|
214
228
|
|
215
229
|
def self.match_and_process_completed line
|
216
230
|
matchdata = (COMPLETED_REGEXP.match line)
|
231
|
+
# exit_status = matchdata[:status].to_i == 500 ? "E" : "I"
|
217
232
|
if matchdata
|
218
233
|
{
|
219
234
|
exit_status: "I",
|
@@ -231,29 +246,6 @@ module LogSense
|
|
231
246
|
end
|
232
247
|
end
|
233
248
|
|
234
|
-
# I, [2021-12-09T16:53:52.657727 #2735058] INFO -- : [0064e403-9eb2-439d-8fe1-a334c86f5532] Completed 200 OK in 13ms (Views: 11.1ms | ActiveRecord: 1.2ms)
|
235
|
-
COMPLETED_NO_ALLOC_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{ID}\] Completed #{STATUS} [^ ]+ in (?<total>#{MSECS})ms \(Views: (?<views>#{MSECS})ms \| ActiveRecord: (?<arec>#{MSECS})ms\)/
|
236
|
-
|
237
|
-
def self.match_and_process_completed_no_alloc line
|
238
|
-
matchdata = (COMPLETED_NO_ALLOC_REGEXP.match line)
|
239
|
-
if matchdata
|
240
|
-
{
|
241
|
-
exit_status: "I",
|
242
|
-
ended_at: matchdata[:timestamp],
|
243
|
-
log_id: matchdata[:id],
|
244
|
-
status: matchdata[:status],
|
245
|
-
duration_total_ms: matchdata[:total],
|
246
|
-
duration_views_ms: matchdata[:views],
|
247
|
-
duration_ar_ms: matchdata[:arec],
|
248
|
-
allocations: -1,
|
249
|
-
comment: ""
|
250
|
-
}
|
251
|
-
else
|
252
|
-
nil
|
253
|
-
end
|
254
|
-
end
|
255
|
-
|
256
|
-
|
257
249
|
# I, [2021-10-19T08:16:34.345162 #10477] INFO -- : [67103c0d-455d-4fe8-951e-87e97628cb66] Processing by PeopleController#show as HTML
|
258
250
|
PROCESSING_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{ID}\] Processing by (?<controller>[^ ]+) as/
|
259
251
|
|
@@ -0,0 +1,360 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html class="no-js" lang="en">
|
3
|
+
<head>
|
4
|
+
<title><%= options[:title] || "Log Sense: #{data[:log_file]}" %></title>
|
5
|
+
|
6
|
+
<meta charset="utf-8" />
|
7
|
+
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
9
|
+
<meta name="author" content="Log Sense">
|
10
|
+
<meta name="description" content="Analysis of <%= data[:log_file] %>">
|
11
|
+
|
12
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
13
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
14
|
+
<link href="https://fonts.googleapis.com/css2?family=PT+Sans&display=swap" rel="stylesheet">
|
15
|
+
|
16
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/foundicons/3.0.0/foundation-icons.min.css">
|
17
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/foundation-sites@6.7.4/dist/css/foundation.min.css">
|
18
|
+
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/zf/dt-1.11.3/datatables.min.css"/>
|
19
|
+
|
20
|
+
|
21
|
+
<script src="https://cdn.jsdelivr.net/npm/vega@5.21.0"></script>
|
22
|
+
<script src="https://cdn.jsdelivr.net/npm/vega-lite@5.2.0"></script>
|
23
|
+
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6.20.2"></script>
|
24
|
+
|
25
|
+
<style>
|
26
|
+
body {
|
27
|
+
font-family: 'PT Sans', sans-serif;
|
28
|
+
font-size: 80%;
|
29
|
+
}
|
30
|
+
|
31
|
+
#offCanvas {
|
32
|
+
color: white;
|
33
|
+
background: #0D0630;
|
34
|
+
border-right: none;
|
35
|
+
box-shadow: none;
|
36
|
+
padding: 0.5rem;
|
37
|
+
}
|
38
|
+
#offCanvas a {
|
39
|
+
color: #E6F9AF;
|
40
|
+
}
|
41
|
+
|
42
|
+
.contents-button {
|
43
|
+
font-size: xx-large;
|
44
|
+
}
|
45
|
+
|
46
|
+
.main-section {
|
47
|
+
margin-left: 45px;
|
48
|
+
}
|
49
|
+
|
50
|
+
h1 {
|
51
|
+
font-size: 1.8rem;
|
52
|
+
}
|
53
|
+
|
54
|
+
h2 {
|
55
|
+
font-size: 1.2rem;
|
56
|
+
}
|
57
|
+
|
58
|
+
th {
|
59
|
+
padding: 0.2rem 1.2rem 0.2rem 0.2rem !important
|
60
|
+
}
|
61
|
+
|
62
|
+
td {
|
63
|
+
padding: 0.2rem 1rem 0.2rem 0.2rem !important;
|
64
|
+
}
|
65
|
+
|
66
|
+
.hits, .visits, .size, .count, .s2xx, .s3xx, .so4xx, .total-hits, .total-visits {
|
67
|
+
text-align: right !important;
|
68
|
+
}
|
69
|
+
|
70
|
+
.card-divider {
|
71
|
+
padding: 0.2rem 0.4rem 0.2rem 0.4rem;
|
72
|
+
background: #D30001;
|
73
|
+
color: white;
|
74
|
+
}
|
75
|
+
|
76
|
+
input, select {
|
77
|
+
font-size: 0.8rem !important;
|
78
|
+
height: 1.5rem !important;
|
79
|
+
padding: 0.2rem 0.4rem 0.2rem 0.4rem !important;
|
80
|
+
}
|
81
|
+
|
82
|
+
.dataTables_info {
|
83
|
+
font-size: small;
|
84
|
+
color: rgb(202, 202, 202);
|
85
|
+
}
|
86
|
+
|
87
|
+
ul.pagination, li.paginate_button {
|
88
|
+
font-size: small;
|
89
|
+
margin-top: 0px !important;
|
90
|
+
margin-bottom: 0px !important;
|
91
|
+
padding-top: 0px !important;
|
92
|
+
padding-bottom: 0px !important;
|
93
|
+
}
|
94
|
+
|
95
|
+
.stats-list {
|
96
|
+
list-style-type: none;
|
97
|
+
clear: left;
|
98
|
+
margin: 0;
|
99
|
+
padding: 0;
|
100
|
+
text-align: center;
|
101
|
+
margin-bottom: 30px;
|
102
|
+
}
|
103
|
+
|
104
|
+
.stats-list .stats-list-positive {
|
105
|
+
color: #228b22;
|
106
|
+
}
|
107
|
+
|
108
|
+
.stats-list .stats-list-negative {
|
109
|
+
color: #a52a2a;
|
110
|
+
}
|
111
|
+
|
112
|
+
.stats-list > li {
|
113
|
+
display: inline-block;
|
114
|
+
margin-right: 10px;
|
115
|
+
padding-right: 10px;
|
116
|
+
border-right: 1px solid #cacaca;
|
117
|
+
text-align: center;
|
118
|
+
font-size: 1.1em;
|
119
|
+
font-weight: bold;
|
120
|
+
}
|
121
|
+
|
122
|
+
.stats-list > li:last-child {
|
123
|
+
border: none;
|
124
|
+
margin: 0;
|
125
|
+
padding: 0;
|
126
|
+
}
|
127
|
+
|
128
|
+
.stats-list > li .stats-list-label {
|
129
|
+
display: block;
|
130
|
+
margin-top: 2px;
|
131
|
+
font-size: 0.9em;
|
132
|
+
font-weight: normal;
|
133
|
+
}
|
134
|
+
|
135
|
+
#streaks-table .ip {
|
136
|
+
vertical-align: top;
|
137
|
+
}
|
138
|
+
#streaks-table .date {
|
139
|
+
font-weight: bold;
|
140
|
+
}
|
141
|
+
#streaks-table .res-title {
|
142
|
+
font-decoration: underline;
|
143
|
+
}
|
144
|
+
</style>
|
145
|
+
|
146
|
+
</head>
|
147
|
+
|
148
|
+
<body>
|
149
|
+
<div class="off-canvas-wrapper">
|
150
|
+
<div class="off-canvas position-left" id="offCanvas" data-off-canvas>
|
151
|
+
<nav>
|
152
|
+
<h2>Navigation</h2>
|
153
|
+
<ul class="no-bullet">
|
154
|
+
<% [
|
155
|
+
"Summary",
|
156
|
+
"Log Structure",
|
157
|
+
"Daily Distribution",
|
158
|
+
"Time Distribution",
|
159
|
+
"Statuses",
|
160
|
+
"Rails Performance",
|
161
|
+
"Fatal Events",
|
162
|
+
"Internal Server Errrors",
|
163
|
+
"Errors",
|
164
|
+
"IPs",
|
165
|
+
"Command Invocation",
|
166
|
+
"Performance"
|
167
|
+
].each do |item| %>
|
168
|
+
<li class="nav-item">
|
169
|
+
<a href="#<%= item.downcase.gsub(' ', '-') %>" data-close><%= item %></a>
|
170
|
+
</li>
|
171
|
+
<% end %>
|
172
|
+
</ul>
|
173
|
+
|
174
|
+
<p>
|
175
|
+
Generated by
|
176
|
+
<a href="https://github.com/avillafiorita/log_sense">LogSense</a> <br />
|
177
|
+
on <%= DateTime.now.strftime("%Y-%m-%d %H:%M") %>.<br />
|
178
|
+
<a href='https://db-ip.com'>IP Geolocation by DB-IP</a>
|
179
|
+
</p>
|
180
|
+
</nav>
|
181
|
+
</div>
|
182
|
+
<div class="off-canvas-content grid-container grid-x fluid" data-off-canvas-content>
|
183
|
+
<div data-sticky-container>
|
184
|
+
<div class="sticky" data-sticky data-margin-top="0">
|
185
|
+
<div class="contents-button">
|
186
|
+
<i id="hamburger" class="fi-list" data-toggle="offCanvas"></i>
|
187
|
+
</div>
|
188
|
+
</div>
|
189
|
+
</div>
|
190
|
+
|
191
|
+
<section class="main-section">
|
192
|
+
<h1><%= options[:title] || "Log Sense: #{data[:log_file]}" %></h1>
|
193
|
+
|
194
|
+
<p><b>Input File:</b> <%= (data[:log_file] || "stdin") %></p>
|
195
|
+
|
196
|
+
<div class="grid-x grid-margin-x">
|
197
|
+
<article class="card small-12 large-6 cell">
|
198
|
+
<div class="card-divider">
|
199
|
+
<h2 id="summary">Summary</h2>
|
200
|
+
</div>
|
201
|
+
<div class="card-section">
|
202
|
+
<%= render "summary.html.erb", data: data %>
|
203
|
+
</div>
|
204
|
+
</article>
|
205
|
+
|
206
|
+
<article class="card cell small-12 large-6">
|
207
|
+
<div class="card-divider">
|
208
|
+
<h2 id="log-structure">Log Structure</h2>
|
209
|
+
</div>
|
210
|
+
<div class="card-section">
|
211
|
+
<%= render "log_structure.html.erb", data: data %>
|
212
|
+
</div>
|
213
|
+
</article>
|
214
|
+
</div>
|
215
|
+
|
216
|
+
<% @reports = [
|
217
|
+
{ title: "Daily Distribution",
|
218
|
+
header: ["Day", "DOW", "Hits"],
|
219
|
+
rows: data[:daily_distribution],
|
220
|
+
vega_spec: {
|
221
|
+
"mark": {
|
222
|
+
"type": "line",
|
223
|
+
"point": {
|
224
|
+
"filled": false,
|
225
|
+
"fill": "white"
|
226
|
+
}
|
227
|
+
},
|
228
|
+
"encoding": {
|
229
|
+
"x": {"field": "Day", "type": "temporal"},
|
230
|
+
"y": {"field": "Hits", "type": "quantitative"}
|
231
|
+
}
|
232
|
+
}
|
233
|
+
},
|
234
|
+
{ title: "Time Distribution",
|
235
|
+
header: ["Hour", "Hits"],
|
236
|
+
rows: data[:time_distribution],
|
237
|
+
vega_spec: {
|
238
|
+
"mark": "bar",
|
239
|
+
"encoding": {
|
240
|
+
"x": {"field": "Hour", "type": "nominal"},
|
241
|
+
"y": {"field": "Hits", "type": "quantitative"}
|
242
|
+
}
|
243
|
+
}
|
244
|
+
},
|
245
|
+
{ title: "Statuses",
|
246
|
+
header: ["Status", "Count"],
|
247
|
+
rows: data[:statuses],
|
248
|
+
vega_spec: {
|
249
|
+
"mark": "bar",
|
250
|
+
"encoding": {
|
251
|
+
"x": {"field": "Status", "type": "nominal"},
|
252
|
+
"y": {"field": "Count", "type": "quantitative"}
|
253
|
+
}
|
254
|
+
}
|
255
|
+
},
|
256
|
+
{ title: "Rails Performance",
|
257
|
+
header: ['Controller', 'Hits', 'Min', 'Avg', 'Max'],
|
258
|
+
rows: @data[:performance],
|
259
|
+
vega_spec: {
|
260
|
+
"mark": "point",
|
261
|
+
"encoding": {
|
262
|
+
"x": {"field": "Avg", "type": "quantitative"},
|
263
|
+
"y": {"field": "Hits", "type": "quantitative"}
|
264
|
+
},
|
265
|
+
}
|
266
|
+
},
|
267
|
+
{ title: "Fatal Events",
|
268
|
+
header: ['Date', 'IP', 'URL', 'Description', 'Log ID'], rows: @data[:fatal],
|
269
|
+
col: "small-12 cell"
|
270
|
+
},
|
271
|
+
{ title: "Internal Server Errors",
|
272
|
+
header: ['Date', 'Status', 'IP', 'URL', 'Description', 'Log ID'], rows: @data[:internal_server_error],
|
273
|
+
col: "small-12 cell"
|
274
|
+
},
|
275
|
+
{ title: "Errors",
|
276
|
+
header: ['Log ID', 'Context', 'Description', 'Count'], rows: @data[:error],
|
277
|
+
col: "small-12 cell"
|
278
|
+
},
|
279
|
+
{ title: "IPs",
|
280
|
+
header: ["IPs", "Hits", "Country"],
|
281
|
+
rows: data[:ips]
|
282
|
+
},
|
283
|
+
]
|
284
|
+
%>
|
285
|
+
<div class="grid-x grid-margin-x">
|
286
|
+
<% @reports.each_with_index do |report, index| %>
|
287
|
+
<article class="card cell <%= report[:col] || "small-12 large-6" %>" >
|
288
|
+
<div class="card-divider">
|
289
|
+
<h2>
|
290
|
+
<%= report[:title] %>
|
291
|
+
</h2>
|
292
|
+
</div>
|
293
|
+
|
294
|
+
<% if report[:vega_spec] %>
|
295
|
+
<div id="<%= "plot-#{index}" %>"></div>
|
296
|
+
<script>
|
297
|
+
plot_spec_<%= index %> = Object.assign(
|
298
|
+
<%= report[:vega_spec].to_json %>,
|
299
|
+
{ "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
300
|
+
width: "container",
|
301
|
+
description: "<%= report[:title] %>",
|
302
|
+
data: {
|
303
|
+
values: [
|
304
|
+
<% report[:rows].each do |row| %>
|
305
|
+
{
|
306
|
+
<% report[:header].each_with_index do |h, i| %>
|
307
|
+
"<%= h %>": <%= (row[i].class == Integer or row[i].class == Float) ? row[i] : "\"#{row[i]}\"" %>,
|
308
|
+
<% end %>
|
309
|
+
},
|
310
|
+
<% end %>
|
311
|
+
]
|
312
|
+
},
|
313
|
+
});
|
314
|
+
vegaEmbed('#<%= "plot-#{index}"%>', plot_spec_<%= index %>);
|
315
|
+
</script>
|
316
|
+
<% end %>
|
317
|
+
<div class="card-section">
|
318
|
+
<%= render "output_table.html.erb", report %>
|
319
|
+
</div>
|
320
|
+
</article>
|
321
|
+
<% end %>
|
322
|
+
</div>
|
323
|
+
|
324
|
+
<div class="grid-x grid-margin-x">
|
325
|
+
<div class="cell small-12 large-6">
|
326
|
+
<article>
|
327
|
+
<h2 id="command-invocation">Command Invocation</h2>
|
328
|
+
|
329
|
+
<%= render "command_invocation.html.erb", data: data, options: options %>
|
330
|
+
</article>
|
331
|
+
</div>
|
332
|
+
|
333
|
+
<div class="small-12 large-6 cell">
|
334
|
+
<article>
|
335
|
+
<h2 id="performance"> Performance</h2>
|
336
|
+
|
337
|
+
<%= render "performance.html.erb", data: data %>
|
338
|
+
</article>
|
339
|
+
</div>
|
340
|
+
</div>
|
341
|
+
</section>
|
342
|
+
</div>
|
343
|
+
|
344
|
+
<script type="text/javascript" src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
345
|
+
<script type="text/javascript" src="js/vendor/what-input.js"></script>
|
346
|
+
<script type="text/javascript" src="https://cdn.datatables.net/v/zf/dt-1.11.3/datatables.min.js"></script>
|
347
|
+
<script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
|
348
|
+
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/foundation-sites@6.7.4/dist/js/foundation.min.js" crossorigin="anonymous"></script>
|
349
|
+
<script>
|
350
|
+
$(document).foundation();
|
351
|
+
|
352
|
+
$(document).ready(function () {
|
353
|
+
$('.data-table').each(function () {
|
354
|
+
$(this).DataTable();
|
355
|
+
});
|
356
|
+
});
|
357
|
+
</script>
|
358
|
+
</div>
|
359
|
+
</body>
|
360
|
+
</html>
|
@@ -26,14 +26,6 @@ table.align_column(2, :right)
|
|
26
26
|
table
|
27
27
|
%>
|
28
28
|
|
29
|
-
** IP and Country
|
30
|
-
|
31
|
-
<%=
|
32
|
-
table = Terminal::Table.new headings: ['IP', 'Events', 'Country'], rows: @data[:ips]
|
33
|
-
table.align_column(1, :right)
|
34
|
-
table
|
35
|
-
%>
|
36
|
-
|
37
29
|
** Rails Performance
|
38
30
|
|
39
31
|
<%= table = Terminal::Table.new headings: ['Controller', 'Hits', 'Min', 'Avg', 'Max'], rows: @data[:performance]
|
@@ -46,7 +38,27 @@ table
|
|
46
38
|
|
47
39
|
** Fatal Events
|
48
40
|
|
49
|
-
<%= table = Terminal::Table.new headings: ['Date', 'IP', 'URL', 'Log ID'], rows: @data[:fatal]
|
41
|
+
<%= table = Terminal::Table.new headings: ['Date', 'IP', 'URL', 'Description', 'Log ID'], rows: @data[:fatal]
|
42
|
+
table
|
43
|
+
%>
|
44
|
+
|
45
|
+
** Internal Server Errors
|
46
|
+
|
47
|
+
<%= table = Terminal::Table.new headings: ['Date', 'Status', 'IP', 'URL', 'Description', 'Log ID'], rows: @data[:internal_server_error]
|
48
|
+
table
|
49
|
+
%>
|
50
|
+
|
51
|
+
** Errors
|
52
|
+
|
53
|
+
<%= table = Terminal::Table.new headings: ['Log ID', 'Context', 'Description', 'Count'], rows: @data[:error]
|
54
|
+
table
|
55
|
+
%>
|
56
|
+
|
57
|
+
** IPs
|
58
|
+
|
59
|
+
<%=
|
60
|
+
table = Terminal::Table.new headings: ['IP', 'Hits', 'Country'], rows: @data[:ips]
|
61
|
+
table.align_column(1, :right)
|
50
62
|
table
|
51
63
|
%>
|
52
64
|
|
data/lib/log_sense/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: log_sense
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adolfo Villafiorita
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-12-
|
11
|
+
date: 2021-12-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: apache_log-parser
|
@@ -159,6 +159,7 @@ files:
|
|
159
159
|
- lib/log_sense/templates/_summary.html.erb
|
160
160
|
- lib/log_sense/templates/_summary.txt.erb
|
161
161
|
- lib/log_sense/templates/apache.html.erb
|
162
|
+
- lib/log_sense/templates/rails.html.erb
|
162
163
|
- lib/log_sense/templates/rails.txt.erb
|
163
164
|
- lib/log_sense/version.rb
|
164
165
|
- log_sense.gemspec
|