log_sense 1.6.1 → 1.7.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 +12 -0
- data/Gemfile.lock +20 -14
- data/Rakefile +9 -10
- data/exe/log_sense +36 -19
- data/ip_locations/dbip-country-lite.sqlite3 +0 -0
- data/lib/log_sense/apache/log_line_parser.rb +59 -0
- data/lib/log_sense/apache/log_parser.rb +101 -0
- data/lib/log_sense/emitter.rb +29 -23
- data/lib/log_sense/ip_locator.rb +8 -5
- data/lib/log_sense/options/checker.rb +26 -0
- data/lib/log_sense/options/parser.rb +170 -0
- data/lib/log_sense/rails/log_parser.rb +409 -0
- data/lib/log_sense/rails_report_shaper.rb +1 -1
- data/lib/log_sense/templates/_cdn_links.html.erb +0 -4
- data/lib/log_sense/templates/_log_structure.html.erb +2 -2
- data/lib/log_sense/templates/_rails.css.erb +3 -2
- data/lib/log_sense/templates/_report_data.html.erb +1 -1
- data/lib/log_sense/templates/_stylesheet.css +21 -19
- data/lib/log_sense/templates/_summary.html.erb +2 -2
- data/lib/log_sense/templates/report_html.erb +1 -0
- data/lib/log_sense/version.rb +1 -1
- data/lib/log_sense.rb +4 -4
- metadata +8 -8
- data/lib/log_sense/apache_log_line_parser.rb +0 -57
- data/lib/log_sense/apache_log_parser.rb +0 -100
- data/lib/log_sense/options_checker.rb +0 -24
- data/lib/log_sense/options_parser.rb +0 -147
- data/lib/log_sense/rails_log_parser.rb +0 -313
@@ -1,313 +0,0 @@
|
|
1
|
-
require "sqlite3"
|
2
|
-
|
3
|
-
module LogSense
|
4
|
-
#
|
5
|
-
# parse a Rails log file and return a SQLite3 DB
|
6
|
-
#
|
7
|
-
class RailsLogParser
|
8
|
-
#
|
9
|
-
# Tell users which format I can parse
|
10
|
-
#
|
11
|
-
def provide
|
12
|
-
[:rails]
|
13
|
-
end
|
14
|
-
|
15
|
-
def parse(streams, options = {})
|
16
|
-
db = SQLite3::Database.new ":memory:"
|
17
|
-
|
18
|
-
db.execute <<-EOS
|
19
|
-
CREATE TABLE IF NOT EXISTS Event(
|
20
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
21
|
-
exit_status TEXT,
|
22
|
-
started_at TEXT,
|
23
|
-
ended_at TEXT,
|
24
|
-
log_id TEXT,
|
25
|
-
ip TEXT,
|
26
|
-
unique_visitor TEXT,
|
27
|
-
url TEXT,
|
28
|
-
controller TEXT,
|
29
|
-
html_verb TEXT,
|
30
|
-
status INTEGER,
|
31
|
-
duration_total_ms FLOAT,
|
32
|
-
duration_views_ms FLOAT,
|
33
|
-
duration_ar_ms FLOAT,
|
34
|
-
allocations INTEGER,
|
35
|
-
comment TEXT,
|
36
|
-
source_file TEXT,
|
37
|
-
line_number INTEGER
|
38
|
-
)
|
39
|
-
EOS
|
40
|
-
|
41
|
-
ins = db.prepare <<-EOS
|
42
|
-
insert into Event(
|
43
|
-
exit_status,
|
44
|
-
started_at,
|
45
|
-
ended_at,
|
46
|
-
log_id,
|
47
|
-
ip,
|
48
|
-
unique_visitor,
|
49
|
-
url,
|
50
|
-
controller,
|
51
|
-
html_verb,
|
52
|
-
status,
|
53
|
-
duration_total_ms,
|
54
|
-
duration_views_ms,
|
55
|
-
duration_ar_ms,
|
56
|
-
allocations,
|
57
|
-
comment,
|
58
|
-
source_file,
|
59
|
-
line_number
|
60
|
-
)
|
61
|
-
values (#{Array.new(17, "?").join(", ")})
|
62
|
-
EOS
|
63
|
-
|
64
|
-
db.execute <<-EOS
|
65
|
-
CREATE TABLE IF NOT EXISTS Error(
|
66
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
67
|
-
log_id TEXT,
|
68
|
-
context TEXT,
|
69
|
-
description TEXT,
|
70
|
-
filename TEXT,
|
71
|
-
line_number INTEGER
|
72
|
-
)
|
73
|
-
EOS
|
74
|
-
|
75
|
-
ins_error = db.prepare <<-EOS
|
76
|
-
insert into Error(
|
77
|
-
log_id,
|
78
|
-
context,
|
79
|
-
description,
|
80
|
-
filename,
|
81
|
-
line_number
|
82
|
-
)
|
83
|
-
values (?, ?, ?, ?, ?)
|
84
|
-
EOS
|
85
|
-
|
86
|
-
# requests in the log might be interleaved.
|
87
|
-
#
|
88
|
-
# We use the 'pending' variable to progressively store data
|
89
|
-
# about requests till they are completed; whey they are
|
90
|
-
# complete, we enter the entry in the DB and remove it from the
|
91
|
-
# hash
|
92
|
-
pending = {}
|
93
|
-
|
94
|
-
# Log lines are either one of:
|
95
|
-
#
|
96
|
-
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Started VERB "URL" for IP at TIMESTAMP
|
97
|
-
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Processing by CONTROLLER as FORMAT
|
98
|
-
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Parameters: JSON
|
99
|
-
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Rendered VIEW within LAYOUT (Duration: DURATION | Allocations: ALLOCATIONS)
|
100
|
-
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Completed STATUS STATUS_STRING in DURATION (Views: DURATION | ActiveRecord: DURATION | Allocations: NUMBER)
|
101
|
-
#
|
102
|
-
# and they appears in the order shown above: started, processing, ...
|
103
|
-
#
|
104
|
-
# Different requests might be interleaved, of course
|
105
|
-
#
|
106
|
-
streams.each do |stream|
|
107
|
-
stream.readlines.each_with_index do |line, line_number|
|
108
|
-
filename = stream == $stdin ? "stdin" : stream.path
|
109
|
-
|
110
|
-
# I and F for completed requests, [ is for error messages
|
111
|
-
next if line[0] != 'I' and line[0] != 'F' and line[0] != '['
|
112
|
-
|
113
|
-
data = match_and_process_error line
|
114
|
-
if data
|
115
|
-
ins_error.execute(data[:log_id],
|
116
|
-
data[:context],
|
117
|
-
data[:description],
|
118
|
-
filename,
|
119
|
-
line_number)
|
120
|
-
next
|
121
|
-
end
|
122
|
-
|
123
|
-
data = match_and_process_start line
|
124
|
-
if data
|
125
|
-
id = data[:log_id]
|
126
|
-
pending[id] = data.merge(pending[id] || {})
|
127
|
-
next
|
128
|
-
end
|
129
|
-
|
130
|
-
data = match_and_process_processing_by line
|
131
|
-
if data
|
132
|
-
id = data[:log_id]
|
133
|
-
pending[id] = data.merge(pending[id] || {})
|
134
|
-
next
|
135
|
-
end
|
136
|
-
|
137
|
-
data = match_and_process_fatal line
|
138
|
-
if data
|
139
|
-
id = data[:log_id]
|
140
|
-
# it might as well be that the first event started before
|
141
|
-
# the log. With this, we make sure we add only events whose
|
142
|
-
# start was logged and parsed
|
143
|
-
if pending[id]
|
144
|
-
event = data.merge(pending[id] || {})
|
145
|
-
|
146
|
-
ins.execute(
|
147
|
-
event[:exit_status],
|
148
|
-
event[:started_at],
|
149
|
-
event[:ended_at],
|
150
|
-
event[:log_id],
|
151
|
-
event[:ip],
|
152
|
-
unique_visitor_id(event),
|
153
|
-
event[:url],
|
154
|
-
event[:controller],
|
155
|
-
event[:html_verb],
|
156
|
-
event[:status],
|
157
|
-
event[:duration_total_ms],
|
158
|
-
event[:duration_views_ms],
|
159
|
-
event[:duration_ar_ms],
|
160
|
-
event[:allocations],
|
161
|
-
event[:comment],
|
162
|
-
filename,
|
163
|
-
line_number
|
164
|
-
)
|
165
|
-
|
166
|
-
pending.delete(id)
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
data = self.match_and_process_completed line
|
171
|
-
if data
|
172
|
-
id = data[:log_id]
|
173
|
-
|
174
|
-
# it might as well be that the first event started before
|
175
|
-
# the log. With this, we make sure we add only events whose
|
176
|
-
# start was logged and parsed
|
177
|
-
if pending[id]
|
178
|
-
event = data.merge (pending[id] || {})
|
179
|
-
|
180
|
-
ins.execute(
|
181
|
-
event[:exit_status],
|
182
|
-
event[:started_at],
|
183
|
-
event[:ended_at],
|
184
|
-
event[:log_id],
|
185
|
-
event[:ip],
|
186
|
-
unique_visitor_id(event),
|
187
|
-
event[:url],
|
188
|
-
event[:controller],
|
189
|
-
event[:html_verb],
|
190
|
-
event[:status],
|
191
|
-
event[:duration_total_ms],
|
192
|
-
event[:duration_views_ms],
|
193
|
-
event[:duration_ar_ms],
|
194
|
-
event[:allocations],
|
195
|
-
event[:comment],
|
196
|
-
filename,
|
197
|
-
line_number
|
198
|
-
)
|
199
|
-
|
200
|
-
pending.delete(id)
|
201
|
-
end
|
202
|
-
end
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
db
|
207
|
-
end
|
208
|
-
|
209
|
-
TIMESTAMP = /(?<timestamp>[^ ]+)/
|
210
|
-
ID = /(?<id>[a-z0-9-]+)/
|
211
|
-
VERB = /(?<verb>GET|POST|PATCH|PUT|DELETE)/
|
212
|
-
URL = /(?<url>[^"]+)/
|
213
|
-
IP = /(?<ip>[0-9.]+)/
|
214
|
-
STATUS = /(?<status>[0-9]+)/
|
215
|
-
STATUS_IN_WORDS = /(OK|Unauthorized|Found|Internal Server Error|Bad Request|Method Not Allowed|Request Timeout|Not Implemented|Bad Gateway|Service Unavailable)/
|
216
|
-
MSECS = /[0-9.]+/
|
217
|
-
|
218
|
-
# Error Messages
|
219
|
-
# [584cffcc-f1fd-4b5c-bb8b-b89621bd4921] ActionController::RoutingError (No route matches [GET] "/assets/foundation-icons.svg"):
|
220
|
-
# [fd8df8b5-83c9-48b5-a056-e5026e31bd5e] ActionView::Template::Error (undefined method `all_my_ancestor' for nil:NilClass):
|
221
|
-
# [d17ed55c-f5f1-442a-a9d6-3035ab91adf0] ActionView::Template::Error (undefined method `volunteer_for' for #<DonationsController:0x007f4864c564b8>
|
222
|
-
EXCEPTION = /[A-Za-z_0-9:]+(Error)?/
|
223
|
-
ERROR_REGEXP = /^\[#{ID}\] (?<context>#{EXCEPTION}) \((?<description>(#{EXCEPTION})?.*)\):/
|
224
|
-
|
225
|
-
def match_and_process_error line
|
226
|
-
matchdata = ERROR_REGEXP.match line
|
227
|
-
if matchdata
|
228
|
-
{
|
229
|
-
log_id: matchdata[:id],
|
230
|
-
context: matchdata[:context],
|
231
|
-
description: matchdata[:description]
|
232
|
-
}
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
|
-
# 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
|
237
|
-
STARTED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{ID}\] Started #{VERB} "#{URL}" for #{IP} at/
|
238
|
-
|
239
|
-
def match_and_process_start line
|
240
|
-
matchdata = STARTED_REGEXP.match line
|
241
|
-
if matchdata
|
242
|
-
{
|
243
|
-
started_at: matchdata[:timestamp],
|
244
|
-
log_id: matchdata[:id],
|
245
|
-
html_verb: matchdata[:verb],
|
246
|
-
url: matchdata[:url],
|
247
|
-
ip: matchdata[:ip]
|
248
|
-
}
|
249
|
-
end
|
250
|
-
end
|
251
|
-
|
252
|
-
# TODO: Add regexps for the performance data (Views ...). We have three cases (view, active records, allocations), (views, active records), (active records, allocations)
|
253
|
-
# 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)
|
254
|
-
# 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)
|
255
|
-
# 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)
|
256
|
-
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]+))?\)/
|
257
|
-
|
258
|
-
def match_and_process_completed(line)
|
259
|
-
matchdata = (COMPLETED_REGEXP.match line)
|
260
|
-
# exit_status = matchdata[:status].to_i == 500 ? "E" : "I"
|
261
|
-
if matchdata
|
262
|
-
{
|
263
|
-
exit_status: "I",
|
264
|
-
ended_at: matchdata[:timestamp],
|
265
|
-
log_id: matchdata[:id],
|
266
|
-
status: matchdata[:status],
|
267
|
-
duration_total_ms: matchdata[:total],
|
268
|
-
duration_views_ms: matchdata[:views],
|
269
|
-
duration_ar_ms: matchdata[:arec],
|
270
|
-
allocations: matchdata[:alloc],
|
271
|
-
comment: ""
|
272
|
-
}
|
273
|
-
end
|
274
|
-
end
|
275
|
-
|
276
|
-
# I, [2021-10-19T08:16:34.345162 #10477] INFO -- : [67103c0d-455d-4fe8-951e-87e97628cb66] Processing by PeopleController#show as HTML
|
277
|
-
PROCESSING_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{ID}\] Processing by (?<controller>[^ ]+) as/
|
278
|
-
|
279
|
-
def match_and_process_processing_by line
|
280
|
-
matchdata = PROCESSING_REGEXP.match line
|
281
|
-
if matchdata
|
282
|
-
{
|
283
|
-
log_id: matchdata[:id],
|
284
|
-
controller: matchdata[:controller]
|
285
|
-
}
|
286
|
-
end
|
287
|
-
end
|
288
|
-
|
289
|
-
# F, [2021-12-04T00:34:05.838973 #2735058] FATAL -- : [3a16162e-a6a5-435e-a9d8-c4df5dc0f728]
|
290
|
-
# F, [2021-12-04T00:34:05.839157 #2735058] FATAL -- : [3a16162e-a6a5-435e-a9d8-c4df5dc0f728] ActionController::RoutingError (No route matches [GET] "/wp/wp-includes/wlwmanifest.xml"):
|
291
|
-
# F, [2021-12-04T00:34:05.839209 #2735058] FATAL -- : [3a16162e-a6a5-435e-a9d8-c4df5dc0f728]
|
292
|
-
# 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'
|
293
|
-
FATAL_REGEXP = /F, \[#{TIMESTAMP} #[0-9]+\] FATAL -- : \[#{ID}\] (?<comment>.*)$/
|
294
|
-
|
295
|
-
def match_and_process_fatal(line)
|
296
|
-
matchdata = FATAL_REGEXP.match line
|
297
|
-
if matchdata
|
298
|
-
{
|
299
|
-
exit_status: "F",
|
300
|
-
log_id: matchdata[:id],
|
301
|
-
comment: matchdata[:comment]
|
302
|
-
}
|
303
|
-
end
|
304
|
-
end
|
305
|
-
|
306
|
-
# generate a unique visitor id from an event
|
307
|
-
def unique_visitor_id(event)
|
308
|
-
date = event[:started_at] || event[:ended_at] || "1970-01-01"
|
309
|
-
"#{DateTime.parse(date).strftime("%Y-%m-%d")} #{event[:ip]}"
|
310
|
-
end
|
311
|
-
end
|
312
|
-
end
|
313
|
-
|