log_bench 0.1.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 +7 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +230 -0
- data/Rakefile +10 -0
- data/exe/log_bench +87 -0
- data/lib/log_bench/app/filter.rb +60 -0
- data/lib/log_bench/app/input_handler.rb +245 -0
- data/lib/log_bench/app/main.rb +96 -0
- data/lib/log_bench/app/monitor.rb +59 -0
- data/lib/log_bench/app/renderer/ansi.rb +176 -0
- data/lib/log_bench/app/renderer/details.rb +488 -0
- data/lib/log_bench/app/renderer/header.rb +99 -0
- data/lib/log_bench/app/renderer/main.rb +30 -0
- data/lib/log_bench/app/renderer/request_list.rb +211 -0
- data/lib/log_bench/app/renderer/scrollbar.rb +38 -0
- data/lib/log_bench/app/screen.rb +96 -0
- data/lib/log_bench/app/sort.rb +61 -0
- data/lib/log_bench/app/state.rb +175 -0
- data/lib/log_bench/json_formatter.rb +92 -0
- data/lib/log_bench/log/cache_entry.rb +82 -0
- data/lib/log_bench/log/call_line_entry.rb +60 -0
- data/lib/log_bench/log/collection.rb +98 -0
- data/lib/log_bench/log/entry.rb +108 -0
- data/lib/log_bench/log/file.rb +103 -0
- data/lib/log_bench/log/parser.rb +64 -0
- data/lib/log_bench/log/query_entry.rb +132 -0
- data/lib/log_bench/log/request.rb +90 -0
- data/lib/log_bench/railtie.rb +45 -0
- data/lib/log_bench/version.rb +5 -0
- data/lib/log_bench.rb +26 -0
- data/lib/tasks/log_bench.rake +97 -0
- data/logbench-preview.png +0 -0
- metadata +171 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LogBench
|
4
|
+
module Log
|
5
|
+
class CacheEntry < Entry
|
6
|
+
SQL_OPERATIONS = %w[SELECT INSERT UPDATE DELETE].freeze
|
7
|
+
|
8
|
+
attr_reader :content, :timing
|
9
|
+
|
10
|
+
def initialize(raw_line)
|
11
|
+
super
|
12
|
+
self.type = :cache
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.build(raw_line)
|
16
|
+
return unless parseable?(raw_line)
|
17
|
+
|
18
|
+
entry = Entry.new(raw_line)
|
19
|
+
return unless entry.type == :cache
|
20
|
+
|
21
|
+
new(raw_line)
|
22
|
+
end
|
23
|
+
|
24
|
+
def duration_ms
|
25
|
+
return 0.0 unless timing
|
26
|
+
|
27
|
+
timing.gsub(/[()ms]/, "").to_f
|
28
|
+
end
|
29
|
+
|
30
|
+
def hit?
|
31
|
+
content.include?("CACHE")
|
32
|
+
end
|
33
|
+
|
34
|
+
def miss?
|
35
|
+
!hit?
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_h
|
39
|
+
super.merge(
|
40
|
+
content: content,
|
41
|
+
timing: timing,
|
42
|
+
operation: operation,
|
43
|
+
duration_ms: duration_ms,
|
44
|
+
hit: hit?
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
attr_reader :operation
|
51
|
+
attr_writer :content, :timing, :operation
|
52
|
+
|
53
|
+
def extract_from_json(data)
|
54
|
+
super
|
55
|
+
message = data["message"] || ""
|
56
|
+
return unless cache_message?(data)
|
57
|
+
|
58
|
+
self.content = message.strip
|
59
|
+
extract_timing_and_operation
|
60
|
+
end
|
61
|
+
|
62
|
+
def extract_timing_and_operation
|
63
|
+
clean_content = remove_ansi_codes(content)
|
64
|
+
self.timing = extract_timing(clean_content)
|
65
|
+
self.operation = extract_operation(clean_content)
|
66
|
+
end
|
67
|
+
|
68
|
+
def extract_timing(text)
|
69
|
+
match = text.match(/\(([0-9.]+ms)\)/)
|
70
|
+
match ? match[1] : nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def extract_operation(text)
|
74
|
+
SQL_OPERATIONS.find { |op| text.include?(op) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def remove_ansi_codes(text)
|
78
|
+
text.gsub(/\e\[[0-9;]*m/, "")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LogBench
|
4
|
+
module Log
|
5
|
+
class CallLineEntry < Entry
|
6
|
+
attr_reader :content
|
7
|
+
|
8
|
+
def initialize(raw_line)
|
9
|
+
super
|
10
|
+
self.type = :sql_call_line
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.build(raw_line)
|
14
|
+
return unless parseable?(raw_line)
|
15
|
+
|
16
|
+
entry = Entry.new(raw_line)
|
17
|
+
return unless entry.type == :sql_call_line
|
18
|
+
|
19
|
+
new(raw_line)
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
super.merge(
|
24
|
+
content: content,
|
25
|
+
file_path: file_path,
|
26
|
+
line_number: line_number,
|
27
|
+
method_name: method_name
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_writer :content
|
34
|
+
attr_accessor :file_path, :line_number, :method_name
|
35
|
+
|
36
|
+
def extract_from_json(data)
|
37
|
+
super
|
38
|
+
message = data["message"] || ""
|
39
|
+
return unless call_line_message?(data)
|
40
|
+
|
41
|
+
self.content = message.strip
|
42
|
+
extract_call_info
|
43
|
+
end
|
44
|
+
|
45
|
+
def extract_call_info
|
46
|
+
# Parse call line like " ↳ app/controllers/users_controller.rb:10:in 'UsersController#show'"
|
47
|
+
if content =~ /↳\s+(.+):(\d+):in\s+'(.+)'/
|
48
|
+
self.file_path = $1
|
49
|
+
self.line_number = $2.to_i
|
50
|
+
self.method_name = $3
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def call_line_message?(data)
|
55
|
+
message = data["message"] || ""
|
56
|
+
message.include?("↳")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LogBench
|
4
|
+
module Log
|
5
|
+
class Collection
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
attr_accessor :entries
|
9
|
+
|
10
|
+
def initialize(input)
|
11
|
+
self.entries = parse_input(input)
|
12
|
+
end
|
13
|
+
|
14
|
+
def each(&block)
|
15
|
+
entries.each(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def size
|
19
|
+
entries.size
|
20
|
+
end
|
21
|
+
|
22
|
+
def empty?
|
23
|
+
entries.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
def requests
|
27
|
+
entries.select { |entry| entry.is_a?(Request) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def queries
|
31
|
+
entries.flat_map(&:queries)
|
32
|
+
end
|
33
|
+
|
34
|
+
def cache_operations
|
35
|
+
entries.flat_map(&:cache_operations)
|
36
|
+
end
|
37
|
+
|
38
|
+
def filter_by_method(method)
|
39
|
+
filtered_requests = requests.select { |req| req.method == method.upcase }
|
40
|
+
create_collection_from_requests(filtered_requests)
|
41
|
+
end
|
42
|
+
|
43
|
+
def filter_by_path(path_pattern)
|
44
|
+
filtered_requests = requests.select { |req| req.path.include?(path_pattern) }
|
45
|
+
create_collection_from_requests(filtered_requests)
|
46
|
+
end
|
47
|
+
|
48
|
+
def filter_by_status(status_range)
|
49
|
+
filtered_requests = requests.select { |req| status_range.include?(req.status) }
|
50
|
+
create_collection_from_requests(filtered_requests)
|
51
|
+
end
|
52
|
+
|
53
|
+
def slow_requests(threshold_ms = 1000)
|
54
|
+
filtered_requests = requests.select { |req| req.duration && req.duration > threshold_ms }
|
55
|
+
create_collection_from_requests(filtered_requests)
|
56
|
+
end
|
57
|
+
|
58
|
+
def sort_by_duration
|
59
|
+
sorted_requests = requests.sort_by { |req| -(req.duration || 0) }
|
60
|
+
create_collection_from_requests(sorted_requests)
|
61
|
+
end
|
62
|
+
|
63
|
+
def sort_by_timestamp
|
64
|
+
sorted_requests = requests.sort_by(&:timestamp)
|
65
|
+
create_collection_from_requests(sorted_requests)
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_a
|
69
|
+
entries
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def create_collection_from_requests(requests)
|
75
|
+
new_collection = self.class.new([])
|
76
|
+
new_collection.entries = requests
|
77
|
+
new_collection
|
78
|
+
end
|
79
|
+
|
80
|
+
def parse_input(input)
|
81
|
+
lines = normalize_input(input)
|
82
|
+
parsed_entries = Parser.parse_lines(lines)
|
83
|
+
Parser.group_by_request(parsed_entries)
|
84
|
+
end
|
85
|
+
|
86
|
+
def normalize_input(input)
|
87
|
+
case input
|
88
|
+
when String
|
89
|
+
[input]
|
90
|
+
when Array
|
91
|
+
input.flat_map { |item| normalize_input(item) }
|
92
|
+
else
|
93
|
+
Array(input)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LogBench
|
4
|
+
module Log
|
5
|
+
class Entry
|
6
|
+
attr_reader :type, :raw_line, :request_id, :timestamp
|
7
|
+
|
8
|
+
def initialize(raw_line)
|
9
|
+
self.raw_line = raw_line.strip
|
10
|
+
self.timestamp = Time.now
|
11
|
+
self.type = :unknown
|
12
|
+
parse!
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.build(raw_line)
|
16
|
+
new(raw_line) if parseable?(raw_line)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.parseable?(line)
|
20
|
+
data = JSON.parse(line.strip)
|
21
|
+
data.is_a?(Hash)
|
22
|
+
rescue JSON::ParserError
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
def http_request?
|
27
|
+
type == :http_request
|
28
|
+
end
|
29
|
+
|
30
|
+
def related_log?
|
31
|
+
!http_request?
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_h
|
35
|
+
{
|
36
|
+
raw: raw_line,
|
37
|
+
timestamp: timestamp,
|
38
|
+
request_id: request_id,
|
39
|
+
type: type
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
attr_writer :type, :raw_line, :timestamp, :request_id
|
46
|
+
|
47
|
+
def parse!
|
48
|
+
parse_json
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_json
|
52
|
+
data = JSON.parse(raw_line)
|
53
|
+
return false unless data.is_a?(Hash)
|
54
|
+
|
55
|
+
# extract_from_json returns false if log should be discarded
|
56
|
+
extract_from_json(data)
|
57
|
+
rescue JSON::ParserError
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
def extract_from_json(data)
|
62
|
+
# Discard logs without request_id - they can't be correlated
|
63
|
+
return false unless data["request_id"]
|
64
|
+
|
65
|
+
self.timestamp = parse_timestamp(data["timestamp"])
|
66
|
+
self.request_id = data["request_id"]
|
67
|
+
self.type = determine_json_type(data)
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
def determine_json_type(data)
|
72
|
+
return :http_request if lograge_request?(data)
|
73
|
+
return :cache if cache_message?(data)
|
74
|
+
return :sql if sql_message?(data)
|
75
|
+
return :sql_call_line if call_stack_message?(data)
|
76
|
+
|
77
|
+
:other
|
78
|
+
end
|
79
|
+
|
80
|
+
def lograge_request?(data)
|
81
|
+
data["method"] && data["path"] && data["status"]
|
82
|
+
end
|
83
|
+
|
84
|
+
def sql_message?(data)
|
85
|
+
message = data["message"] || ""
|
86
|
+
%w[SELECT INSERT UPDATE DELETE TRANSACTION BEGIN COMMIT ROLLBACK SAVEPOINT].any? { |op| message.include?(op) }
|
87
|
+
end
|
88
|
+
|
89
|
+
def cache_message?(data)
|
90
|
+
message = data["message"] || ""
|
91
|
+
message.include?("CACHE")
|
92
|
+
end
|
93
|
+
|
94
|
+
def call_stack_message?(data)
|
95
|
+
message = data["message"] || ""
|
96
|
+
message.include?("↳")
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_timestamp(timestamp_str)
|
100
|
+
return Time.now unless timestamp_str
|
101
|
+
|
102
|
+
Time.parse(timestamp_str)
|
103
|
+
rescue ArgumentError
|
104
|
+
Time.now
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LogBench
|
4
|
+
module Log
|
5
|
+
class File
|
6
|
+
attr_reader :path, :last_position
|
7
|
+
|
8
|
+
def initialize(path)
|
9
|
+
self.path = find_log_file(path)
|
10
|
+
self.last_position = 0
|
11
|
+
validate!
|
12
|
+
end
|
13
|
+
|
14
|
+
def requests
|
15
|
+
collection.requests
|
16
|
+
end
|
17
|
+
|
18
|
+
def entries
|
19
|
+
collection.entries
|
20
|
+
end
|
21
|
+
|
22
|
+
def collection
|
23
|
+
@collection ||= Collection.new(lines)
|
24
|
+
end
|
25
|
+
|
26
|
+
def lines
|
27
|
+
@lines ||= read_lines
|
28
|
+
end
|
29
|
+
|
30
|
+
def reload!
|
31
|
+
self.lines = nil
|
32
|
+
self.collection = nil
|
33
|
+
self.last_position = 0
|
34
|
+
end
|
35
|
+
|
36
|
+
def tail(max_lines = 1000)
|
37
|
+
all_lines = read_lines
|
38
|
+
recent_lines = all_lines.last(max_lines)
|
39
|
+
Collection.new(recent_lines)
|
40
|
+
end
|
41
|
+
|
42
|
+
def watch(&block)
|
43
|
+
return enum_for(:watch) unless block_given?
|
44
|
+
|
45
|
+
loop do
|
46
|
+
new_lines = read_new_lines
|
47
|
+
next if new_lines.empty?
|
48
|
+
|
49
|
+
new_collection = Collection.new(new_lines)
|
50
|
+
yield new_collection unless new_collection.empty?
|
51
|
+
|
52
|
+
sleep 0.5
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def size
|
57
|
+
::File.size(path)
|
58
|
+
end
|
59
|
+
|
60
|
+
def exist?
|
61
|
+
::File.exist?(path)
|
62
|
+
end
|
63
|
+
|
64
|
+
def mtime
|
65
|
+
::File.mtime(path)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
attr_writer :path, :last_position
|
71
|
+
|
72
|
+
def read_lines
|
73
|
+
return [] unless exist?
|
74
|
+
|
75
|
+
::File.readlines(path, chomp: true)
|
76
|
+
end
|
77
|
+
|
78
|
+
def read_new_lines
|
79
|
+
return [] unless exist?
|
80
|
+
return [] unless size > last_position
|
81
|
+
|
82
|
+
new_lines = []
|
83
|
+
::File.open(path, "r") do |file|
|
84
|
+
file.seek(last_position)
|
85
|
+
new_lines = file.readlines(chomp: true)
|
86
|
+
self.last_position = file.tell
|
87
|
+
end
|
88
|
+
|
89
|
+
new_lines
|
90
|
+
end
|
91
|
+
|
92
|
+
def find_log_file(path)
|
93
|
+
candidates = [path, "log/development.log"]
|
94
|
+
|
95
|
+
candidates.find { |candidate| ::File.exist?(candidate) } || path
|
96
|
+
end
|
97
|
+
|
98
|
+
def validate!
|
99
|
+
raise Error, "File not found: #{path}" unless exist?
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LogBench
|
4
|
+
module Log
|
5
|
+
class Parser
|
6
|
+
def self.parse_line(raw_line)
|
7
|
+
return unless Entry.parseable?(raw_line)
|
8
|
+
|
9
|
+
entry = Entry.new(raw_line)
|
10
|
+
build_specific_entry(entry)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.parse_lines(lines)
|
14
|
+
lines.map { |line| parse_line(line) }.compact
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.group_by_request(entries)
|
18
|
+
grouped = entries.group_by(&:request_id)
|
19
|
+
build_requests_from_groups(grouped)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.build_specific_entry(entry)
|
23
|
+
case entry.type
|
24
|
+
when :http_request
|
25
|
+
Request.build(entry.raw_line)
|
26
|
+
when :sql
|
27
|
+
QueryEntry.build(entry.raw_line)
|
28
|
+
when :cache
|
29
|
+
CacheEntry.build(entry.raw_line)
|
30
|
+
when :sql_call_line
|
31
|
+
CallLineEntry.build(entry.raw_line)
|
32
|
+
else
|
33
|
+
entry
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.build_requests_from_groups(grouped)
|
38
|
+
requests = []
|
39
|
+
|
40
|
+
grouped.each do |request_id, entries|
|
41
|
+
next unless request_id
|
42
|
+
|
43
|
+
request = find_request_entry(entries)
|
44
|
+
next unless request
|
45
|
+
|
46
|
+
related_logs = find_related_logs(entries)
|
47
|
+
related_logs.each { |log| request.add_related_log(log) }
|
48
|
+
|
49
|
+
requests << request
|
50
|
+
end
|
51
|
+
|
52
|
+
requests.sort_by(&:timestamp)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.find_request_entry(entries)
|
56
|
+
entries.find { |entry| entry.is_a?(Request) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.find_related_logs(entries)
|
60
|
+
entries.reject { |entry| entry.is_a?(Request) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LogBench
|
4
|
+
module Log
|
5
|
+
class QueryEntry < Entry
|
6
|
+
SELECT = "SELECT"
|
7
|
+
INSERT = "INSERT"
|
8
|
+
UPDATE = "UPDATE"
|
9
|
+
DELETE = "DELETE"
|
10
|
+
TRANSACTION = "TRANSACTION"
|
11
|
+
BEGIN_TRANSACTION = "BEGIN"
|
12
|
+
COMMIT = "COMMIT"
|
13
|
+
ROLLBACK = "ROLLBACK"
|
14
|
+
SAVEPOINT = "SAVEPOINT"
|
15
|
+
SQL_OPERATIONS = [SELECT, INSERT, UPDATE, DELETE, TRANSACTION, BEGIN_TRANSACTION, COMMIT, ROLLBACK, SAVEPOINT].freeze
|
16
|
+
|
17
|
+
attr_reader :content, :timing
|
18
|
+
|
19
|
+
def initialize(raw_line)
|
20
|
+
super
|
21
|
+
self.type = :sql
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.build(raw_line)
|
25
|
+
return unless parseable?(raw_line)
|
26
|
+
|
27
|
+
entry = Entry.new(raw_line)
|
28
|
+
return unless entry.type == :sql
|
29
|
+
|
30
|
+
new(raw_line)
|
31
|
+
end
|
32
|
+
|
33
|
+
def duration_ms
|
34
|
+
return 0.0 unless timing
|
35
|
+
|
36
|
+
timing.gsub(/[()ms]/, "").to_f
|
37
|
+
end
|
38
|
+
|
39
|
+
def select?
|
40
|
+
operation == SELECT
|
41
|
+
end
|
42
|
+
|
43
|
+
def insert?
|
44
|
+
operation == INSERT
|
45
|
+
end
|
46
|
+
|
47
|
+
def update?
|
48
|
+
operation == UPDATE
|
49
|
+
end
|
50
|
+
|
51
|
+
def delete?
|
52
|
+
operation == DELETE
|
53
|
+
end
|
54
|
+
|
55
|
+
def transaction?
|
56
|
+
operation == TRANSACTION
|
57
|
+
end
|
58
|
+
|
59
|
+
def begin?
|
60
|
+
operation == BEGIN_TRANSACTION
|
61
|
+
end
|
62
|
+
|
63
|
+
def commit?
|
64
|
+
operation == COMMIT
|
65
|
+
end
|
66
|
+
|
67
|
+
def rollback?
|
68
|
+
operation == ROLLBACK
|
69
|
+
end
|
70
|
+
|
71
|
+
def savepoint?
|
72
|
+
operation == SAVEPOINT
|
73
|
+
end
|
74
|
+
|
75
|
+
def slow?(threshold_ms = 100)
|
76
|
+
duration_ms > threshold_ms
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_h
|
80
|
+
super.merge(
|
81
|
+
content: content,
|
82
|
+
timing: timing,
|
83
|
+
operation: operation,
|
84
|
+
duration_ms: duration_ms,
|
85
|
+
has_ansi: has_ansi_codes?(content)
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def extract_from_json(data)
|
92
|
+
# Call parent method which checks for request_id
|
93
|
+
return false unless super
|
94
|
+
|
95
|
+
message = data["message"] || ""
|
96
|
+
return false unless sql_message?(data)
|
97
|
+
|
98
|
+
self.content = message.strip
|
99
|
+
extract_timing_and_operation
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
def extract_timing_and_operation
|
104
|
+
clean_content = remove_ansi_codes(content)
|
105
|
+
self.timing = extract_timing(clean_content)
|
106
|
+
self.operation = extract_operation(clean_content)
|
107
|
+
end
|
108
|
+
|
109
|
+
def extract_timing(text)
|
110
|
+
match = text.match(/\(([0-9.]+ms)\)/)
|
111
|
+
match ? match[1] : nil
|
112
|
+
end
|
113
|
+
|
114
|
+
def extract_operation(text)
|
115
|
+
SQL_OPERATIONS.find { |op| text.include?(op) }
|
116
|
+
end
|
117
|
+
|
118
|
+
def remove_ansi_codes(text)
|
119
|
+
text.gsub(/\e\[[0-9;]*m/, "")
|
120
|
+
end
|
121
|
+
|
122
|
+
def has_ansi_codes?(text)
|
123
|
+
text.match?(/\e\[[0-9;]*m/)
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
attr_reader :operation
|
129
|
+
attr_writer :content, :timing, :operation
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|