logister-ruby 0.1.1 → 0.2.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 +14 -0
- data/lib/generators/logister/templates/logister.rb +5 -0
- data/lib/logister/configuration.rb +6 -1
- data/lib/logister/middleware.rb +56 -3
- data/lib/logister/railtie.rb +7 -0
- data/lib/logister/reporter.rb +52 -2
- data/lib/logister/sql_subscriber.rb +57 -0
- data/lib/logister/version.rb +1 -1
- data/lib/logister.rb +9 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f3f9f67b976e92e535974c8a243b14f011088bca57e5ab3dc0385de6ba4c003
|
|
4
|
+
data.tar.gz: d744d4996b00d27a2e51e54b5b91134f4c0ae049c76270a41c7ef93deec04189
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a8dbbbdde7778369654c287f9076a89efaadf5a3d03bdd5cb064546bac2b2b72bd53d641b424d3b4bc3cefecfce0bfd1b0a9c50714a8c58506b21c6765b8902
|
|
7
|
+
data.tar.gz: 823a8c724d3aee752558e9936c23f924c774157c83a7b997885ba19a8dbf6aa835277506101f2eea341dde05923353f58127f6b94c0d8cbb6711823c5f43c946
|
data/README.md
CHANGED
|
@@ -56,6 +56,20 @@ end
|
|
|
56
56
|
|
|
57
57
|
If Rails is present, the gem installs middleware that reports unhandled exceptions automatically.
|
|
58
58
|
|
|
59
|
+
## Database load metrics (ActiveRecord)
|
|
60
|
+
|
|
61
|
+
You can capture SQL timing metrics using ActiveSupport notifications:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
Logister.configure do |config|
|
|
65
|
+
config.capture_db_metrics = true
|
|
66
|
+
config.db_metric_min_duration_ms = 10.0
|
|
67
|
+
config.db_metric_sample_rate = 1.0
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This emits metric events with `message: "db.query"` and context fields such as `duration_ms`, `name`, `sql`, and `binds_count`.
|
|
72
|
+
|
|
59
73
|
## Manual reporting
|
|
60
74
|
|
|
61
75
|
```ruby
|
|
@@ -17,6 +17,11 @@ Logister.configure do |config|
|
|
|
17
17
|
config.ignore_exceptions = []
|
|
18
18
|
config.ignore_paths = []
|
|
19
19
|
|
|
20
|
+
# Optional ActiveRecord SQL instrumentation.
|
|
21
|
+
config.capture_db_metrics = false
|
|
22
|
+
config.db_metric_min_duration_ms = 10.0
|
|
23
|
+
config.db_metric_sample_rate = 1.0
|
|
24
|
+
|
|
20
25
|
config.before_notify = lambda do |payload|
|
|
21
26
|
payload
|
|
22
27
|
end
|
|
@@ -4,7 +4,8 @@ module Logister
|
|
|
4
4
|
class Configuration
|
|
5
5
|
attr_accessor :api_key, :endpoint, :environment, :service, :release, :enabled, :timeout_seconds, :logger,
|
|
6
6
|
:ignore_exceptions, :ignore_environments, :ignore_paths, :before_notify,
|
|
7
|
-
:async, :queue_size, :max_retries, :retry_base_interval
|
|
7
|
+
:async, :queue_size, :max_retries, :retry_base_interval,
|
|
8
|
+
:capture_db_metrics, :db_metric_min_duration_ms, :db_metric_sample_rate
|
|
8
9
|
|
|
9
10
|
def initialize
|
|
10
11
|
@api_key = ENV['LOGISTER_API_KEY']
|
|
@@ -26,6 +27,10 @@ module Logister
|
|
|
26
27
|
@queue_size = 1000
|
|
27
28
|
@max_retries = 3
|
|
28
29
|
@retry_base_interval = 0.5
|
|
30
|
+
|
|
31
|
+
@capture_db_metrics = false
|
|
32
|
+
@db_metric_min_duration_ms = 0.0
|
|
33
|
+
@db_metric_sample_rate = 1.0
|
|
29
34
|
end
|
|
30
35
|
end
|
|
31
36
|
end
|
data/lib/logister/middleware.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
|
|
1
3
|
module Logister
|
|
2
4
|
class Middleware
|
|
3
5
|
def initialize(app)
|
|
@@ -10,12 +12,63 @@ module Logister
|
|
|
10
12
|
Logister.report_error(
|
|
11
13
|
e,
|
|
12
14
|
context: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
method: env['REQUEST_METHOD']
|
|
15
|
+
request: build_request_context(env),
|
|
16
|
+
app: build_app_context
|
|
16
17
|
}
|
|
17
18
|
)
|
|
18
19
|
raise
|
|
19
20
|
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def build_request_context(env)
|
|
25
|
+
ctx = {
|
|
26
|
+
id: env['action_dispatch.request_id'],
|
|
27
|
+
path: env['PATH_INFO'],
|
|
28
|
+
method: env['REQUEST_METHOD'],
|
|
29
|
+
ip: remote_ip(env),
|
|
30
|
+
user_agent: env['HTTP_USER_AGENT']
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Params — available if ActionDispatch has already parsed them
|
|
34
|
+
if (params = env['action_dispatch.request.parameters'])
|
|
35
|
+
ctx[:params] = filter_params(params)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
ctx.compact
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_app_context
|
|
42
|
+
ctx = {
|
|
43
|
+
ruby: RUBY_VERSION,
|
|
44
|
+
hostname: hostname
|
|
45
|
+
}
|
|
46
|
+
ctx[:rails] = Rails::VERSION::STRING if defined?(Rails::VERSION)
|
|
47
|
+
ctx
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Respect X-Forwarded-For set by proxies, fall back to REMOTE_ADDR
|
|
51
|
+
def remote_ip(env)
|
|
52
|
+
forwarded = env['HTTP_X_FORWARDED_FOR'].to_s.split(',').first&.strip
|
|
53
|
+
forwarded.nil? || forwarded.empty? ? env['REMOTE_ADDR'] : forwarded
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Remove sensitive parameter values the same way Rails does
|
|
57
|
+
SENSITIVE_PARAMS = %w[password password_confirmation token secret api_key
|
|
58
|
+
credit_card cvv ssn].freeze
|
|
59
|
+
|
|
60
|
+
def filter_params(params)
|
|
61
|
+
params.each_with_object({}) do |(k, v), h|
|
|
62
|
+
h[k] = SENSITIVE_PARAMS.any? { |s| k.to_s.downcase.include?(s) } ? '[FILTERED]' : v
|
|
63
|
+
end
|
|
64
|
+
rescue StandardError
|
|
65
|
+
{}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def hostname
|
|
69
|
+
Socket.gethostname
|
|
70
|
+
rescue StandardError
|
|
71
|
+
'unknown'
|
|
72
|
+
end
|
|
20
73
|
end
|
|
21
74
|
end
|
data/lib/logister/railtie.rb
CHANGED
|
@@ -21,6 +21,9 @@ module Logister
|
|
|
21
21
|
copy_setting(app, config, :queue_size)
|
|
22
22
|
copy_setting(app, config, :max_retries)
|
|
23
23
|
copy_setting(app, config, :retry_base_interval)
|
|
24
|
+
copy_setting(app, config, :capture_db_metrics)
|
|
25
|
+
copy_setting(app, config, :db_metric_min_duration_ms)
|
|
26
|
+
copy_setting(app, config, :db_metric_sample_rate)
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
|
|
@@ -28,6 +31,10 @@ module Logister
|
|
|
28
31
|
app.middleware.use Logister::Middleware
|
|
29
32
|
end
|
|
30
33
|
|
|
34
|
+
initializer 'logister.sql_subscriber' do
|
|
35
|
+
Logister::SqlSubscriber.install!
|
|
36
|
+
end
|
|
37
|
+
|
|
31
38
|
private
|
|
32
39
|
|
|
33
40
|
def copy_setting(app, config, key)
|
data/lib/logister/reporter.rb
CHANGED
|
@@ -14,12 +14,15 @@ module Logister
|
|
|
14
14
|
return false if ignored_exception?(exception)
|
|
15
15
|
return false if ignored_path?(context)
|
|
16
16
|
|
|
17
|
+
merged_context = context.dup
|
|
18
|
+
merged_context[:user] = current_user_context if current_user_context
|
|
19
|
+
|
|
17
20
|
payload = build_payload(
|
|
18
21
|
event_type: 'error',
|
|
19
22
|
level: level,
|
|
20
23
|
message: "#{exception.class}: #{exception.message}",
|
|
21
24
|
fingerprint: fingerprint || default_fingerprint(exception),
|
|
22
|
-
context:
|
|
25
|
+
context: merged_context.merge(
|
|
23
26
|
exception: {
|
|
24
27
|
class: exception.class.to_s,
|
|
25
28
|
message: exception.message.to_s,
|
|
@@ -53,6 +56,20 @@ module Logister
|
|
|
53
56
|
@client.publish(payload)
|
|
54
57
|
end
|
|
55
58
|
|
|
59
|
+
# Store user info for the current thread so it is automatically attached to
|
|
60
|
+
# every error reported during this request.
|
|
61
|
+
#
|
|
62
|
+
# Logister.set_user(id: current_user.id, email: current_user.email, name: current_user.name)
|
|
63
|
+
#
|
|
64
|
+
def set_user(id: nil, email: nil, name: nil, **extra)
|
|
65
|
+
ctx = { id: id, email: email, name: name }.merge(extra).compact
|
|
66
|
+
Thread.current[:logister_user] = ctx.empty? ? nil : ctx
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def clear_user
|
|
70
|
+
Thread.current[:logister_user] = nil
|
|
71
|
+
end
|
|
72
|
+
|
|
56
73
|
def flush(timeout: 2)
|
|
57
74
|
@client.flush(timeout: timeout)
|
|
58
75
|
end
|
|
@@ -63,6 +80,10 @@ module Logister
|
|
|
63
80
|
|
|
64
81
|
private
|
|
65
82
|
|
|
83
|
+
def current_user_context
|
|
84
|
+
Thread.current[:logister_user]
|
|
85
|
+
end
|
|
86
|
+
|
|
66
87
|
def build_payload(event_type:, level:, message:, fingerprint:, context:)
|
|
67
88
|
{
|
|
68
89
|
event_type: event_type,
|
|
@@ -118,7 +139,36 @@ module Logister
|
|
|
118
139
|
end
|
|
119
140
|
|
|
120
141
|
def default_fingerprint(exception)
|
|
121
|
-
|
|
142
|
+
# Prefer class + first backtrace location so that errors with dynamic
|
|
143
|
+
# values in their message (e.g. "Couldn't find User with 'id'=42") still
|
|
144
|
+
# group together across different IDs / UUIDs.
|
|
145
|
+
location = Array(exception.backtrace).first.to_s
|
|
146
|
+
.sub(/:in\s+.+$/, '') # strip method name
|
|
147
|
+
.sub(/\A.*\/gems\//, 'gems/') # normalise gem paths
|
|
148
|
+
.sub(/\A#{Regexp.escape(Dir.pwd.to_s)}\//, '') # strip app root
|
|
149
|
+
|
|
150
|
+
if location.empty?
|
|
151
|
+
# No backtrace available — scrub common dynamic tokens from the message
|
|
152
|
+
# before hashing so that e.g. "id=42" and "id=99" hash the same way.
|
|
153
|
+
scrubbed = scrub_dynamic_values(exception.message.to_s)
|
|
154
|
+
Digest::SHA256.hexdigest("#{exception.class}|#{scrubbed}")[0, 32]
|
|
155
|
+
else
|
|
156
|
+
Digest::SHA256.hexdigest("#{exception.class}|#{location}")[0, 32]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Strip values that tend to vary per-occurrence but carry no grouping signal:
|
|
161
|
+
# - numeric IDs: id=42, 'id'=42, id: 42
|
|
162
|
+
# - UUIDs
|
|
163
|
+
# - hex digests (≥8 hex chars)
|
|
164
|
+
# - quoted string values in ActiveRecord-style messages
|
|
165
|
+
def scrub_dynamic_values(message)
|
|
166
|
+
message
|
|
167
|
+
.gsub(/\b(id['"]?\s*[=:]\s*)\d+/i, '\1?')
|
|
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+/, '?')
|
|
122
172
|
end
|
|
123
173
|
end
|
|
124
174
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Logister
|
|
2
|
+
class SqlSubscriber
|
|
3
|
+
IGNORED_SQL_NAMES = %w[SCHEMA TRANSACTION].freeze
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
def install!
|
|
7
|
+
return if @installed
|
|
8
|
+
|
|
9
|
+
ActiveSupport::Notifications.subscribe('sql.active_record') do |name, started, finished, _id, payload|
|
|
10
|
+
handle_sql_event(name, started, finished, payload)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
@installed = true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def handle_sql_event(_name, started, finished, payload)
|
|
19
|
+
config = Logister.configuration
|
|
20
|
+
return unless config.capture_db_metrics
|
|
21
|
+
return if payload[:cached]
|
|
22
|
+
return if IGNORED_SQL_NAMES.include?(payload[:name].to_s)
|
|
23
|
+
|
|
24
|
+
duration_ms = (finished - started) * 1000.0
|
|
25
|
+
return if duration_ms < config.db_metric_min_duration_ms.to_f
|
|
26
|
+
return if sampled_out?(config.db_metric_sample_rate)
|
|
27
|
+
|
|
28
|
+
level = duration_ms >= 500 ? 'warn' : 'info'
|
|
29
|
+
|
|
30
|
+
Logister.report_metric(
|
|
31
|
+
message: 'db.query',
|
|
32
|
+
level: level,
|
|
33
|
+
context: {
|
|
34
|
+
duration_ms: duration_ms.round(2),
|
|
35
|
+
name: payload[:name].to_s,
|
|
36
|
+
sql: payload[:sql].to_s,
|
|
37
|
+
cached: false,
|
|
38
|
+
binds_count: Array(payload[:binds]).size
|
|
39
|
+
},
|
|
40
|
+
tags: {
|
|
41
|
+
category: 'database'
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
config.logger.warn("logister sql subscriber failed: #{e.class} #{e.message}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def sampled_out?(sample_rate)
|
|
49
|
+
rate = sample_rate.to_f
|
|
50
|
+
return true if rate <= 0.0
|
|
51
|
+
return false if rate >= 1.0
|
|
52
|
+
|
|
53
|
+
rand > rate
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/logister/version.rb
CHANGED
data/lib/logister.rb
CHANGED
|
@@ -3,6 +3,7 @@ require_relative 'logister/configuration'
|
|
|
3
3
|
require_relative 'logister/client'
|
|
4
4
|
require_relative 'logister/reporter'
|
|
5
5
|
require_relative 'logister/middleware'
|
|
6
|
+
require_relative 'logister/sql_subscriber'
|
|
6
7
|
|
|
7
8
|
module Logister
|
|
8
9
|
class << self
|
|
@@ -27,6 +28,14 @@ module Logister
|
|
|
27
28
|
reporter.report_metric(**kwargs)
|
|
28
29
|
end
|
|
29
30
|
|
|
31
|
+
def set_user(id: nil, email: nil, name: nil, **extra)
|
|
32
|
+
reporter.set_user(id: id, email: email, name: name, **extra)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def clear_user
|
|
36
|
+
reporter.clear_user
|
|
37
|
+
end
|
|
38
|
+
|
|
30
39
|
def flush(timeout: 2)
|
|
31
40
|
reporter.flush(timeout: timeout)
|
|
32
41
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: logister-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Logister
|
|
@@ -55,6 +55,7 @@ files:
|
|
|
55
55
|
- lib/logister/middleware.rb
|
|
56
56
|
- lib/logister/railtie.rb
|
|
57
57
|
- lib/logister/reporter.rb
|
|
58
|
+
- lib/logister/sql_subscriber.rb
|
|
58
59
|
- lib/logister/version.rb
|
|
59
60
|
- logister-ruby.gemspec
|
|
60
61
|
homepage: https://logister.org
|