logister-ruby 0.2.0 → 0.2.2
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 +47 -0
- data/lib/logister/active_job_reporter.rb +83 -0
- data/lib/logister/client.rb +33 -18
- data/lib/logister/configuration.rb +12 -1
- data/lib/logister/context_helpers.rb +262 -0
- data/lib/logister/context_store.rb +134 -0
- data/lib/logister/middleware.rb +167 -39
- data/lib/logister/railtie.rb +18 -0
- data/lib/logister/reporter.rb +83 -38
- data/lib/logister/request_subscriber.rb +104 -0
- data/lib/logister/sql_subscriber.rb +31 -15
- data/lib/logister/version.rb +3 -1
- data/lib/logister.rb +24 -8
- metadata +5 -1
data/lib/logister/middleware.rb
CHANGED
|
@@ -1,74 +1,202 @@
|
|
|
1
|
-
|
|
1
|
+
require_relative "context_helpers"
|
|
2
|
+
require_relative "context_store"
|
|
2
3
|
|
|
3
4
|
module Logister
|
|
4
5
|
class Middleware
|
|
6
|
+
FILTERED_HEADER_PLACEHOLDER = "[FILTERED]".freeze
|
|
7
|
+
SENSITIVE_HEADERS = %w[authorization cookie set-cookie x-api-key x-csrf-token].freeze
|
|
8
|
+
|
|
5
9
|
def initialize(app)
|
|
6
10
|
@app = app
|
|
7
11
|
end
|
|
8
12
|
|
|
9
13
|
def call(env)
|
|
14
|
+
Logister::ContextStore.reset_request_scope!
|
|
15
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
10
16
|
@app.call(env)
|
|
11
17
|
rescue StandardError => e
|
|
18
|
+
request = ActionDispatch::Request.new(env)
|
|
19
|
+
request_context = build_request_context(request, env, error: e, started_at: started_at)
|
|
20
|
+
|
|
12
21
|
Logister.report_error(
|
|
13
22
|
e,
|
|
14
|
-
context:
|
|
15
|
-
request: build_request_context(env),
|
|
16
|
-
app: build_app_context
|
|
17
|
-
}
|
|
23
|
+
context: request_context
|
|
18
24
|
)
|
|
19
25
|
raise
|
|
26
|
+
ensure
|
|
27
|
+
request_id = env["action_dispatch.request_id"]
|
|
28
|
+
Logister::ContextStore.clear_request_summary(request_id)
|
|
29
|
+
Logister::ContextStore.reset_request_scope!
|
|
20
30
|
end
|
|
21
31
|
|
|
22
32
|
private
|
|
23
33
|
|
|
24
|
-
def build_request_context(env)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
def build_request_context(request, env, error:, started_at:)
|
|
35
|
+
request_id = env["action_dispatch.request_id"].to_s.presence
|
|
36
|
+
path = request.path.to_s
|
|
37
|
+
method = request.request_method.to_s
|
|
38
|
+
params = request.filtered_parameters.to_h
|
|
39
|
+
headers = extract_headers(env)
|
|
40
|
+
referer = request.referer.to_s.presence || headers["Referer"]
|
|
41
|
+
http_version = env["HTTP_VERSION"].to_s.presence || env["SERVER_PROTOCOL"].to_s.presence
|
|
42
|
+
rails_action = rails_action_name(params)
|
|
43
|
+
response_status = response_status_for(error)
|
|
44
|
+
duration_ms = elapsed_duration_ms(started_at)
|
|
45
|
+
current_user = current_user(env)
|
|
46
|
+
user_context = Logister::ContextHelpers.user_context_for(current_user)
|
|
47
|
+
request_summary = Logister::ContextStore.request_summary(request_id) || {}
|
|
48
|
+
dependencies = collected_dependencies(request: request, env: env)
|
|
49
|
+
breadcrumbs = Logister::ContextStore.breadcrumbs
|
|
50
|
+
feature_flags = Logister::ContextHelpers.resolve_feature_flags(request: request, env: env, user: current_user)
|
|
51
|
+
trace_context = Logister::ContextHelpers.trace_context(headers: headers, env: env)
|
|
52
|
+
client_ip = Logister::ContextHelpers.anonymize_ip(request.ip.to_s.presence)
|
|
53
|
+
|
|
54
|
+
base_context = {
|
|
55
|
+
request_id: request_id,
|
|
56
|
+
path: path,
|
|
57
|
+
method: method,
|
|
58
|
+
clientIp: client_ip,
|
|
59
|
+
headers: headers,
|
|
60
|
+
httpMethod: method,
|
|
61
|
+
httpVersion: http_version,
|
|
62
|
+
params: params,
|
|
63
|
+
railsAction: rails_action,
|
|
64
|
+
referer: referer,
|
|
65
|
+
requestId: request_id,
|
|
66
|
+
url: request.original_url.to_s.presence,
|
|
67
|
+
response: {
|
|
68
|
+
status: request_summary[:status] || response_status,
|
|
69
|
+
contentType: request.content_type.to_s.presence,
|
|
70
|
+
format: request_summary[:format] || request.format&.to_s.presence,
|
|
71
|
+
durationMs: duration_ms
|
|
72
|
+
}.compact,
|
|
73
|
+
route: {
|
|
74
|
+
name: env["action_dispatch.route_name"].to_s.presence,
|
|
75
|
+
pathTemplate: route_path_template(env),
|
|
76
|
+
controller: request_summary[:controller] || route_value(params, "controller"),
|
|
77
|
+
action: request_summary[:action] || route_value(params, "action")
|
|
78
|
+
}.compact,
|
|
79
|
+
performance: {
|
|
80
|
+
dbRuntimeMs: request_summary[:dbRuntimeMs],
|
|
81
|
+
viewRuntimeMs: request_summary[:viewRuntimeMs],
|
|
82
|
+
allocations: request_summary[:allocations]
|
|
83
|
+
}.compact,
|
|
84
|
+
dependencyCalls: dependencies.presence,
|
|
85
|
+
breadcrumbs: breadcrumbs.presence,
|
|
86
|
+
request: {
|
|
87
|
+
clientIp: client_ip,
|
|
88
|
+
headers: headers,
|
|
89
|
+
httpMethod: method,
|
|
90
|
+
httpVersion: http_version,
|
|
91
|
+
params: params,
|
|
92
|
+
railsAction: rails_action,
|
|
93
|
+
referer: referer,
|
|
94
|
+
requestId: request_id,
|
|
95
|
+
url: request.original_url.to_s.presence
|
|
96
|
+
}.compact
|
|
97
|
+
}.compact
|
|
98
|
+
|
|
99
|
+
Logister::ContextHelpers.compact_deep(
|
|
100
|
+
base_context
|
|
101
|
+
.merge(trace_context)
|
|
102
|
+
.merge(feature_flags)
|
|
103
|
+
.merge(user_context)
|
|
104
|
+
.merge(Logister::ContextHelpers.runtime_context)
|
|
105
|
+
.merge(Logister::ContextHelpers.deployment_context)
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def rails_action_name(params)
|
|
110
|
+
return nil unless params.is_a?(Hash)
|
|
111
|
+
|
|
112
|
+
controller_name = params["controller"].to_s.presence || params[:controller].to_s.presence
|
|
113
|
+
action_name = params["action"].to_s.presence || params[:action].to_s.presence
|
|
114
|
+
return nil if controller_name.blank? || action_name.blank?
|
|
115
|
+
|
|
116
|
+
"#{controller_name}##{action_name}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def extract_headers(env)
|
|
120
|
+
headers = {}
|
|
121
|
+
|
|
122
|
+
env.each do |key, value|
|
|
123
|
+
next unless value.is_a?(String)
|
|
124
|
+
|
|
125
|
+
header_name = rack_env_to_header_name(key)
|
|
126
|
+
next unless header_name
|
|
127
|
+
|
|
128
|
+
headers[header_name] = filter_header_value(header_name, value)
|
|
36
129
|
end
|
|
37
130
|
|
|
38
|
-
|
|
131
|
+
headers.sort.to_h
|
|
39
132
|
end
|
|
40
133
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
134
|
+
def rack_env_to_header_name(key)
|
|
135
|
+
if key.start_with?("HTTP_")
|
|
136
|
+
key.delete_prefix("HTTP_").split("_").map(&:capitalize).join("-")
|
|
137
|
+
elsif key == "CONTENT_TYPE"
|
|
138
|
+
"Content-Type"
|
|
139
|
+
elsif key == "CONTENT_LENGTH"
|
|
140
|
+
"Content-Length"
|
|
141
|
+
else
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
48
144
|
end
|
|
49
145
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
146
|
+
def filter_header_value(name, value)
|
|
147
|
+
return FILTERED_HEADER_PLACEHOLDER if SENSITIVE_HEADERS.include?(name.to_s.downcase)
|
|
148
|
+
|
|
149
|
+
value
|
|
54
150
|
end
|
|
55
151
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
152
|
+
def current_user(env)
|
|
153
|
+
controller = env["action_controller.instance"]
|
|
154
|
+
return nil unless controller
|
|
59
155
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
156
|
+
if controller.respond_to?(:current_user)
|
|
157
|
+
controller.public_send(:current_user)
|
|
158
|
+
elsif controller.respond_to?(:current_user, true)
|
|
159
|
+
controller.send(:current_user)
|
|
63
160
|
end
|
|
64
161
|
rescue StandardError
|
|
65
|
-
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def collected_dependencies(request:, env:)
|
|
166
|
+
custom = Logister::ContextHelpers.resolve_dependency_context(request: request, env: env).fetch(:dependencyCalls, [])
|
|
167
|
+
manual = Logister::ContextStore.dependencies
|
|
168
|
+
Array(manual) + Array(custom)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def response_status_for(error)
|
|
172
|
+
return 500 unless defined?(ActionDispatch::ExceptionWrapper)
|
|
173
|
+
|
|
174
|
+
ActionDispatch::ExceptionWrapper.status_code_for_exception(error.class.name)
|
|
175
|
+
rescue StandardError
|
|
176
|
+
500
|
|
66
177
|
end
|
|
67
178
|
|
|
68
|
-
def
|
|
69
|
-
|
|
179
|
+
def elapsed_duration_ms(started_at)
|
|
180
|
+
return nil unless started_at
|
|
181
|
+
|
|
182
|
+
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0).round(2)
|
|
70
183
|
rescue StandardError
|
|
71
|
-
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def route_path_template(env)
|
|
188
|
+
pattern = env["action_dispatch.route_uri_pattern"]
|
|
189
|
+
return pattern.spec.to_s.presence if pattern.respond_to?(:spec)
|
|
190
|
+
|
|
191
|
+
pattern.to_s.presence
|
|
192
|
+
rescue StandardError
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def route_value(params, key)
|
|
197
|
+
return nil unless params.is_a?(Hash)
|
|
198
|
+
|
|
199
|
+
params[key].to_s.presence || params[key.to_sym].to_s.presence
|
|
72
200
|
end
|
|
73
201
|
end
|
|
74
202
|
end
|
data/lib/logister/railtie.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'rails/railtie'
|
|
2
|
+
require_relative 'active_job_reporter'
|
|
2
3
|
|
|
3
4
|
module Logister
|
|
4
5
|
class Railtie < Rails::Railtie
|
|
@@ -24,6 +25,13 @@ module Logister
|
|
|
24
25
|
copy_setting(app, config, :capture_db_metrics)
|
|
25
26
|
copy_setting(app, config, :db_metric_min_duration_ms)
|
|
26
27
|
copy_setting(app, config, :db_metric_sample_rate)
|
|
28
|
+
copy_setting(app, config, :feature_flags_resolver)
|
|
29
|
+
copy_setting(app, config, :dependency_resolver)
|
|
30
|
+
copy_setting(app, config, :anonymize_ip)
|
|
31
|
+
copy_setting(app, config, :max_breadcrumbs)
|
|
32
|
+
copy_setting(app, config, :max_dependencies)
|
|
33
|
+
copy_setting(app, config, :capture_sql_breadcrumbs)
|
|
34
|
+
copy_setting(app, config, :sql_breadcrumb_min_duration_ms)
|
|
27
35
|
end
|
|
28
36
|
end
|
|
29
37
|
|
|
@@ -35,6 +43,16 @@ module Logister
|
|
|
35
43
|
Logister::SqlSubscriber.install!
|
|
36
44
|
end
|
|
37
45
|
|
|
46
|
+
initializer "logister.request_subscriber" do
|
|
47
|
+
Logister::RequestSubscriber.install!
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
initializer "logister.active_job_reporter" do
|
|
51
|
+
ActiveSupport.on_load(:active_job) do
|
|
52
|
+
Logister::ActiveJobReporter.install!
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
38
56
|
private
|
|
39
57
|
|
|
40
58
|
def copy_setting(app, config, key)
|
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
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require "logger"
|
|
2
|
+
|
|
3
|
+
module Logister
|
|
4
|
+
class RequestSubscriber
|
|
5
|
+
IGNORED_SQL_NAMES = %w[SCHEMA TRANSACTION].freeze
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def install!
|
|
9
|
+
return if @installed
|
|
10
|
+
|
|
11
|
+
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |_name, _started, _finished, _id, payload|
|
|
12
|
+
handle_process_action(payload)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, started, finished, _id, payload|
|
|
16
|
+
handle_sql_breadcrumb(started, finished, payload)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@installed = true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def handle_process_action(payload)
|
|
25
|
+
return unless payload.is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
request_id = payload[:request_id].to_s.presence
|
|
28
|
+
return unless request_id
|
|
29
|
+
|
|
30
|
+
Logister::ContextStore.store_request_summary(
|
|
31
|
+
request_id,
|
|
32
|
+
{
|
|
33
|
+
status: payload[:status],
|
|
34
|
+
format: payload[:format].to_s.presence,
|
|
35
|
+
method: payload[:method].to_s.presence,
|
|
36
|
+
path: payload[:path].to_s.presence,
|
|
37
|
+
controller: payload[:controller].to_s.presence,
|
|
38
|
+
action: payload[:action].to_s.presence,
|
|
39
|
+
dbRuntimeMs: numeric(payload[:db_runtime]),
|
|
40
|
+
viewRuntimeMs: numeric(payload[:view_runtime]),
|
|
41
|
+
allocations: payload[:allocations]
|
|
42
|
+
}.compact
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
Logister.add_breadcrumb(
|
|
46
|
+
category: "request",
|
|
47
|
+
message: "#{payload[:controller]}##{payload[:action]} completed",
|
|
48
|
+
data: {
|
|
49
|
+
status: payload[:status],
|
|
50
|
+
method: payload[:method],
|
|
51
|
+
path: payload[:path],
|
|
52
|
+
dbRuntimeMs: numeric(payload[:db_runtime]),
|
|
53
|
+
viewRuntimeMs: numeric(payload[:view_runtime])
|
|
54
|
+
}.compact
|
|
55
|
+
)
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
logger.warn("logister request subscriber (process_action) failed: #{e.class} #{e.message}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def handle_sql_breadcrumb(started, finished, payload)
|
|
61
|
+
config = configuration
|
|
62
|
+
return unless config&.capture_sql_breadcrumbs
|
|
63
|
+
return unless payload.is_a?(Hash)
|
|
64
|
+
return if payload[:cached]
|
|
65
|
+
return if IGNORED_SQL_NAMES.include?(payload[:name].to_s)
|
|
66
|
+
|
|
67
|
+
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
68
|
+
return if duration_ms < config.sql_breadcrumb_min_duration_ms.to_f
|
|
69
|
+
|
|
70
|
+
sql_name = payload[:name].to_s.presence || "SQL"
|
|
71
|
+
Logister.add_breadcrumb(
|
|
72
|
+
category: "db",
|
|
73
|
+
message: "#{sql_name} query",
|
|
74
|
+
data: {
|
|
75
|
+
durationMs: duration_ms,
|
|
76
|
+
sql: payload[:sql].to_s[0, 250]
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
logger.warn("logister request subscriber (sql breadcrumb) failed: #{e.class} #{e.message}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def numeric(value)
|
|
84
|
+
return nil if value.nil?
|
|
85
|
+
|
|
86
|
+
value.to_f.round(2)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def configuration
|
|
90
|
+
return nil unless Logister.respond_to?(:configuration)
|
|
91
|
+
|
|
92
|
+
Logister.configuration
|
|
93
|
+
rescue StandardError
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def logger
|
|
98
|
+
configuration&.logger || Logger.new($stdout)
|
|
99
|
+
rescue StandardError
|
|
100
|
+
Logger.new($stdout)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
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