rails-pretty-logger 0.2.9 → 0.3.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/README.md +215 -35
- data/app/assets/javascripts/rails/pretty/logger/application.js +16 -23
- data/app/assets/stylesheets/rails/pretty/logger/application.css +1 -15
- data/app/assets/stylesheets/rails/pretty/logger/dashboards.css +463 -141
- data/app/assets/stylesheets/rails/pretty/logger/list.css +1 -94
- data/app/controllers/rails/pretty/logger/application_controller.rb +31 -0
- data/app/controllers/rails/pretty/logger/dashboards_controller.rb +9 -4
- data/app/controllers/rails/pretty/logger/hourly_logs_controller.rb +36 -4
- data/app/helpers/rails/pretty/logger/application_helper.rb +32 -0
- data/app/helpers/rails/pretty/logger/dashboards_helper.rb +114 -14
- data/app/views/layouts/rails/pretty/logger/application.html.erb +9 -8
- data/app/views/partials/_error_pagination.html.erb +12 -10
- data/app/views/partials/_log_entries.html.erb +5 -0
- data/app/views/partials/_log_filters.html.erb +14 -0
- data/app/views/partials/_pretyyloggernavbar.html.erb +14 -9
- data/app/views/rails/pretty/logger/dashboards/index.html.erb +37 -20
- data/app/views/rails/pretty/logger/dashboards/logs.html.erb +33 -14
- data/app/views/rails/pretty/logger/hourly_logs/index.html.erb +49 -25
- data/app/views/rails/pretty/logger/hourly_logs/logs.html.erb +35 -14
- data/config/locales/rails_pretty_logger.en.yml +35 -0
- data/config/locales/rails_pretty_logger.tr.yml +35 -0
- data/lib/generators/rails_pretty_logger/install/install_generator.rb +29 -0
- data/lib/generators/rails_pretty_logger/install/templates/rails_pretty_logger.rb +20 -0
- data/lib/rails/pretty/logger/active_support_logger.rb +3 -7
- data/lib/rails/pretty/logger/config/logger_config.rb +0 -16
- data/lib/rails/pretty/logger/configuration.rb +18 -0
- data/lib/rails/pretty/logger/console_logger.rb +2 -2
- data/lib/rails/pretty/logger/engine.rb +22 -4
- data/lib/rails/pretty/logger/rails_logger.rb +62 -41
- data/lib/rails/pretty/logger/version.rb +1 -1
- data/lib/rails/pretty/logger.rb +547 -31
- data/lib/tasks/rails/pretty/logger_tasks.rake +36 -23
- metadata +79 -38
- data/Rakefile +0 -22
- data/app/assets/javascripts/rails/pretty/logger/dashboards.js +0 -2
- data/app/assets/javascripts/rails/pretty/logger/list.min.js +0 -2
- data/app/models/rails/pretty/logger/application_record.rb +0 -9
- data/app/assets/config/{rails_pretty_logger_manifest.js → manifest.js} +1 -1
data/lib/rails/pretty/logger.rb
CHANGED
|
@@ -1,12 +1,39 @@
|
|
|
1
|
+
require "active_support/core_ext/object/blank"
|
|
2
|
+
require "active_support/core_ext/string/conversions"
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "pathname"
|
|
7
|
+
require "rails/pretty/logger/configuration"
|
|
1
8
|
require "rails/pretty/logger/engine"
|
|
2
9
|
|
|
3
10
|
module Rails::Pretty::Logger
|
|
11
|
+
def self.configuration
|
|
12
|
+
@configuration ||= Configuration.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.configure
|
|
16
|
+
yield configuration
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.reset_configuration!
|
|
20
|
+
@configuration = Configuration.new
|
|
21
|
+
end
|
|
4
22
|
|
|
5
23
|
class PrettyLogger
|
|
24
|
+
class InvalidLogFile < StandardError; end
|
|
25
|
+
class FileTooLarge < StandardError; end
|
|
26
|
+
SEVERITIES = %w[DEBUG INFO WARN ERROR FATAL UNKNOWN].freeze
|
|
27
|
+
LINE_INDEX_CACHE_LIMIT = 32
|
|
28
|
+
TAIL_READ_CHUNK_SIZE = 64 * 1024
|
|
29
|
+
STRUCTURED_TIMESTAMP_KEYS = %w[@timestamp timestamp time datetime created_at].freeze
|
|
30
|
+
STRUCTURED_SEVERITY_KEYS = %w[severity level log_level].freeze
|
|
31
|
+
|
|
32
|
+
attr_reader :log_file
|
|
6
33
|
|
|
7
34
|
def initialize(params)
|
|
8
|
-
@log_file = params[:log_file]
|
|
9
35
|
@filter_params = params
|
|
36
|
+
@log_file = self.class.resolve_log_file(params[:log_file])
|
|
10
37
|
end
|
|
11
38
|
|
|
12
39
|
def self.logger
|
|
@@ -21,14 +48,43 @@ module Rails::Pretty::Logger
|
|
|
21
48
|
File.size?("#{log_file}").to_f / 2**20
|
|
22
49
|
end
|
|
23
50
|
|
|
51
|
+
def self.log_root
|
|
52
|
+
Rails.root.join("log")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.resolve_log_file(log_file)
|
|
56
|
+
raise InvalidLogFile if log_file.blank?
|
|
57
|
+
|
|
58
|
+
candidate = Pathname.new(log_file.to_s)
|
|
59
|
+
candidate = log_root.join(candidate) unless candidate.absolute?
|
|
60
|
+
|
|
61
|
+
root_path = real_log_root
|
|
62
|
+
real_path = candidate.realpath
|
|
63
|
+
|
|
64
|
+
unless real_path.to_s == root_path.to_s || real_path.to_s.start_with?("#{root_path}/")
|
|
65
|
+
raise InvalidLogFile
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
raise InvalidLogFile unless real_path.file?
|
|
69
|
+
|
|
70
|
+
real_path.to_s
|
|
71
|
+
rescue Errno::ENOENT, Errno::EACCES, ArgumentError
|
|
72
|
+
raise InvalidLogFile
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.real_log_root
|
|
76
|
+
FileUtils.mkdir_p(log_root)
|
|
77
|
+
log_root.realpath
|
|
78
|
+
end
|
|
79
|
+
|
|
24
80
|
def self.get_log_file_list
|
|
25
|
-
log_files =
|
|
26
|
-
|
|
81
|
+
log_files = Dir[File.join(log_root, "*")].select { |file| File.file?(file) }
|
|
82
|
+
logs_atr(log_files)
|
|
27
83
|
end
|
|
28
84
|
|
|
29
85
|
def self.get_hourly_log_file_list
|
|
30
|
-
log_files =
|
|
31
|
-
|
|
86
|
+
log_files = Dir[File.join(log_root, "hourly", "**", "*")].select { |file| File.file?(file) }.sort
|
|
87
|
+
logs_atr(log_files)
|
|
32
88
|
end
|
|
33
89
|
|
|
34
90
|
def self.logs_atr(log_files)
|
|
@@ -41,8 +97,129 @@ module Rails::Pretty::Logger
|
|
|
41
97
|
log
|
|
42
98
|
end
|
|
43
99
|
|
|
100
|
+
def self.ensure_file_size_within_limit!(log_file)
|
|
101
|
+
max_file_size = Rails::Pretty::Logger.configuration.max_file_size
|
|
102
|
+
return if max_file_size.blank?
|
|
103
|
+
|
|
104
|
+
raise FileTooLarge if File.size(log_file) > max_file_size.to_i
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.tail_lines
|
|
108
|
+
Rails::Pretty::Logger.configuration.tail_lines.to_i.positive? ? Rails::Pretty::Logger.configuration.tail_lines.to_i : 500
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.structured_log_payload(line)
|
|
112
|
+
payload = JSON.parse(line)
|
|
113
|
+
payload if payload.is_a?(Hash)
|
|
114
|
+
rescue JSON::ParserError, TypeError
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.custom_log_metadata(line)
|
|
119
|
+
parser = Rails::Pretty::Logger.configuration.log_line_parser
|
|
120
|
+
return {} unless parser.respond_to?(:call)
|
|
121
|
+
|
|
122
|
+
metadata = parser.call(line)
|
|
123
|
+
metadata.is_a?(Hash) ? metadata : {}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.fetch_line_index_cache(cache_key, signature)
|
|
127
|
+
entry_key = [cache_key, signature]
|
|
128
|
+
|
|
129
|
+
line_index_cache_mutex.synchronize do
|
|
130
|
+
if line_index_cache.key?(entry_key)
|
|
131
|
+
line_index_cache_order.delete(entry_key)
|
|
132
|
+
line_index_cache_order << entry_key
|
|
133
|
+
return line_index_cache.fetch(entry_key)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if (offsets = read_persistent_line_index(cache_key, signature))
|
|
138
|
+
store_line_index_cache(entry_key, offsets)
|
|
139
|
+
return offsets
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
offsets = yield.freeze
|
|
143
|
+
write_persistent_line_index(cache_key, signature, offsets)
|
|
144
|
+
store_line_index_cache(entry_key, offsets)
|
|
145
|
+
|
|
146
|
+
offsets
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def self.store_line_index_cache(entry_key, offsets)
|
|
150
|
+
line_index_cache_mutex.synchronize do
|
|
151
|
+
line_index_cache[entry_key] = offsets
|
|
152
|
+
line_index_cache_order.delete(entry_key)
|
|
153
|
+
line_index_cache_order << entry_key
|
|
154
|
+
|
|
155
|
+
while line_index_cache_order.length > LINE_INDEX_CACHE_LIMIT
|
|
156
|
+
line_index_cache.delete(line_index_cache_order.shift)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def self.clear_line_index_cache!
|
|
162
|
+
clear_line_index_memory_cache!
|
|
163
|
+
FileUtils.rm_rf(line_index_cache_root)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.clear_line_index_memory_cache!
|
|
167
|
+
line_index_cache_mutex.synchronize do
|
|
168
|
+
line_index_cache.clear
|
|
169
|
+
line_index_cache_order.clear
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self.clear_line_index_cache_for!(log_file)
|
|
174
|
+
line_index_cache_mutex.synchronize do
|
|
175
|
+
line_index_cache.delete_if { |(cache_key, _signature), _offsets| cache_key.first == log_file || cache_key[1] == log_file }
|
|
176
|
+
line_index_cache_order.delete_if { |cache_key, _signature| cache_key.first == log_file || cache_key[1] == log_file }
|
|
177
|
+
end
|
|
178
|
+
FileUtils.rm_rf(line_index_cache_root)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def self.line_index_cache
|
|
182
|
+
@line_index_cache ||= {}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def self.line_index_cache_order
|
|
186
|
+
@line_index_cache_order ||= []
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.line_index_cache_mutex
|
|
190
|
+
@line_index_cache_mutex ||= Mutex.new
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def self.read_persistent_line_index(cache_key, signature)
|
|
194
|
+
payload = Marshal.load(File.binread(line_index_cache_path(cache_key)))
|
|
195
|
+
return unless payload[:signature] == signature
|
|
196
|
+
|
|
197
|
+
payload[:offsets].freeze
|
|
198
|
+
rescue Errno::ENOENT, EOFError, TypeError, ArgumentError
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def self.write_persistent_line_index(cache_key, signature, offsets)
|
|
203
|
+
FileUtils.mkdir_p(line_index_cache_root)
|
|
204
|
+
path = line_index_cache_path(cache_key)
|
|
205
|
+
temp_path = "#{path}.#{$$}.tmp"
|
|
206
|
+
File.binwrite(temp_path, Marshal.dump(signature: signature, offsets: offsets))
|
|
207
|
+
FileUtils.mv(temp_path, path)
|
|
208
|
+
rescue SystemCallError, TypeError, ArgumentError
|
|
209
|
+
FileUtils.rm_f(temp_path) if temp_path
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def self.line_index_cache_path(cache_key)
|
|
213
|
+
line_index_cache_root.join("#{Digest::SHA256.hexdigest(Marshal.dump(cache_key))}.marshal")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def self.line_index_cache_root
|
|
217
|
+
Rails.root.join("tmp", "cache", "rails_pretty_logger", "line_indexes")
|
|
218
|
+
end
|
|
219
|
+
|
|
44
220
|
def clear_logs
|
|
45
|
-
open(@log_file, File::TRUNC) {}
|
|
221
|
+
File.open(@log_file, File::TRUNC) {}
|
|
222
|
+
self.class.clear_line_index_cache_for!(@log_file)
|
|
46
223
|
end
|
|
47
224
|
|
|
48
225
|
def start_date
|
|
@@ -54,44 +231,71 @@ module Rails::Pretty::Logger
|
|
|
54
231
|
end
|
|
55
232
|
|
|
56
233
|
def filter_logs_with_date(file)
|
|
57
|
-
|
|
234
|
+
each_filtered_log_line(file).to_a
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def each_filtered_log_line(file)
|
|
238
|
+
return enum_for(:each_filtered_log_line, file) unless block_given?
|
|
239
|
+
|
|
240
|
+
each_filtered_log_line_with_offset(file) { |line, _offset| yield line }
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def each_filtered_log_line_with_offset(file)
|
|
244
|
+
return enum_for(:each_filtered_log_line_with_offset, file) unless block_given?
|
|
245
|
+
|
|
58
246
|
start = false
|
|
59
247
|
|
|
60
|
-
|
|
248
|
+
each_raw_log_line_with_offset(file) do |line, offset|
|
|
61
249
|
if get_date_from_log_line(line)
|
|
62
250
|
start = true
|
|
63
|
-
|
|
251
|
+
yield line, offset
|
|
64
252
|
elsif start && !(line_include_date?(line))
|
|
65
|
-
|
|
253
|
+
yield line, offset
|
|
66
254
|
else
|
|
67
255
|
start = false
|
|
68
256
|
end
|
|
69
257
|
end
|
|
70
|
-
return arr
|
|
71
258
|
end
|
|
72
259
|
|
|
73
260
|
def get_test_logs(file)
|
|
74
|
-
|
|
75
|
-
IO.foreach(file) do |line|
|
|
76
|
-
arr.push(line)
|
|
77
|
-
end
|
|
78
|
-
return arr
|
|
261
|
+
IO.foreach(file).to_a
|
|
79
262
|
end
|
|
80
263
|
|
|
81
264
|
def get_logs_from_file(file)
|
|
82
|
-
|
|
83
|
-
|
|
265
|
+
each_log_line(file).to_a
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def each_log_line(file)
|
|
269
|
+
return enum_for(:each_log_line, file) unless block_given?
|
|
270
|
+
|
|
271
|
+
each_log_line_with_offset(file) { |line, _offset| yield line }
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def each_log_line_with_offset(file)
|
|
275
|
+
return enum_for(:each_log_line_with_offset, file) unless block_given?
|
|
276
|
+
|
|
277
|
+
if test_log?(file) || hourly_log?(file)
|
|
278
|
+
each_raw_log_line_with_offset(file) { |line, offset| yield line, offset }
|
|
84
279
|
else
|
|
85
|
-
|
|
280
|
+
each_filtered_log_line_with_offset(file) { |line, offset| yield line, offset }
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def each_raw_log_line_with_offset(file)
|
|
285
|
+
return enum_for(:each_raw_log_line_with_offset, file) unless block_given?
|
|
286
|
+
|
|
287
|
+
File.open(file, "r") do |io|
|
|
288
|
+
until io.eof?
|
|
289
|
+
offset = io.pos
|
|
290
|
+
line = io.gets
|
|
291
|
+
yield line, offset if line
|
|
292
|
+
end
|
|
86
293
|
end
|
|
87
294
|
end
|
|
88
295
|
|
|
89
296
|
def get_date_from_log_line(line)
|
|
90
|
-
|
|
91
|
-
if
|
|
92
|
-
date_string_index = line.index("at ")
|
|
93
|
-
string_date = line[date_string_index .. date_string_index + 13]
|
|
94
|
-
date = string_date.to_date.strftime("%Y-%m-%d")
|
|
297
|
+
date = date_from_log_line(line)
|
|
298
|
+
if date.present?
|
|
95
299
|
start_date = @filter_params.dig(:date_range, :start)
|
|
96
300
|
end_date = @filter_params.dig(:date_range, :end)
|
|
97
301
|
if start_date.present? && end_date.present?
|
|
@@ -103,7 +307,7 @@ module Rails::Pretty::Logger
|
|
|
103
307
|
end
|
|
104
308
|
|
|
105
309
|
def line_include_date?(line)
|
|
106
|
-
line.
|
|
310
|
+
date_from_log_line(line).present?
|
|
107
311
|
end
|
|
108
312
|
|
|
109
313
|
def validate_date
|
|
@@ -121,25 +325,337 @@ module Rails::Pretty::Logger
|
|
|
121
325
|
def log_data
|
|
122
326
|
error = validate_date
|
|
123
327
|
divider = set_divider_value
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
328
|
+
return grouped_log_data(error, divider) if request_grouping?
|
|
329
|
+
|
|
330
|
+
page_start = @filter_params[:page].to_i * divider
|
|
331
|
+
page_end = page_start + divider
|
|
332
|
+
|
|
333
|
+
self.class.ensure_file_size_within_limit!(@log_file)
|
|
334
|
+
|
|
335
|
+
line_offsets = cached_log_line_offsets
|
|
336
|
+
paginated_logs = read_log_lines_at_offsets(@log_file, line_offsets[page_start...page_end] || [])
|
|
337
|
+
|
|
128
338
|
data = {}
|
|
129
|
-
data[:logs_count] =
|
|
339
|
+
data[:logs_count] = (line_offsets.length.to_f / divider).ceil
|
|
130
340
|
data[:paginated_logs] = paginated_logs
|
|
131
341
|
data[:error] = error
|
|
132
342
|
return data
|
|
133
343
|
end
|
|
134
344
|
|
|
345
|
+
def tail_log_data
|
|
346
|
+
self.class.ensure_file_size_within_limit!(@log_file)
|
|
347
|
+
|
|
348
|
+
lines = tail_lines(@log_file, self.class.tail_lines).select { |line| line_matches_filters?(line) }
|
|
349
|
+
{
|
|
350
|
+
logs_count: lines.any? ? 1 : 0,
|
|
351
|
+
paginated_logs: lines,
|
|
352
|
+
error: nil
|
|
353
|
+
}
|
|
354
|
+
end
|
|
355
|
+
|
|
135
356
|
def set_divider_value
|
|
136
357
|
if @filter_params[:date_range].blank?
|
|
137
358
|
100
|
|
138
359
|
elsif @filter_params[:date_range][:divider].blank?
|
|
139
360
|
100
|
|
140
361
|
else
|
|
141
|
-
@filter_params[:date_range][:divider].to_i
|
|
362
|
+
divider = @filter_params[:date_range][:divider].to_i
|
|
363
|
+
divider.positive? ? divider : 100
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def test_log?(file)
|
|
368
|
+
File.basename(file).include?("test")
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def hourly_log?(file)
|
|
372
|
+
file.include?("#{File::SEPARATOR}hourly#{File::SEPARATOR}")
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def line_matches_filters?(line)
|
|
376
|
+
query = @filter_params[:query].to_s.strip
|
|
377
|
+
return false if query.present? && !line.downcase.include?(query.downcase)
|
|
378
|
+
|
|
379
|
+
severity = @filter_params[:severity].to_s.upcase
|
|
380
|
+
return true unless SEVERITIES.include?(severity)
|
|
381
|
+
|
|
382
|
+
structured_severity = structured_log_severity(line)
|
|
383
|
+
return structured_severity == severity if structured_severity.present?
|
|
384
|
+
|
|
385
|
+
line.match?(/\b#{Regexp.escape(severity)}\b/i)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def tail_lines(file, count)
|
|
389
|
+
count = count.to_i
|
|
390
|
+
return [] unless count.positive?
|
|
391
|
+
|
|
392
|
+
File.open(file, "rb") do |io|
|
|
393
|
+
offset = io.size
|
|
394
|
+
return [] if offset.zero?
|
|
395
|
+
|
|
396
|
+
chunks = []
|
|
397
|
+
newline_count = 0
|
|
398
|
+
|
|
399
|
+
while offset.positive? && newline_count <= count
|
|
400
|
+
chunk_size = [TAIL_READ_CHUNK_SIZE, offset].min
|
|
401
|
+
offset -= chunk_size
|
|
402
|
+
io.seek(offset)
|
|
403
|
+
|
|
404
|
+
chunk = io.read(chunk_size)
|
|
405
|
+
chunks.unshift(chunk)
|
|
406
|
+
newline_count += chunk.count("\n")
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
chunks.join.lines.last(count).map { |line| line.force_encoding(Encoding.default_external) }
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def request_grouping?
|
|
414
|
+
@filter_params[:group].to_s == "request"
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def cached_log_line_offsets
|
|
418
|
+
self.class.fetch_line_index_cache(log_line_index_cache_key, log_file_signature) do
|
|
419
|
+
build_log_line_offsets
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def build_log_line_offsets
|
|
424
|
+
offsets = []
|
|
425
|
+
|
|
426
|
+
each_log_line_with_offset(@log_file) do |line, offset|
|
|
427
|
+
offsets << offset if line_matches_filters?(line)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
offsets
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def read_log_lines_at_offsets(file, offsets)
|
|
434
|
+
File.open(file, "r") do |io|
|
|
435
|
+
offsets.map do |offset|
|
|
436
|
+
io.seek(offset)
|
|
437
|
+
io.gets
|
|
438
|
+
end.compact
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def log_line_index_cache_key
|
|
443
|
+
[
|
|
444
|
+
@log_file,
|
|
445
|
+
date_filtered_log? ? start_date : nil,
|
|
446
|
+
date_filtered_log? ? end_date : nil,
|
|
447
|
+
@filter_params[:query].to_s.strip.downcase,
|
|
448
|
+
normalized_severity_filter,
|
|
449
|
+
Rails::Pretty::Logger.configuration.log_line_parser&.object_id
|
|
450
|
+
]
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def log_file_signature
|
|
454
|
+
stat = File.stat(@log_file)
|
|
455
|
+
[stat.size, stat.mtime.to_f, stat.ctime.to_f]
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def date_filtered_log?
|
|
459
|
+
!test_log?(@log_file) && !hourly_log?(@log_file)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def normalized_severity_filter
|
|
463
|
+
severity = @filter_params[:severity].to_s.upcase
|
|
464
|
+
SEVERITIES.include?(severity) ? severity : nil
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def grouped_log_data(error, divider)
|
|
468
|
+
self.class.ensure_file_size_within_limit!(@log_file)
|
|
469
|
+
|
|
470
|
+
page_start = @filter_params[:page].to_i * divider
|
|
471
|
+
page_end = page_start + divider
|
|
472
|
+
matching_groups = matching_request_group_index
|
|
473
|
+
|
|
474
|
+
paginated_groups = (matching_groups[page_start...page_end] || []).map { |group| request_group_from_index(group) }
|
|
475
|
+
|
|
476
|
+
{
|
|
477
|
+
logs_count: (matching_groups.length.to_f / divider).ceil,
|
|
478
|
+
paginated_logs: paginated_groups,
|
|
479
|
+
error: error
|
|
480
|
+
}
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def each_request_group(file)
|
|
484
|
+
return enum_for(:each_request_group, file) unless block_given?
|
|
485
|
+
|
|
486
|
+
cached_request_group_index.each do |group|
|
|
487
|
+
yield request_group_from_index(group)
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def cached_request_group_index
|
|
492
|
+
self.class.fetch_line_index_cache(request_group_index_cache_key, log_file_signature) do
|
|
493
|
+
build_request_group_index
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def build_request_group_index
|
|
498
|
+
groups = []
|
|
499
|
+
current_group = nil
|
|
500
|
+
|
|
501
|
+
each_log_line_with_offset(@log_file) do |line, offset|
|
|
502
|
+
if (metadata = request_start_metadata(line))
|
|
503
|
+
groups << current_group if current_group
|
|
504
|
+
current_group = metadata.merge(line_offsets: [offset])
|
|
505
|
+
elsif current_group
|
|
506
|
+
current_group[:line_offsets] << offset
|
|
507
|
+
current_group.merge!(request_completion_metadata(line) || {})
|
|
508
|
+
else
|
|
509
|
+
current_group = { type: :ungrouped, line_offsets: [offset] }
|
|
510
|
+
end
|
|
142
511
|
end
|
|
512
|
+
|
|
513
|
+
groups << current_group if current_group
|
|
514
|
+
groups
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def matching_request_group_index
|
|
518
|
+
groups = cached_request_group_index
|
|
519
|
+
return groups unless filtered_request_groups?
|
|
520
|
+
|
|
521
|
+
groups.select { |group| group_matches_filters?(request_group_from_index(group)) }
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def request_group_from_index(group)
|
|
525
|
+
group.merge(lines: read_log_lines_at_offsets(@log_file, group.fetch(:line_offsets))).tap do |request_group|
|
|
526
|
+
request_group.delete(:line_offsets)
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def filtered_request_groups?
|
|
531
|
+
@filter_params[:query].to_s.strip.present? || normalized_severity_filter.present?
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def group_matches_filters?(group)
|
|
535
|
+
group.fetch(:lines).any? { |line| line_matches_filters?(line) }
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def request_group_index_cache_key
|
|
539
|
+
[
|
|
540
|
+
:request_groups,
|
|
541
|
+
@log_file,
|
|
542
|
+
date_filtered_log? ? start_date : nil,
|
|
543
|
+
date_filtered_log? ? end_date : nil,
|
|
544
|
+
Rails::Pretty::Logger.configuration.log_line_parser&.object_id
|
|
545
|
+
]
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def request_start_metadata(line)
|
|
549
|
+
metadata = custom_log_metadata(line)
|
|
550
|
+
request_method = metadata_value(metadata, :request_method, :method)
|
|
551
|
+
request_path = metadata_value(metadata, :request_path, :path)
|
|
552
|
+
|
|
553
|
+
if request_method.present? && request_path.present?
|
|
554
|
+
return {
|
|
555
|
+
type: :request,
|
|
556
|
+
method: request_method.to_s,
|
|
557
|
+
path: request_path.to_s,
|
|
558
|
+
ip: metadata_value(metadata, :request_ip, :ip),
|
|
559
|
+
started_at: metadata_value(metadata, :request_started_at, :started_at, :timestamp, :time).to_s
|
|
560
|
+
}
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
match = line.strip.match(/\AStarted\s+(?<method>[A-Z]+)\s+"(?<path>[^"]+)"(?:\s+for\s+(?<ip>\S+))?\s+at\s+(?<timestamp>.+)\z/)
|
|
564
|
+
return unless match
|
|
565
|
+
|
|
566
|
+
{
|
|
567
|
+
type: :request,
|
|
568
|
+
method: match[:method],
|
|
569
|
+
path: match[:path],
|
|
570
|
+
ip: match[:ip],
|
|
571
|
+
started_at: match[:timestamp].strip
|
|
572
|
+
}
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def request_completion_metadata(line)
|
|
576
|
+
metadata = custom_log_metadata(line)
|
|
577
|
+
response_status = metadata_value(metadata, :response_status, :status)
|
|
578
|
+
duration = metadata_value(metadata, :duration, :request_duration)
|
|
579
|
+
|
|
580
|
+
if response_status.present? || duration.present?
|
|
581
|
+
return {
|
|
582
|
+
status: response_status.to_s,
|
|
583
|
+
duration: duration.to_s
|
|
584
|
+
}
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
match = line.strip.match(/\ACompleted\s+(?<status>\d{3}).*?\sin\s+(?<duration>[\d.]+ms)/)
|
|
588
|
+
return unless match
|
|
589
|
+
|
|
590
|
+
{
|
|
591
|
+
status: match[:status],
|
|
592
|
+
duration: match[:duration]
|
|
593
|
+
}
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def date_from_log_line(line)
|
|
597
|
+
timestamp = custom_log_timestamp(line) || request_timestamp(line) || structured_log_timestamp(line)
|
|
598
|
+
timestamp&.to_date&.strftime("%Y-%m-%d")
|
|
599
|
+
rescue Date::Error, NoMethodError
|
|
600
|
+
nil
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def request_timestamp(line)
|
|
604
|
+
match = line.strip.match(/\AStarted\s+.*\sat\s+(?<timestamp>.+)\z/)
|
|
605
|
+
match[:timestamp] if match
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def structured_log_timestamp(line)
|
|
609
|
+
payload = self.class.structured_log_payload(line)
|
|
610
|
+
return unless payload
|
|
611
|
+
|
|
612
|
+
STRUCTURED_TIMESTAMP_KEYS.each do |key|
|
|
613
|
+
return payload[key].to_s if payload[key].present?
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
nil
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def structured_log_severity(line)
|
|
620
|
+
severity = custom_log_severity(line)
|
|
621
|
+
return severity if severity.present?
|
|
622
|
+
|
|
623
|
+
payload = self.class.structured_log_payload(line)
|
|
624
|
+
return unless payload
|
|
625
|
+
|
|
626
|
+
STRUCTURED_SEVERITY_KEYS.each do |key|
|
|
627
|
+
severity = payload[key].to_s.upcase
|
|
628
|
+
return severity if SEVERITIES.include?(severity)
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
nested_log = payload["log"]
|
|
632
|
+
return unless nested_log.respond_to?(:[])
|
|
633
|
+
|
|
634
|
+
nested_severity = nested_log["level"].to_s.upcase
|
|
635
|
+
nested_severity if SEVERITIES.include?(nested_severity)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def custom_log_timestamp(line)
|
|
639
|
+
metadata_value(custom_log_metadata(line), :timestamp, :time, :datetime, :created_at)
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def custom_log_severity(line)
|
|
643
|
+
severity = metadata_value(custom_log_metadata(line), :severity, :level, :log_level).to_s.upcase
|
|
644
|
+
severity if SEVERITIES.include?(severity)
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def custom_log_metadata(line)
|
|
648
|
+
self.class.custom_log_metadata(line)
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def metadata_value(metadata, *keys)
|
|
652
|
+
keys.each do |key|
|
|
653
|
+
string_key = key.to_s
|
|
654
|
+
return metadata[string_key] if metadata.key?(string_key)
|
|
655
|
+
return metadata[key] if metadata.key?(key)
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
nil
|
|
143
659
|
end
|
|
144
660
|
|
|
145
661
|
end
|