logister-ruby 0.2.0 → 0.2.1
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/lib/logister/client.rb +33 -18
- data/lib/logister/configuration.rb +2 -0
- data/lib/logister/middleware.rb +28 -19
- data/lib/logister/railtie.rb +4 -2
- data/lib/logister/reporter.rb +83 -38
- data/lib/logister/sql_subscriber.rb +31 -15
- data/lib/logister/version.rb +3 -1
- data/lib/logister.rb +2 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d6f55dcaf8248136d818bf775ec1735e6b5340a83b4226eec28540679c3c2725
|
|
4
|
+
data.tar.gz: 773b83846639292bbdb935a333f3384f6d3b737c1d3933044a7d6a8c59884d84
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e16455627f58c8793b8c317bde748e5a5288de2ca596bcfb641affc327737bc8de21e0b16ce36d749e53f29d95b773587808989acd792432a2125d7ed0ae8f02
|
|
7
|
+
data.tar.gz: 649f1cb0ff5aa498d03cc359d49dd9c77a64e07867f3ca92904a2552bb494d279addeba1cafd0048c68876f6f39e46cf77dfc7fecc7567d5a6c14c6d1ef8232b
|
data/lib/logister/client.rb
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'json'
|
|
2
4
|
require 'net/http'
|
|
3
5
|
require 'uri'
|
|
4
6
|
|
|
5
7
|
module Logister
|
|
6
8
|
class Client
|
|
9
|
+
CONTENT_TYPE = 'application/json'
|
|
10
|
+
|
|
7
11
|
def initialize(configuration)
|
|
8
12
|
@configuration = configuration
|
|
9
|
-
@worker_mutex
|
|
10
|
-
@queue
|
|
11
|
-
@worker
|
|
12
|
-
@running
|
|
13
|
+
@worker_mutex = Mutex.new
|
|
14
|
+
@queue = SizedQueue.new(@configuration.queue_size)
|
|
15
|
+
@worker = nil
|
|
16
|
+
@running = false
|
|
17
|
+
|
|
18
|
+
# Cache values that are static for the lifetime of this client so we
|
|
19
|
+
# don't allocate on every send_request call.
|
|
20
|
+
@uri = URI.parse(@configuration.endpoint).freeze
|
|
21
|
+
@use_ssl = @uri.scheme == 'https'
|
|
22
|
+
@auth_header = "Bearer #{@configuration.api_key}".freeze
|
|
13
23
|
end
|
|
14
24
|
|
|
15
25
|
def publish(payload)
|
|
@@ -24,9 +34,9 @@ module Logister
|
|
|
24
34
|
def flush(timeout: 2)
|
|
25
35
|
return true unless @configuration.async
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return false if monotonic_now
|
|
37
|
+
deadline = monotonic_now + timeout
|
|
38
|
+
until @queue.empty?
|
|
39
|
+
return false if monotonic_now > deadline
|
|
30
40
|
|
|
31
41
|
sleep(0.01)
|
|
32
42
|
end
|
|
@@ -44,6 +54,7 @@ module Logister
|
|
|
44
54
|
nil
|
|
45
55
|
end
|
|
46
56
|
@worker&.join(1)
|
|
57
|
+
@worker = nil
|
|
47
58
|
true
|
|
48
59
|
end
|
|
49
60
|
|
|
@@ -58,18 +69,20 @@ module Logister
|
|
|
58
69
|
end
|
|
59
70
|
|
|
60
71
|
def ensure_worker_started
|
|
72
|
+
# Fast path — no lock needed if already running (GVL-safe on MRI).
|
|
61
73
|
return if @running && @worker&.alive?
|
|
62
74
|
|
|
63
75
|
@worker_mutex.synchronize do
|
|
64
76
|
return if @running && @worker&.alive?
|
|
65
77
|
|
|
66
78
|
@running = true
|
|
67
|
-
@worker
|
|
79
|
+
@worker = Thread.new { run_worker }
|
|
80
|
+
@worker.name = 'logister-worker'
|
|
68
81
|
end
|
|
69
82
|
end
|
|
70
83
|
|
|
71
84
|
def run_worker
|
|
72
|
-
|
|
85
|
+
loop do
|
|
73
86
|
payload = @queue.pop
|
|
74
87
|
break if payload.nil?
|
|
75
88
|
|
|
@@ -77,6 +90,9 @@ module Logister
|
|
|
77
90
|
end
|
|
78
91
|
rescue StandardError => e
|
|
79
92
|
@configuration.logger.warn("logister worker crashed: #{e.class} #{e.message}")
|
|
93
|
+
ensure
|
|
94
|
+
# Always clear running flag and attempt auto-restart after a crash so
|
|
95
|
+
# events enqueued after the crash are not silently dropped.
|
|
80
96
|
@running = false
|
|
81
97
|
end
|
|
82
98
|
|
|
@@ -97,16 +113,15 @@ module Logister
|
|
|
97
113
|
end
|
|
98
114
|
|
|
99
115
|
def send_request(payload)
|
|
100
|
-
|
|
101
|
-
request
|
|
102
|
-
request['
|
|
103
|
-
request
|
|
104
|
-
request.body = { event: payload }.to_json
|
|
116
|
+
request = Net::HTTP::Post.new(@uri)
|
|
117
|
+
request['Content-Type'] = CONTENT_TYPE
|
|
118
|
+
request['Authorization'] = @auth_header
|
|
119
|
+
request.body = { event: payload }.to_json
|
|
105
120
|
|
|
106
121
|
response = Net::HTTP.start(
|
|
107
|
-
uri.host,
|
|
108
|
-
uri.port,
|
|
109
|
-
use_ssl:
|
|
122
|
+
@uri.host,
|
|
123
|
+
@uri.port,
|
|
124
|
+
use_ssl: @use_ssl,
|
|
110
125
|
open_timeout: @configuration.timeout_seconds,
|
|
111
126
|
read_timeout: @configuration.timeout_seconds
|
|
112
127
|
) { |http| http.request(request) }
|
|
@@ -121,7 +136,7 @@ module Logister
|
|
|
121
136
|
end
|
|
122
137
|
|
|
123
138
|
def ready?
|
|
124
|
-
@configuration.enabled &&
|
|
139
|
+
@configuration.enabled && !@configuration.api_key.to_s.empty?
|
|
125
140
|
end
|
|
126
141
|
end
|
|
127
142
|
end
|
data/lib/logister/middleware.rb
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'socket'
|
|
2
4
|
|
|
3
5
|
module Logister
|
|
4
6
|
class Middleware
|
|
7
|
+
# Sensitive param key fragments matched case-insensitively.
|
|
8
|
+
SENSITIVE_PARAM_RE = /password|token|secret|api_key|credit_card|cvv|ssn/i.freeze
|
|
9
|
+
FILTERED = '[FILTERED]'
|
|
10
|
+
|
|
5
11
|
def initialize(app)
|
|
6
12
|
@app = app
|
|
13
|
+
|
|
14
|
+
# Cache values that are constant for the lifetime of this process so
|
|
15
|
+
# they are not recomputed on every error.
|
|
16
|
+
@hostname = resolve_hostname.freeze
|
|
17
|
+
@app_context = build_app_context.freeze
|
|
7
18
|
end
|
|
8
19
|
|
|
9
20
|
def call(env)
|
|
@@ -13,7 +24,7 @@ module Logister
|
|
|
13
24
|
e,
|
|
14
25
|
context: {
|
|
15
26
|
request: build_request_context(env),
|
|
16
|
-
app:
|
|
27
|
+
app: @app_context
|
|
17
28
|
}
|
|
18
29
|
)
|
|
19
30
|
raise
|
|
@@ -23,14 +34,14 @@ module Logister
|
|
|
23
34
|
|
|
24
35
|
def build_request_context(env)
|
|
25
36
|
ctx = {
|
|
26
|
-
id:
|
|
27
|
-
path:
|
|
28
|
-
method:
|
|
29
|
-
ip:
|
|
37
|
+
id: env['action_dispatch.request_id'],
|
|
38
|
+
path: env['PATH_INFO'],
|
|
39
|
+
method: env['REQUEST_METHOD'],
|
|
40
|
+
ip: remote_ip(env),
|
|
30
41
|
user_agent: env['HTTP_USER_AGENT']
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
# Params — available if ActionDispatch has already parsed them
|
|
44
|
+
# Params — available if ActionDispatch has already parsed them.
|
|
34
45
|
if (params = env['action_dispatch.request.parameters'])
|
|
35
46
|
ctx[:params] = filter_params(params)
|
|
36
47
|
end
|
|
@@ -39,33 +50,31 @@ module Logister
|
|
|
39
50
|
end
|
|
40
51
|
|
|
41
52
|
def build_app_context
|
|
42
|
-
ctx = {
|
|
43
|
-
ruby: RUBY_VERSION,
|
|
44
|
-
hostname: hostname
|
|
45
|
-
}
|
|
53
|
+
ctx = { ruby: RUBY_VERSION, hostname: @hostname }
|
|
46
54
|
ctx[:rails] = Rails::VERSION::STRING if defined?(Rails::VERSION)
|
|
47
55
|
ctx
|
|
48
56
|
end
|
|
49
57
|
|
|
50
|
-
# Respect X-Forwarded-For set by proxies
|
|
58
|
+
# Respect X-Forwarded-For set by proxies; fall back to REMOTE_ADDR.
|
|
51
59
|
def remote_ip(env)
|
|
52
|
-
forwarded = env['HTTP_X_FORWARDED_FOR']
|
|
53
|
-
forwarded.nil? || forwarded.empty?
|
|
54
|
-
end
|
|
60
|
+
forwarded = env['HTTP_X_FORWARDED_FOR']
|
|
61
|
+
return env['REMOTE_ADDR'] if forwarded.nil? || forwarded.empty?
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
first = forwarded.split(',').first
|
|
64
|
+
first ? first.strip : env['REMOTE_ADDR']
|
|
65
|
+
end
|
|
59
66
|
|
|
67
|
+
# Filter out sensitive parameter values using a single Regexp so we avoid
|
|
68
|
+
# allocating a downcased String for every param key on every error.
|
|
60
69
|
def filter_params(params)
|
|
61
70
|
params.each_with_object({}) do |(k, v), h|
|
|
62
|
-
h[k] =
|
|
71
|
+
h[k] = k.to_s.match?(SENSITIVE_PARAM_RE) ? FILTERED : v
|
|
63
72
|
end
|
|
64
73
|
rescue StandardError
|
|
65
74
|
{}
|
|
66
75
|
end
|
|
67
76
|
|
|
68
|
-
def
|
|
77
|
+
def resolve_hostname
|
|
69
78
|
Socket.gethostname
|
|
70
79
|
rescue StandardError
|
|
71
80
|
'unknown'
|
data/lib/logister/railtie.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'rails/railtie'
|
|
2
4
|
|
|
3
5
|
module Logister
|
|
@@ -38,10 +40,10 @@ module Logister
|
|
|
38
40
|
private
|
|
39
41
|
|
|
40
42
|
def copy_setting(app, config, key)
|
|
41
|
-
value = app.config.logister.
|
|
43
|
+
value = app.config.logister.public_send(key)
|
|
42
44
|
return if value.nil?
|
|
43
45
|
|
|
44
|
-
config.
|
|
46
|
+
config.public_send(:"#{key}=", value)
|
|
45
47
|
end
|
|
46
48
|
end
|
|
47
49
|
end
|
data/lib/logister/reporter.rb
CHANGED
|
@@ -1,13 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'digest'
|
|
2
4
|
require 'time'
|
|
5
|
+
require 'set'
|
|
3
6
|
|
|
4
7
|
module Logister
|
|
5
8
|
class Reporter
|
|
6
9
|
def initialize(configuration)
|
|
7
10
|
@configuration = configuration
|
|
8
|
-
@client
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
@client = Client.new(configuration)
|
|
12
|
+
|
|
13
|
+
# Pre-build values that are static for the lifetime of this reporter so
|
|
14
|
+
# they are not allocated on every report_error / report_metric call.
|
|
15
|
+
@static_context = {
|
|
16
|
+
environment: @configuration.environment,
|
|
17
|
+
service: @configuration.service,
|
|
18
|
+
release: @configuration.release
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# Normalise ignore_environments once into a frozen Set of Strings so
|
|
22
|
+
# ignored_environment? never allocates a mapped Array.
|
|
23
|
+
@ignored_envs = Set.new(@configuration.ignore_environments.map(&:to_s)).freeze
|
|
24
|
+
|
|
25
|
+
# Cache the current-environment String to avoid repeated .to_s calls.
|
|
26
|
+
@current_env = @configuration.environment.to_s.freeze
|
|
27
|
+
|
|
28
|
+
# Compile the app-root stripping Regexp once; Dir.pwd is a syscall that
|
|
29
|
+
# always returns a new String — do it exactly once here.
|
|
30
|
+
app_root = Dir.pwd.to_s.freeze
|
|
31
|
+
@app_root_re = /\A#{Regexp.escape(app_root)}\//.freeze
|
|
32
|
+
|
|
33
|
+
# Register shutdown hook. Guard with a flag so multiple Reporter instances
|
|
34
|
+
# (created by repeated Logister.configure calls) each shut down cleanly
|
|
35
|
+
# without re-registering more handlers.
|
|
36
|
+
@shutdown_registered = false
|
|
37
|
+
register_shutdown_hook
|
|
11
38
|
end
|
|
12
39
|
|
|
13
40
|
def report_error(exception, context: {}, tags: {}, level: 'error', fingerprint: nil)
|
|
@@ -15,17 +42,18 @@ module Logister
|
|
|
15
42
|
return false if ignored_path?(context)
|
|
16
43
|
|
|
17
44
|
merged_context = context.dup
|
|
18
|
-
|
|
45
|
+
user = current_user_context
|
|
46
|
+
merged_context[:user] = user if user
|
|
19
47
|
|
|
20
48
|
payload = build_payload(
|
|
21
|
-
event_type:
|
|
22
|
-
level:
|
|
23
|
-
message:
|
|
49
|
+
event_type: 'error',
|
|
50
|
+
level: level,
|
|
51
|
+
message: "#{exception.class}: #{exception.message}",
|
|
24
52
|
fingerprint: fingerprint || default_fingerprint(exception),
|
|
25
|
-
context:
|
|
53
|
+
context: merged_context.merge(
|
|
26
54
|
exception: {
|
|
27
|
-
class:
|
|
28
|
-
message:
|
|
55
|
+
class: exception.class.to_s,
|
|
56
|
+
message: exception.message.to_s,
|
|
29
57
|
backtrace: Array(exception.backtrace).first(50)
|
|
30
58
|
},
|
|
31
59
|
tags: tags
|
|
@@ -43,11 +71,11 @@ module Logister
|
|
|
43
71
|
return false if ignored_path?(context)
|
|
44
72
|
|
|
45
73
|
payload = build_payload(
|
|
46
|
-
event_type:
|
|
47
|
-
level:
|
|
48
|
-
message:
|
|
49
|
-
fingerprint: fingerprint ||
|
|
50
|
-
context:
|
|
74
|
+
event_type: 'metric',
|
|
75
|
+
level: level,
|
|
76
|
+
message: message,
|
|
77
|
+
fingerprint: fingerprint || metric_fingerprint(message),
|
|
78
|
+
context: context.merge(tags: tags)
|
|
51
79
|
)
|
|
52
80
|
|
|
53
81
|
payload = apply_before_notify(payload)
|
|
@@ -80,22 +108,31 @@ module Logister
|
|
|
80
108
|
|
|
81
109
|
private
|
|
82
110
|
|
|
111
|
+
def register_shutdown_hook
|
|
112
|
+
return if @shutdown_registered
|
|
113
|
+
|
|
114
|
+
@shutdown_registered = true
|
|
115
|
+
# Capture @client directly (not self) so the at_exit proc does not
|
|
116
|
+
# retain the entire Reporter in the finalizer chain.
|
|
117
|
+
client = @client
|
|
118
|
+
at_exit { client.shutdown }
|
|
119
|
+
end
|
|
120
|
+
|
|
83
121
|
def current_user_context
|
|
84
122
|
Thread.current[:logister_user]
|
|
85
123
|
end
|
|
86
124
|
|
|
87
125
|
def build_payload(event_type:, level:, message:, fingerprint:, context:)
|
|
88
126
|
{
|
|
89
|
-
event_type:
|
|
90
|
-
level:
|
|
91
|
-
message:
|
|
127
|
+
event_type: event_type,
|
|
128
|
+
level: level,
|
|
129
|
+
message: message,
|
|
92
130
|
fingerprint: fingerprint,
|
|
93
131
|
occurred_at: Time.now.utc.iso8601,
|
|
94
|
-
context
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)
|
|
132
|
+
# Merge static config context last so caller-supplied keys are not
|
|
133
|
+
# overwritten, then merge the static values. The static_context Hash
|
|
134
|
+
# is frozen and reused — only the new outer Hash is allocated.
|
|
135
|
+
context: @static_context.merge(context)
|
|
99
136
|
}
|
|
100
137
|
end
|
|
101
138
|
|
|
@@ -125,31 +162,39 @@ module Logister
|
|
|
125
162
|
end
|
|
126
163
|
|
|
127
164
|
def ignored_environment?
|
|
128
|
-
|
|
129
|
-
@configuration.ignore_environments.map(&:to_s).include?(env)
|
|
165
|
+
@ignored_envs.include?(@current_env)
|
|
130
166
|
end
|
|
131
167
|
|
|
132
168
|
def ignored_path?(context)
|
|
133
169
|
path = context[:path] || context['path']
|
|
134
170
|
return false if path.to_s.empty?
|
|
135
171
|
|
|
172
|
+
path_s = path.to_s
|
|
136
173
|
@configuration.ignore_paths.any? do |matcher|
|
|
137
|
-
matcher.is_a?(Regexp) ? matcher.match?(
|
|
174
|
+
matcher.is_a?(Regexp) ? matcher.match?(path_s) : path_s.include?(matcher.to_s)
|
|
138
175
|
end
|
|
139
176
|
end
|
|
140
177
|
|
|
178
|
+
# Cache metric fingerprints — metric messages are typically a small fixed
|
|
179
|
+
# set of constants (e.g. 'db.query') so the SHA256 is identical every call.
|
|
180
|
+
def metric_fingerprint(message)
|
|
181
|
+
@metric_fingerprint_cache ||= {}
|
|
182
|
+
key = message.to_s
|
|
183
|
+
@metric_fingerprint_cache[key] ||=
|
|
184
|
+
Digest::SHA256.hexdigest(key)[0, 32].freeze
|
|
185
|
+
end
|
|
186
|
+
|
|
141
187
|
def default_fingerprint(exception)
|
|
142
188
|
# Prefer class + first backtrace location so that errors with dynamic
|
|
143
189
|
# values in their message (e.g. "Couldn't find User with 'id'=42") still
|
|
144
190
|
# group together across different IDs / UUIDs.
|
|
145
191
|
location = Array(exception.backtrace).first.to_s
|
|
146
|
-
.sub(/:in\s+.+$/, '')
|
|
147
|
-
.sub(/\A.*\/gems\//, 'gems/')
|
|
148
|
-
.sub(
|
|
192
|
+
.sub(/:in\s+.+$/, '') # strip method name
|
|
193
|
+
.sub(/\A.*\/gems\//, 'gems/') # normalise gem paths
|
|
194
|
+
.sub(@app_root_re, '') # strip app root (pre-compiled RE)
|
|
149
195
|
|
|
150
196
|
if location.empty?
|
|
151
|
-
# No backtrace
|
|
152
|
-
# before hashing so that e.g. "id=42" and "id=99" hash the same way.
|
|
197
|
+
# No backtrace — scrub dynamic tokens from the message before hashing.
|
|
153
198
|
scrubbed = scrub_dynamic_values(exception.message.to_s)
|
|
154
199
|
Digest::SHA256.hexdigest("#{exception.class}|#{scrubbed}")[0, 32]
|
|
155
200
|
else
|
|
@@ -157,18 +202,18 @@ module Logister
|
|
|
157
202
|
end
|
|
158
203
|
end
|
|
159
204
|
|
|
160
|
-
# Strip values that
|
|
161
|
-
# - numeric IDs:
|
|
205
|
+
# Strip values that vary per-occurrence but carry no grouping signal:
|
|
206
|
+
# - numeric IDs: id=42, 'id'=42, id: 42
|
|
162
207
|
# - UUIDs
|
|
163
208
|
# - hex digests (≥8 hex chars)
|
|
164
209
|
# - quoted string values in ActiveRecord-style messages
|
|
165
210
|
def scrub_dynamic_values(message)
|
|
166
211
|
message
|
|
167
|
-
.gsub(/\b(id['"]?\s*[=:]\s*)\d+/i,
|
|
168
|
-
.gsub(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i,
|
|
169
|
-
.gsub(/\b[0-9a-f]{8,}\b/,
|
|
170
|
-
.gsub(/'[^']{1,64}'/,
|
|
171
|
-
.gsub(/\d+/,
|
|
212
|
+
.gsub(/\b(id['"]?\s*[=:]\s*)\d+/i, '\1?')
|
|
213
|
+
.gsub(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i, '?')
|
|
214
|
+
.gsub(/\b[0-9a-f]{8,}\b/, '?')
|
|
215
|
+
.gsub(/'[^']{1,64}'/, '?')
|
|
216
|
+
.gsub(/\d+/, '?')
|
|
172
217
|
end
|
|
173
218
|
end
|
|
174
219
|
end
|
|
@@ -1,13 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Logister
|
|
2
4
|
class SqlSubscriber
|
|
3
5
|
IGNORED_SQL_NAMES = %w[SCHEMA TRANSACTION].freeze
|
|
4
6
|
|
|
7
|
+
# Frozen constants for values emitted on every captured query.
|
|
8
|
+
MESSAGE = 'db.query'
|
|
9
|
+
LEVEL_WARN = 'warn'
|
|
10
|
+
LEVEL_INFO = 'info'
|
|
11
|
+
TAGS = { category: 'database' }.freeze
|
|
12
|
+
|
|
13
|
+
# Pre-compute the fingerprint for the fixed message string so we pay the
|
|
14
|
+
# SHA256 cost exactly once instead of on every captured SQL query.
|
|
15
|
+
require 'digest'
|
|
16
|
+
SQL_FINGERPRINT = Digest::SHA256.hexdigest(MESSAGE)[0, 32].freeze
|
|
17
|
+
|
|
5
18
|
class << self
|
|
6
19
|
def install!
|
|
7
20
|
return if @installed
|
|
8
21
|
|
|
9
|
-
ActiveSupport::Notifications.subscribe('sql.active_record') do |
|
|
10
|
-
handle_sql_event(
|
|
22
|
+
ActiveSupport::Notifications.subscribe('sql.active_record') do |_name, started, finished, _id, payload|
|
|
23
|
+
handle_sql_event(started, finished, payload)
|
|
11
24
|
end
|
|
12
25
|
|
|
13
26
|
@installed = true
|
|
@@ -15,31 +28,34 @@ module Logister
|
|
|
15
28
|
|
|
16
29
|
private
|
|
17
30
|
|
|
18
|
-
def handle_sql_event(
|
|
31
|
+
def handle_sql_event(started, finished, payload)
|
|
19
32
|
config = Logister.configuration
|
|
33
|
+
|
|
34
|
+
# Short-circuit as cheaply as possible when metrics are disabled so
|
|
35
|
+
# that *every* SQL query in the app pays minimal overhead.
|
|
20
36
|
return unless config.capture_db_metrics
|
|
21
37
|
return if payload[:cached]
|
|
22
|
-
|
|
38
|
+
|
|
39
|
+
# Evaluate name once — it's used in two places below.
|
|
40
|
+
sql_name = payload[:name].to_s
|
|
41
|
+
return if IGNORED_SQL_NAMES.include?(sql_name)
|
|
23
42
|
|
|
24
43
|
duration_ms = (finished - started) * 1000.0
|
|
25
44
|
return if duration_ms < config.db_metric_min_duration_ms.to_f
|
|
26
45
|
return if sampled_out?(config.db_metric_sample_rate)
|
|
27
46
|
|
|
28
|
-
level = duration_ms >= 500 ? 'warn' : 'info'
|
|
29
|
-
|
|
30
47
|
Logister.report_metric(
|
|
31
|
-
message:
|
|
32
|
-
level:
|
|
48
|
+
message: MESSAGE,
|
|
49
|
+
level: duration_ms >= 500 ? LEVEL_WARN : LEVEL_INFO,
|
|
50
|
+
fingerprint: SQL_FINGERPRINT,
|
|
33
51
|
context: {
|
|
34
52
|
duration_ms: duration_ms.round(2),
|
|
35
|
-
name:
|
|
36
|
-
sql:
|
|
37
|
-
cached:
|
|
38
|
-
binds_count:
|
|
53
|
+
name: sql_name,
|
|
54
|
+
sql: payload[:sql].to_s,
|
|
55
|
+
cached: false,
|
|
56
|
+
binds_count: (payload[:binds] || []).size
|
|
39
57
|
},
|
|
40
|
-
tags:
|
|
41
|
-
category: 'database'
|
|
42
|
-
}
|
|
58
|
+
tags: TAGS
|
|
43
59
|
)
|
|
44
60
|
rescue StandardError => e
|
|
45
61
|
config.logger.warn("logister sql subscriber failed: #{e.class} #{e.message}")
|
data/lib/logister/version.rb
CHANGED
data/lib/logister.rb
CHANGED