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
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require "optparse/date"
|
5
|
+
require "log_sense/version"
|
6
|
+
require "log_sense/options/checker"
|
7
|
+
|
8
|
+
module LogSense
|
9
|
+
#
|
10
|
+
# Parse command line options
|
11
|
+
#
|
12
|
+
module Options
|
13
|
+
module Parser
|
14
|
+
#
|
15
|
+
# parse command line options
|
16
|
+
#
|
17
|
+
def self.parse(options)
|
18
|
+
# Defaults
|
19
|
+
args = {
|
20
|
+
geolocation: true,
|
21
|
+
ignore_crawlers: false,
|
22
|
+
input_filenames: [],
|
23
|
+
input_format: "apache",
|
24
|
+
limit: 100,
|
25
|
+
no_selfpoll: false,
|
26
|
+
only_crawlers: false,
|
27
|
+
output_format: "html",
|
28
|
+
pattern: "php",
|
29
|
+
verbose: false,
|
30
|
+
}
|
31
|
+
|
32
|
+
opt_parser = OptionParser.new do |opts|
|
33
|
+
opts.banner = "Usage: log_sense [options] [logfile ...]"
|
34
|
+
|
35
|
+
opts.on(
|
36
|
+
"-tTITLE", "--title=TITLE",
|
37
|
+
String,
|
38
|
+
"Title to use in the report") do |optval|
|
39
|
+
args[:title] = optval
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on(
|
43
|
+
"-fFORMAT", "--input-format=FORMAT",
|
44
|
+
String,
|
45
|
+
"Log format (stored in log or sqlite3): rails or apache #{dft(args[:input_format])}") do |optval|
|
46
|
+
args[:input_format] = optval
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on(
|
50
|
+
"-iFORMAT", "--input-files=file,file,",
|
51
|
+
Array,
|
52
|
+
"Input file(s), log file or sqlite3 (can also be passed as arguments)") do |optval|
|
53
|
+
args[:input_filenames] = optval
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on(
|
57
|
+
"-tFORMAT", "--output-format=FORMAT",
|
58
|
+
String,
|
59
|
+
"Output format: html, txt, sqlite, ufw #{dft(args[:output_format])}") do |optval|
|
60
|
+
args[:output_format] = optval
|
61
|
+
end
|
62
|
+
|
63
|
+
opts.on(
|
64
|
+
"-oOUTPUT_FILE", "--output-file=OUTPUT_FILE",
|
65
|
+
String,
|
66
|
+
"Output file. #{dft('STDOUT')}") do |n|
|
67
|
+
args[:output_filename] = n
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on(
|
71
|
+
"-bDATE", "--begin=DATE",
|
72
|
+
Date,
|
73
|
+
"Consider only entries after or on DATE") do |optval|
|
74
|
+
args[:from_date] = optval
|
75
|
+
end
|
76
|
+
|
77
|
+
opts.on(
|
78
|
+
"-eDATE", "--end=DATE",
|
79
|
+
Date,
|
80
|
+
"Consider only entries before or on DATE") do |optval|
|
81
|
+
args[:to_date] = optval
|
82
|
+
end
|
83
|
+
|
84
|
+
opts.on(
|
85
|
+
"-lN", "--limit=N",
|
86
|
+
Integer,
|
87
|
+
"Limit to the N most requested resources #{dft(args[:limit])}") do |optval|
|
88
|
+
args[:limit] = optval
|
89
|
+
end
|
90
|
+
|
91
|
+
opts.on(
|
92
|
+
"-wWIDTH", "--width=WIDTH",
|
93
|
+
Integer,
|
94
|
+
"Maximum width of long columns in textual reports") do |optval|
|
95
|
+
args[:width] = optval
|
96
|
+
end
|
97
|
+
|
98
|
+
opts.on(
|
99
|
+
"-rROWS", "--rows=ROWS",
|
100
|
+
Integer,
|
101
|
+
"Maximum number of rows for columns with multiple entries in textual reports") do |optval|
|
102
|
+
args[:inner_rows] = optval
|
103
|
+
end
|
104
|
+
|
105
|
+
opts.on(
|
106
|
+
"-pPATTERN", "--pattern=PATTERN",
|
107
|
+
String,
|
108
|
+
"Pattern to use with ufw report to select IP to blacklist #{dft(args[:pattern])}") do |optval|
|
109
|
+
args[:pattern] = optval
|
110
|
+
end
|
111
|
+
|
112
|
+
opts.on("-cPOLICY", "--crawlers=POLICY",
|
113
|
+
String,
|
114
|
+
"Decide what to do with crawlers (applies to Apache Logs)") do |optval|
|
115
|
+
case optval
|
116
|
+
when "only"
|
117
|
+
args[:only_crawlers] = true
|
118
|
+
when "ignore"
|
119
|
+
args[:ignore_crawlers] = true
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
opts.on(
|
124
|
+
"--no-selfpoll",
|
125
|
+
"Ignore self poll entries (requests from ::1; applies to Apache Logs) #{dft(args[:no_selfpoll])}") do
|
126
|
+
args[:no_selfpoll] = true
|
127
|
+
end
|
128
|
+
|
129
|
+
opts.on("--no-geo",
|
130
|
+
"Do not geolocate entries #{dft(args[:geolocation])}") do
|
131
|
+
args[:geolocation] = false
|
132
|
+
end
|
133
|
+
|
134
|
+
opts.on(
|
135
|
+
"--verbose",
|
136
|
+
"Inform about progress (output to STDERR) #{dft(args[:verbose])}") do
|
137
|
+
args[:verbose] = true
|
138
|
+
end
|
139
|
+
|
140
|
+
opts.on("-v", "--version", "Prints version information") do
|
141
|
+
puts "log_sense version #{LogSense::VERSION}"
|
142
|
+
puts "Copyright (C) 2021-2024 Shair.Tech"
|
143
|
+
puts "Distributed under the terms of the MIT license"
|
144
|
+
exit
|
145
|
+
end
|
146
|
+
|
147
|
+
opts.on("-h", "--help", "Prints this help") do
|
148
|
+
puts opts
|
149
|
+
puts
|
150
|
+
puts "This is version #{LogSense::VERSION}"
|
151
|
+
puts
|
152
|
+
puts "Output formats:"
|
153
|
+
puts
|
154
|
+
puts Options::Checker.chains_to_s
|
155
|
+
|
156
|
+
exit 0
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
opt_parser.parse!(options)
|
161
|
+
|
162
|
+
args
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.dft(value)
|
166
|
+
"(DEFAULT: #{value})"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,409 @@
|
|
1
|
+
require "sqlite3"
|
2
|
+
|
3
|
+
module LogSense
|
4
|
+
module Rails
|
5
|
+
#
|
6
|
+
# parse a Rails log file and return a in-memory SQLite3 DB
|
7
|
+
#
|
8
|
+
class LogParser
|
9
|
+
#
|
10
|
+
# Tell users which format I can parse
|
11
|
+
#
|
12
|
+
def provide
|
13
|
+
[:rails]
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse(streams, options = {})
|
17
|
+
db = SQLite3::Database.new ":memory:"
|
18
|
+
|
19
|
+
db.execute <<-EOS
|
20
|
+
CREATE TABLE IF NOT EXISTS Event(
|
21
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
22
|
+
exit_status TEXT,
|
23
|
+
started_at TEXT,
|
24
|
+
ended_at TEXT,
|
25
|
+
log_id TEXT,
|
26
|
+
ip TEXT,
|
27
|
+
unique_visitor TEXT,
|
28
|
+
url TEXT,
|
29
|
+
controller TEXT,
|
30
|
+
html_verb TEXT,
|
31
|
+
status INTEGER,
|
32
|
+
duration_total_ms FLOAT,
|
33
|
+
duration_views_ms FLOAT,
|
34
|
+
duration_ar_ms FLOAT,
|
35
|
+
allocations INTEGER,
|
36
|
+
comment TEXT,
|
37
|
+
source_file TEXT,
|
38
|
+
line_number INTEGER
|
39
|
+
)
|
40
|
+
EOS
|
41
|
+
|
42
|
+
ins = db.prepare <<-EOS
|
43
|
+
insert into Event(
|
44
|
+
exit_status,
|
45
|
+
started_at,
|
46
|
+
ended_at,
|
47
|
+
log_id,
|
48
|
+
ip,
|
49
|
+
unique_visitor,
|
50
|
+
url,
|
51
|
+
controller,
|
52
|
+
html_verb,
|
53
|
+
status,
|
54
|
+
duration_total_ms,
|
55
|
+
duration_views_ms,
|
56
|
+
duration_ar_ms,
|
57
|
+
allocations,
|
58
|
+
comment,
|
59
|
+
source_file,
|
60
|
+
line_number
|
61
|
+
)
|
62
|
+
values (#{Array.new(17, "?").join(", ")})
|
63
|
+
EOS
|
64
|
+
|
65
|
+
db.execute <<-EOS
|
66
|
+
CREATE TABLE IF NOT EXISTS Error(
|
67
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
68
|
+
log_id TEXT,
|
69
|
+
context TEXT,
|
70
|
+
description TEXT,
|
71
|
+
filename TEXT,
|
72
|
+
line_number INTEGER
|
73
|
+
)
|
74
|
+
EOS
|
75
|
+
|
76
|
+
ins_error = db.prepare <<-EOS
|
77
|
+
insert into Error(
|
78
|
+
log_id,
|
79
|
+
context,
|
80
|
+
description,
|
81
|
+
filename,
|
82
|
+
line_number
|
83
|
+
)
|
84
|
+
values (?, ?, ?, ?, ?)
|
85
|
+
EOS
|
86
|
+
|
87
|
+
db.execute <<-EOS
|
88
|
+
CREATE TABLE IF NOT EXISTS Render(
|
89
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
90
|
+
partial TEXT,
|
91
|
+
duration_ms FLOAT,
|
92
|
+
allocations INTEGER,
|
93
|
+
filename TEXT,
|
94
|
+
line_number INTEGER
|
95
|
+
)
|
96
|
+
EOS
|
97
|
+
|
98
|
+
ins_rendered = db.prepare <<-EOS
|
99
|
+
insert into Render(
|
100
|
+
partial,
|
101
|
+
duration_ms,
|
102
|
+
allocations,
|
103
|
+
filename,
|
104
|
+
line_number
|
105
|
+
)
|
106
|
+
values (?, ?, ?, ?, ?)
|
107
|
+
EOS
|
108
|
+
|
109
|
+
# requests in the log might be interleaved.
|
110
|
+
#
|
111
|
+
# We use the 'pending' variable to progressively store data
|
112
|
+
# about requests till they are completed; whey they are
|
113
|
+
# complete, we enter the entry in the DB and remove it from the
|
114
|
+
# hash
|
115
|
+
pending = {}
|
116
|
+
|
117
|
+
# Log lines are either one of:
|
118
|
+
#
|
119
|
+
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Started VERB "URL" for IP at TIMESTAMP
|
120
|
+
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Processing by CONTROLLER as FORMAT
|
121
|
+
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Parameters: JSON
|
122
|
+
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Rendered VIEW within LAYOUT (Duration: DURATION | Allocations: ALLOCATIONS)
|
123
|
+
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Completed STATUS STATUS_STRING in DURATION (Views: DURATION | ActiveRecord: DURATION | Allocations: NUMBER)
|
124
|
+
#
|
125
|
+
# and they appears in the order shown above: started, processing, ...
|
126
|
+
#
|
127
|
+
# Different requests might be interleaved, of course
|
128
|
+
#
|
129
|
+
streams.each do |stream|
|
130
|
+
stream.readlines.each_with_index do |line, line_number|
|
131
|
+
filename = stream == $stdin ? "stdin" : stream.path
|
132
|
+
|
133
|
+
#
|
134
|
+
# These are for development logs
|
135
|
+
#
|
136
|
+
|
137
|
+
data = match_and_process_rendered line
|
138
|
+
if data
|
139
|
+
ins_rendered.execute(
|
140
|
+
data[:partial], data[:duration], data[:allocations],
|
141
|
+
filename, line_number
|
142
|
+
)
|
143
|
+
end
|
144
|
+
|
145
|
+
#
|
146
|
+
#
|
147
|
+
#
|
148
|
+
|
149
|
+
# I and F for completed requests, [ is for error messages
|
150
|
+
next if line[0] != 'I' and line[0] != 'F' and line[0] != '['
|
151
|
+
|
152
|
+
data = match_and_process_error line
|
153
|
+
if data
|
154
|
+
ins_error.execute(data[:log_id],
|
155
|
+
data[:context],
|
156
|
+
data[:description],
|
157
|
+
filename,
|
158
|
+
line_number)
|
159
|
+
next
|
160
|
+
end
|
161
|
+
|
162
|
+
data = match_and_process_start line
|
163
|
+
if data
|
164
|
+
id = data[:log_id]
|
165
|
+
pending[id] = data.merge(pending[id] || {})
|
166
|
+
next
|
167
|
+
end
|
168
|
+
|
169
|
+
data = match_and_process_processing_by line
|
170
|
+
if data
|
171
|
+
id = data[:log_id]
|
172
|
+
pending[id] = data.merge(pending[id] || {})
|
173
|
+
next
|
174
|
+
end
|
175
|
+
|
176
|
+
data = match_and_process_fatal line
|
177
|
+
if data
|
178
|
+
id = data[:log_id]
|
179
|
+
# it might as well be that the first event started before
|
180
|
+
# the log. With this, we make sure we add only events whose
|
181
|
+
# start was logged and parsed
|
182
|
+
if pending[id]
|
183
|
+
event = data.merge(pending[id] || {})
|
184
|
+
|
185
|
+
ins.execute(
|
186
|
+
event[:exit_status],
|
187
|
+
event[:started_at],
|
188
|
+
event[:ended_at],
|
189
|
+
event[:log_id],
|
190
|
+
event[:ip],
|
191
|
+
unique_visitor_id(event),
|
192
|
+
event[:url],
|
193
|
+
event[:controller],
|
194
|
+
event[:html_verb],
|
195
|
+
event[:status],
|
196
|
+
event[:duration_total_ms],
|
197
|
+
event[:duration_views_ms],
|
198
|
+
event[:duration_ar_ms],
|
199
|
+
event[:allocations],
|
200
|
+
event[:comment],
|
201
|
+
filename,
|
202
|
+
line_number
|
203
|
+
)
|
204
|
+
|
205
|
+
pending.delete(id)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
data = self.match_and_process_completed line
|
210
|
+
if data
|
211
|
+
id = data[:log_id]
|
212
|
+
|
213
|
+
# it might as well be that the first event started before
|
214
|
+
# the log. With this, we make sure we add only events whose
|
215
|
+
# start was logged and parsed
|
216
|
+
if pending[id]
|
217
|
+
event = data.merge (pending[id] || {})
|
218
|
+
|
219
|
+
ins.execute(
|
220
|
+
event[:exit_status],
|
221
|
+
event[:started_at],
|
222
|
+
event[:ended_at],
|
223
|
+
event[:log_id],
|
224
|
+
event[:ip],
|
225
|
+
unique_visitor_id(event),
|
226
|
+
event[:url],
|
227
|
+
event[:controller],
|
228
|
+
event[:html_verb],
|
229
|
+
event[:status],
|
230
|
+
event[:duration_total_ms],
|
231
|
+
event[:duration_views_ms],
|
232
|
+
event[:duration_ar_ms],
|
233
|
+
event[:allocations],
|
234
|
+
event[:comment],
|
235
|
+
filename,
|
236
|
+
line_number
|
237
|
+
)
|
238
|
+
|
239
|
+
pending.delete(id)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
db
|
246
|
+
end
|
247
|
+
|
248
|
+
TIMESTAMP = /(?<timestamp>[^ ]+)/
|
249
|
+
ID = /(?<id>[a-z0-9-]+)/
|
250
|
+
VERB = /(?<verb>GET|POST|PATCH|PUT|DELETE)/
|
251
|
+
URL = /(?<url>[^"]+)/
|
252
|
+
IP = /(?<ip>[0-9.]+)/
|
253
|
+
STATUS = /(?<status>[0-9]+)/
|
254
|
+
STATUS_IN_WORDS = /(OK|Unauthorized|Found|Internal Server Error|Bad Request|Method Not Allowed|Request Timeout|Not Implemented|Bad Gateway|Service Unavailable)/
|
255
|
+
MSECS = /[0-9.]+/
|
256
|
+
|
257
|
+
# Error Messages
|
258
|
+
# [584cffcc-f1fd-4b5c-bb8b-b89621bd4921] ActionController::RoutingError (No route matches [GET] "/assets/foundation-icons.svg"):
|
259
|
+
# [fd8df8b5-83c9-48b5-a056-e5026e31bd5e] ActionView::Template::Error (undefined method `all_my_ancestor' for nil:NilClass):
|
260
|
+
# [d17ed55c-f5f1-442a-a9d6-3035ab91adf0] ActionView::Template::Error (undefined method `volunteer_for' for #<DonationsController:0x007f4864c564b8>
|
261
|
+
EXCEPTION = /[A-Za-z_0-9:]+(Error)?/
|
262
|
+
ERROR_REGEXP = /^\[#{ID}\] (?<context>#{EXCEPTION}) \((?<description>(#{EXCEPTION})?.*)\):/
|
263
|
+
|
264
|
+
def match_and_process_error line
|
265
|
+
matchdata = ERROR_REGEXP.match line
|
266
|
+
if matchdata
|
267
|
+
{
|
268
|
+
log_id: matchdata[:id],
|
269
|
+
context: matchdata[:context],
|
270
|
+
description: matchdata[:description]
|
271
|
+
}
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
# 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
|
276
|
+
STARTED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{ID}\] Started #{VERB} "#{URL}" for #{IP} at/
|
277
|
+
|
278
|
+
def match_and_process_start line
|
279
|
+
matchdata = STARTED_REGEXP.match line
|
280
|
+
if matchdata
|
281
|
+
{
|
282
|
+
started_at: matchdata[:timestamp],
|
283
|
+
log_id: matchdata[:id],
|
284
|
+
html_verb: matchdata[:verb],
|
285
|
+
url: matchdata[:url],
|
286
|
+
ip: matchdata[:ip]
|
287
|
+
}
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# TODO: Add regexps for the performance data (Views ...). We have three cases (view, active records, allocations), (views, active records), (active records, allocations)
|
292
|
+
# 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)
|
293
|
+
# 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)
|
294
|
+
# 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)
|
295
|
+
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]+))?\)/
|
296
|
+
|
297
|
+
def match_and_process_completed(line)
|
298
|
+
matchdata = (COMPLETED_REGEXP.match line)
|
299
|
+
# exit_status = matchdata[:status].to_i == 500 ? "E" : "I"
|
300
|
+
if matchdata
|
301
|
+
{
|
302
|
+
exit_status: "I",
|
303
|
+
ended_at: matchdata[:timestamp],
|
304
|
+
log_id: matchdata[:id],
|
305
|
+
status: matchdata[:status],
|
306
|
+
duration_total_ms: matchdata[:total],
|
307
|
+
duration_views_ms: matchdata[:views],
|
308
|
+
duration_ar_ms: matchdata[:arec],
|
309
|
+
allocations: matchdata[:alloc],
|
310
|
+
comment: ""
|
311
|
+
}
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# I, [2021-10-19T08:16:34.345162 #10477] INFO -- : [67103c0d-455d-4fe8-951e-87e97628cb66] Processing by PeopleController#show as HTML
|
316
|
+
PROCESSING_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{ID}\] Processing by (?<controller>[^ ]+) as/
|
317
|
+
|
318
|
+
def match_and_process_processing_by line
|
319
|
+
matchdata = PROCESSING_REGEXP.match line
|
320
|
+
if matchdata
|
321
|
+
{
|
322
|
+
log_id: matchdata[:id],
|
323
|
+
controller: matchdata[:controller]
|
324
|
+
}
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
# F, [2021-12-04T00:34:05.838973 #2735058] FATAL -- : [3a16162e-a6a5-435e-a9d8-c4df5dc0f728]
|
329
|
+
# 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"):
|
330
|
+
# F, [2021-12-04T00:34:05.839209 #2735058] FATAL -- : [3a16162e-a6a5-435e-a9d8-c4df5dc0f728]
|
331
|
+
# 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'
|
332
|
+
FATAL_REGEXP = /F, \[#{TIMESTAMP} #[0-9]+\] FATAL -- : \[#{ID}\] (?<comment>.*)$/
|
333
|
+
|
334
|
+
def match_and_process_fatal(line)
|
335
|
+
matchdata = FATAL_REGEXP.match line
|
336
|
+
if matchdata
|
337
|
+
{
|
338
|
+
exit_status: "F",
|
339
|
+
log_id: matchdata[:id],
|
340
|
+
comment: matchdata[:comment]
|
341
|
+
}
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# Started GET "/projects?locale=it" for 127.0.0.1 at 2024-06-06 23:23:31 +0200
|
346
|
+
# Processing by EmployeesController#index as HTML
|
347
|
+
# Parameters: {"locale"=>"it"}
|
348
|
+
# [...]
|
349
|
+
# Completed 200 OK in 135ms (Views: 128.0ms | ActiveRecord: 2.5ms | Allocations: 453450)
|
350
|
+
#
|
351
|
+
# Started GET "/serviceworker.js" for 127.0.0.1 at 2024-06-06 23:23:29 +0200
|
352
|
+
# ActionController::RoutingError (No route matches [GET] "/serviceworker.js"):
|
353
|
+
#
|
354
|
+
#
|
355
|
+
# Started POST "/projects?locale=it" for 127.0.0.1 at 2024-06-06 23:34:33 +0200
|
356
|
+
# Processing by ProjectsController#create as TURBO_STREAM
|
357
|
+
# Parameters: {"authenticity_token"=>"[FILTERED]", "project"=>{"name"=>"AA", "funding_agency"=>"", "total_cost"=>"0,00", "personnel_cost"=>"0,00", "percentage_funded"=>"0", "from_date"=>"2024-01-01", "to_date"=>"2025-12-31", "notes"=>""}, "commit"=>"Crea Progetto", "locale"=>"it"}
|
358
|
+
#
|
359
|
+
# Completed in 48801ms (ActiveRecord: 17.8ms | Allocations: 2274498)
|
360
|
+
# Completed 422 Unprocessable Entity in 16ms (Views: 5.1ms | ActiveRecord: 2.0ms | Allocations: 10093)
|
361
|
+
#
|
362
|
+
# Completed 500 Internal Server Error in 24ms (ActiveRecord: 1.4ms | Allocations: 4660)
|
363
|
+
# ActionView::Template::Error (Error: Undefined variable: "$white".
|
364
|
+
# on line 6:28 of app/assets/stylesheets/_animations.scss
|
365
|
+
# from line 16:9 of app/assets/stylesheets/application.scss
|
366
|
+
# >> from { background-color: $white; }
|
367
|
+
|
368
|
+
# ---------------------------^
|
369
|
+
# ):
|
370
|
+
# 9: = csrf_meta_tags
|
371
|
+
# 10: = csp_meta_tag
|
372
|
+
# 11:
|
373
|
+
# 12: = stylesheet_link_tag "application", "data-turbo-track": "reload"
|
374
|
+
# 13: = javascript_importmap_tags
|
375
|
+
# 14:
|
376
|
+
# 15: %body
|
377
|
+
#
|
378
|
+
# app/views/layouts/application.html.haml:12
|
379
|
+
# app/controllers/application_controller.rb:26:in `switch_locale'
|
380
|
+
|
381
|
+
# Rendered devise/sessions/_project_partial.html.erb (Duration: 78.4ms | Allocations: 88373)
|
382
|
+
# Rendered devise/sessions/new.html.haml within layouts/application (Duration: 100.0ms | Allocations: 104118)
|
383
|
+
# Rendered application/_favicon.html.erb (Duration: 2.6ms | Allocations: 4454)
|
384
|
+
# Rendered layouts/_manage_notice.html.erb (Duration: 0.3ms | Allocations: 193)
|
385
|
+
# Rendered layout layouts/application.html.erb (Duration: 263.4ms | Allocations: 367467)
|
386
|
+
# Rendered donations/_switcher.html.haml (Duration: 41.1ms | Allocations: 9550)
|
387
|
+
# Rendered donations/_status_header.html.haml (Duration: 1.4ms | Allocations: 3192)
|
388
|
+
# Rendered donations/_status_header.html.haml (Duration: 0.0ms | Allocations: 7)
|
389
|
+
RENDERED_REGEXP = /^ *Rendered (?<partial>[^ ]+) .*\(Duration: (?<duration>[0-9.]+)ms \| Allocations: (?<allocations>[0-9]+)\)$/
|
390
|
+
|
391
|
+
def match_and_process_rendered(line)
|
392
|
+
matchdata = RENDERED_REGEXP.match line
|
393
|
+
if matchdata
|
394
|
+
{
|
395
|
+
partial: matchdata[:partial],
|
396
|
+
duration: matchdata[:duration],
|
397
|
+
allocations: matchdata[:allocations]
|
398
|
+
}
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# generate a unique visitor id from an event
|
403
|
+
def unique_visitor_id(event)
|
404
|
+
date = event[:started_at] || event[:ended_at] || "1970-01-01"
|
405
|
+
"#{DateTime.parse(date).strftime("%Y-%m-%d")} #{event[:ip]}"
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
@@ -1,7 +1,3 @@
|
|
1
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
2
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
3
|
-
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@300;700&display=swap" rel="stylesheet">
|
4
|
-
|
5
1
|
<% LogSense::Emitter::CDN_CSS.each do |link| %>
|
6
2
|
<link rel="stylesheet" type="text/css" href="<%= link %>">
|
7
3
|
<% end %>
|
@@ -1,10 +1,10 @@
|
|
1
1
|
<ul class="stats-list">
|
2
2
|
<li>
|
3
|
-
<%= data[:first_day]
|
3
|
+
<%= data[:first_day]&.strftime("%b %d, %Y") %>
|
4
4
|
<span class="stats-list-label">From</span>
|
5
5
|
</li>
|
6
6
|
<li>
|
7
|
-
<%= data[:last_day]
|
7
|
+
<%= data[:last_day]&.strftime("%b %d, %Y") %>
|
8
8
|
<span class="stats-list-label">To</span>
|
9
9
|
</li>
|
10
10
|
<li class="stats-list-positive">
|
@@ -1,7 +1,7 @@
|
|
1
1
|
<script>
|
2
2
|
/* this is used both by Vega and DataTable for <%= report[:title] %>*/
|
3
3
|
data_<%= index %> = [
|
4
|
-
<% report[:rows].each do |row| -%>
|
4
|
+
<% (report[:rows] || []).each do |row| -%>
|
5
5
|
{
|
6
6
|
<% report[:header].each_with_index do |h, i| -%>
|
7
7
|
"<%= h %>": "<%= Emitter::process row[i] %>",
|
@@ -1,6 +1,12 @@
|
|
1
|
+
:root {
|
2
|
+
--font-family: Inter, Helvetica, Arial, sans-serif;
|
3
|
+
|
4
|
+
}
|
5
|
+
|
1
6
|
body {
|
2
|
-
font-family:
|
3
|
-
font-size: 12px;
|
7
|
+
font-family: var(--font-family);
|
8
|
+
/* font-size: 12px; */
|
9
|
+
font-size: 0.86rem;
|
4
10
|
}
|
5
11
|
|
6
12
|
#offCanvas {
|
@@ -34,29 +40,22 @@ nav h2 {
|
|
34
40
|
}
|
35
41
|
|
36
42
|
h1 {
|
37
|
-
font-family:
|
43
|
+
font-family: var(--font-family);
|
38
44
|
font-size: 2rem;
|
39
45
|
font-weight: bold;
|
46
|
+
color: #1C1C1C;
|
40
47
|
}
|
41
48
|
|
42
49
|
h2 {
|
43
|
-
font-family:
|
44
|
-
font-size: 1.
|
50
|
+
font-family: var(--font-family);
|
51
|
+
font-size: 1.6rem;
|
45
52
|
font-weight: bold;
|
46
|
-
|
47
|
-
color: white;
|
48
|
-
background: #1C1C1C;
|
49
|
-
|
50
|
-
padding: 0.2rem 0.8rem 0.2rem 0.8rem;
|
51
|
-
border-radius: 5px 5px 0px 0px;
|
52
|
-
}
|
53
|
-
|
54
|
-
th {
|
55
|
-
padding: 0.2rem 1.2rem 0.2rem 0.2rem !important
|
53
|
+
color: #1C1C1C;
|
56
54
|
}
|
57
55
|
|
58
|
-
td {
|
59
|
-
padding: 0.2rem
|
56
|
+
th, td {
|
57
|
+
padding-top: 0.2rem !important;
|
58
|
+
padding-bottom: 0.2rem !important;
|
60
59
|
}
|
61
60
|
|
62
61
|
.dataTables_wrapper {
|
@@ -64,9 +63,12 @@ td {
|
|
64
63
|
padding: 0.5rem;
|
65
64
|
}
|
66
65
|
|
66
|
+
table.dataTable > tbody > tr:nth-child(2n) {
|
67
|
+
border-shadow: none !important;
|
68
|
+
}
|
69
|
+
|
67
70
|
article {
|
68
|
-
margin-top:
|
69
|
-
border-radius: 5px;
|
71
|
+
margin-top: 2rem;
|
70
72
|
}
|
71
73
|
|
72
74
|
|