logister-ruby 0.2.1 → 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/configuration.rb +12 -3
- data/lib/logister/context_helpers.rb +262 -0
- data/lib/logister/context_store.rb +134 -0
- data/lib/logister/middleware.rb +166 -47
- data/lib/logister/railtie.rb +20 -4
- data/lib/logister/request_subscriber.rb +104 -0
- data/lib/logister/version.rb +1 -1
- data/lib/logister.rb +24 -10
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ffb0ff308c1a54f984e8c68088561c76a044e90766bee92a4ef23a51e0c5a78
|
|
4
|
+
data.tar.gz: fd0a658239131bec7061a722ed45044c0a2c7aba2125242b766837d0fd8d5dc8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c89ea19d2cd68d4e7de0d85ff4b8c727fa0378d9a9e3b86e9a631fbb3e1a0df05d8cf7c9620636c9d7581011e53800cd57c9dffd7dabb9f83e52494c04d32c49
|
|
7
|
+
data.tar.gz: 42e48cd2049ab3adf2c496209dd71734891db9134f621920c988c138fb31bd3af99d1a72f39cc2b85f624f8f61ae586b873a036302fe0d29ca767d725cac4e9c
|
data/README.md
CHANGED
|
@@ -23,6 +23,21 @@ Logister.configure do |config|
|
|
|
23
23
|
config.environment = Rails.env
|
|
24
24
|
config.service = Rails.application.class.module_parent_name.underscore
|
|
25
25
|
config.release = ENV["RELEASE_SHA"]
|
|
26
|
+
|
|
27
|
+
# Optional richer context hooks
|
|
28
|
+
config.anonymize_ip = false
|
|
29
|
+
config.max_breadcrumbs = 40
|
|
30
|
+
config.max_dependencies = 20
|
|
31
|
+
config.capture_sql_breadcrumbs = true
|
|
32
|
+
config.sql_breadcrumb_min_duration_ms = 25.0
|
|
33
|
+
|
|
34
|
+
config.feature_flags_resolver = lambda do |request:, user:, **|
|
|
35
|
+
{ new_checkout: user&.respond_to?(:beta?) && user.beta? }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
config.dependency_resolver = lambda do |**|
|
|
39
|
+
[] # or return [{ name:, host:, method:, status:, durationMs:, kind: }]
|
|
40
|
+
end
|
|
26
41
|
end
|
|
27
42
|
```
|
|
28
43
|
|
|
@@ -55,6 +70,7 @@ end
|
|
|
55
70
|
## Rails auto-reporting
|
|
56
71
|
|
|
57
72
|
If Rails is present, the gem installs middleware that reports unhandled exceptions automatically.
|
|
73
|
+
It also attaches richer context (trace IDs, route/response/performance info, breadcrumbs, dependency calls, and user metadata when available).
|
|
58
74
|
|
|
59
75
|
## Database load metrics (ActiveRecord)
|
|
60
76
|
|
|
@@ -70,6 +86,37 @@ end
|
|
|
70
86
|
|
|
71
87
|
This emits metric events with `message: "db.query"` and context fields such as `duration_ms`, `name`, `sql`, and `binds_count`.
|
|
72
88
|
|
|
89
|
+
## Breadcrumbs and dependencies
|
|
90
|
+
|
|
91
|
+
You can add manual breadcrumbs and dependency calls that will be attached to captured errors:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
Logister.add_breadcrumb(
|
|
95
|
+
category: "checkout",
|
|
96
|
+
message: "Starting payment authorization",
|
|
97
|
+
data: { order_id: 123 }
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
Logister.add_dependency(
|
|
101
|
+
name: "stripe.charge",
|
|
102
|
+
host: "api.stripe.com",
|
|
103
|
+
method: "POST",
|
|
104
|
+
status: 200,
|
|
105
|
+
duration_ms: 184.7,
|
|
106
|
+
kind: "http"
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The gem also captures request and SQL breadcrumbs automatically in Rails.
|
|
111
|
+
|
|
112
|
+
## ActiveJob error context
|
|
113
|
+
|
|
114
|
+
Failed ActiveJob executions are auto-reported with `job` context:
|
|
115
|
+
- job class/id/queue/retries/schedule
|
|
116
|
+
- filtered job arguments (using `filter_parameters`)
|
|
117
|
+
- runtime/deployment metadata
|
|
118
|
+
- breadcrumbs/dependency calls collected during the job
|
|
119
|
+
|
|
73
120
|
## Manual reporting
|
|
74
121
|
|
|
75
122
|
```ruby
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require_relative "context_helpers"
|
|
2
|
+
require_relative "context_store"
|
|
3
|
+
|
|
4
|
+
module Logister
|
|
5
|
+
module ActiveJobReporter
|
|
6
|
+
def self.install!
|
|
7
|
+
return unless defined?(ActiveJob::Base)
|
|
8
|
+
return if ActiveJob::Base < Logister::ActiveJobReporter::Instrumentation
|
|
9
|
+
|
|
10
|
+
ActiveJob::Base.include(Logister::ActiveJobReporter::Instrumentation)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module Instrumentation
|
|
14
|
+
extend ActiveSupport::Concern
|
|
15
|
+
|
|
16
|
+
included do
|
|
17
|
+
around_perform do |job, block|
|
|
18
|
+
Logister::ContextStore.reset_request_scope!
|
|
19
|
+
Logister.add_breadcrumb(
|
|
20
|
+
category: "job",
|
|
21
|
+
message: "Starting #{job.class.name}",
|
|
22
|
+
data: { queue: job.queue_name, jobId: job.job_id }
|
|
23
|
+
)
|
|
24
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
block.call
|
|
28
|
+
rescue StandardError => error
|
|
29
|
+
Logister.report_error(
|
|
30
|
+
error,
|
|
31
|
+
context: Logister::ActiveJobReporter.build_job_error_context(job, started_at: started_at)
|
|
32
|
+
)
|
|
33
|
+
raise
|
|
34
|
+
ensure
|
|
35
|
+
Logister::ContextStore.reset_request_scope!
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
def build_job_error_context(job, started_at:)
|
|
44
|
+
Logister::ContextHelpers.compact_deep(
|
|
45
|
+
{
|
|
46
|
+
job: {
|
|
47
|
+
jobClass: job.class.name.to_s,
|
|
48
|
+
jobId: job.job_id.to_s.presence,
|
|
49
|
+
providerJobId: job.provider_job_id.to_s.presence,
|
|
50
|
+
queue: job.queue_name.to_s.presence,
|
|
51
|
+
priority: job.priority,
|
|
52
|
+
executions: job.executions,
|
|
53
|
+
exceptionExecutions: serialize_exception_executions(job),
|
|
54
|
+
locale: job.locale.to_s.presence,
|
|
55
|
+
timezone: (job.respond_to?(:timezone) ? job.timezone.to_s.presence : nil),
|
|
56
|
+
enqueuedAt: (job.respond_to?(:enqueued_at) ? time_to_iso8601(job.enqueued_at) : nil),
|
|
57
|
+
scheduledAt: time_to_iso8601(job.scheduled_at),
|
|
58
|
+
arguments: Logister::ContextHelpers.filtered_job_arguments(job)
|
|
59
|
+
}.compact,
|
|
60
|
+
breadcrumbs: Logister::ContextStore.breadcrumbs.presence,
|
|
61
|
+
dependencyCalls: Logister::ContextStore.dependencies.presence,
|
|
62
|
+
runtime: Logister::ContextHelpers.runtime_context[:runtime],
|
|
63
|
+
deployment: Logister::ContextHelpers.deployment_context[:deployment]
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def serialize_exception_executions(job)
|
|
69
|
+
raw = job.respond_to?(:exception_executions) ? job.exception_executions : nil
|
|
70
|
+
return nil if raw.nil?
|
|
71
|
+
|
|
72
|
+
raw.is_a?(Hash) ? raw.transform_keys(&:to_s) : raw
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def time_to_iso8601(value)
|
|
76
|
+
return nil unless value.respond_to?(:iso8601)
|
|
77
|
+
|
|
78
|
+
value.iso8601
|
|
79
|
+
rescue StandardError
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
require 'logger'
|
|
4
2
|
|
|
5
3
|
module Logister
|
|
@@ -7,7 +5,10 @@ module Logister
|
|
|
7
5
|
attr_accessor :api_key, :endpoint, :environment, :service, :release, :enabled, :timeout_seconds, :logger,
|
|
8
6
|
:ignore_exceptions, :ignore_environments, :ignore_paths, :before_notify,
|
|
9
7
|
:async, :queue_size, :max_retries, :retry_base_interval,
|
|
10
|
-
:capture_db_metrics, :db_metric_min_duration_ms, :db_metric_sample_rate
|
|
8
|
+
:capture_db_metrics, :db_metric_min_duration_ms, :db_metric_sample_rate,
|
|
9
|
+
:feature_flags_resolver, :dependency_resolver, :anonymize_ip,
|
|
10
|
+
:max_breadcrumbs, :max_dependencies,
|
|
11
|
+
:capture_sql_breadcrumbs, :sql_breadcrumb_min_duration_ms
|
|
11
12
|
|
|
12
13
|
def initialize
|
|
13
14
|
@api_key = ENV['LOGISTER_API_KEY']
|
|
@@ -33,6 +34,14 @@ module Logister
|
|
|
33
34
|
@capture_db_metrics = false
|
|
34
35
|
@db_metric_min_duration_ms = 0.0
|
|
35
36
|
@db_metric_sample_rate = 1.0
|
|
37
|
+
|
|
38
|
+
@feature_flags_resolver = nil
|
|
39
|
+
@dependency_resolver = nil
|
|
40
|
+
@anonymize_ip = false
|
|
41
|
+
@max_breadcrumbs = 40
|
|
42
|
+
@max_dependencies = 20
|
|
43
|
+
@capture_sql_breadcrumbs = true
|
|
44
|
+
@sql_breadcrumb_min_duration_ms = 25.0
|
|
36
45
|
end
|
|
37
46
|
end
|
|
38
47
|
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
require "socket"
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module Logister
|
|
6
|
+
module ContextHelpers
|
|
7
|
+
FILTERED_VALUE = "[FILTERED]".freeze
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def runtime_context
|
|
12
|
+
{
|
|
13
|
+
runtime: {
|
|
14
|
+
rubyVersion: RUBY_VERSION,
|
|
15
|
+
railsVersion: defined?(Rails) ? Rails.version : nil,
|
|
16
|
+
rackVersion: defined?(Rack) ? Rack.release : nil,
|
|
17
|
+
platform: RUBY_PLATFORM
|
|
18
|
+
}.compact
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def deployment_context
|
|
23
|
+
config = Logister.configuration if Logister.respond_to?(:configuration)
|
|
24
|
+
environment = config&.respond_to?(:environment) ? config.environment.to_s.presence : nil
|
|
25
|
+
service = config&.respond_to?(:service) ? config.service.to_s.presence : nil
|
|
26
|
+
release = config&.respond_to?(:release) ? config.release.to_s.presence : nil
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
deployment: {
|
|
30
|
+
environment: environment || ENV["RAILS_ENV"].to_s.presence || ENV["RACK_ENV"].to_s.presence || "development",
|
|
31
|
+
service: service || ENV["LOGISTER_SERVICE"].to_s.presence || "ruby-app",
|
|
32
|
+
release: release || ENV["LOGISTER_RELEASE"].to_s.presence,
|
|
33
|
+
region: ENV["FLY_REGION"].to_s.presence || ENV["RAILS_REGION"].to_s.presence || ENV["AWS_REGION"].to_s.presence,
|
|
34
|
+
hostname: Socket.gethostname.to_s.presence,
|
|
35
|
+
processPid: Process.pid
|
|
36
|
+
}.compact
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def trace_context(headers:, env:)
|
|
41
|
+
traceparent = header_value(headers, "Traceparent")
|
|
42
|
+
b3_trace_id = header_value(headers, "X-B3-Traceid")
|
|
43
|
+
b3_span_id = header_value(headers, "X-B3-Spanid")
|
|
44
|
+
datadog_trace_id = header_value(headers, "X-Datadog-Trace-Id")
|
|
45
|
+
datadog_parent_id = header_value(headers, "X-Datadog-Parent-Id")
|
|
46
|
+
amzn_trace_id = header_value(headers, "X-Amzn-Trace-Id")
|
|
47
|
+
|
|
48
|
+
parsed_trace_id, parsed_span_id, parsed_sampled = parse_traceparent(traceparent)
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
trace: {
|
|
52
|
+
traceId: parsed_trace_id || b3_trace_id || datadog_trace_id,
|
|
53
|
+
spanId: parsed_span_id || b3_span_id || datadog_parent_id,
|
|
54
|
+
sampled: parsed_sampled,
|
|
55
|
+
traceparent: traceparent,
|
|
56
|
+
requestId: env["action_dispatch.request_id"].to_s.presence,
|
|
57
|
+
amznTraceId: amzn_trace_id
|
|
58
|
+
}.compact
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def resolve_feature_flags(request:, env:, user:)
|
|
63
|
+
resolver = configuration_value(:feature_flags_resolver)
|
|
64
|
+
return {} unless resolver.respond_to?(:call)
|
|
65
|
+
|
|
66
|
+
raw = call_resolver(resolver, request: request, env: env, user: user)
|
|
67
|
+
flags = normalize_flags_hash(raw)
|
|
68
|
+
return {} if flags.empty?
|
|
69
|
+
|
|
70
|
+
{ featureFlags: flags }
|
|
71
|
+
rescue StandardError
|
|
72
|
+
{}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def resolve_dependency_context(request:, env:)
|
|
76
|
+
resolver = configuration_value(:dependency_resolver)
|
|
77
|
+
return {} unless resolver.respond_to?(:call)
|
|
78
|
+
|
|
79
|
+
raw = call_resolver(resolver, request: request, env: env)
|
|
80
|
+
list = normalize_dependency_list(raw)
|
|
81
|
+
return {} if list.empty?
|
|
82
|
+
|
|
83
|
+
{ dependencyCalls: list }
|
|
84
|
+
rescue StandardError
|
|
85
|
+
{}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def anonymize_ip(ip)
|
|
89
|
+
return nil if ip.to_s.strip.empty?
|
|
90
|
+
return ip.to_s unless configuration_value(:anonymize_ip, false)
|
|
91
|
+
|
|
92
|
+
parsed = IPAddr.new(ip.to_s)
|
|
93
|
+
if parsed.ipv4?
|
|
94
|
+
segments = ip.to_s.split(".")
|
|
95
|
+
return ip.to_s if segments.size != 4
|
|
96
|
+
|
|
97
|
+
"#{segments[0]}.#{segments[1]}.#{segments[2]}.0"
|
|
98
|
+
else
|
|
99
|
+
"#{parsed.mask(64).to_s}/64"
|
|
100
|
+
end
|
|
101
|
+
rescue StandardError
|
|
102
|
+
ip.to_s
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def user_context_for(user)
|
|
106
|
+
return {} unless user
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
user: {
|
|
110
|
+
id: safe_call(user, :id).to_s.presence,
|
|
111
|
+
class: user.class.name.to_s.presence,
|
|
112
|
+
email_hash: hashed_email(user),
|
|
113
|
+
role: safe_call(user, :role).to_s.presence,
|
|
114
|
+
account_id: safe_call(user, :account_id).to_s.presence || safe_call(user, :tenant_id).to_s.presence
|
|
115
|
+
}.compact
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def filtered_job_arguments(job)
|
|
120
|
+
arguments = Array(job.arguments)
|
|
121
|
+
return arguments if arguments.empty?
|
|
122
|
+
|
|
123
|
+
filter = ActiveSupport::ParameterFilter.new(
|
|
124
|
+
Array(Rails.application.config.filter_parameters)
|
|
125
|
+
)
|
|
126
|
+
arguments.map { |argument| filter_argument(argument, filter) }
|
|
127
|
+
rescue StandardError
|
|
128
|
+
arguments
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def safe_call(object, method_name)
|
|
132
|
+
return nil unless object.respond_to?(method_name)
|
|
133
|
+
|
|
134
|
+
object.public_send(method_name)
|
|
135
|
+
rescue StandardError
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def hash_value(value)
|
|
140
|
+
return nil if value.to_s.strip.empty?
|
|
141
|
+
|
|
142
|
+
Digest::SHA256.hexdigest(value.to_s.strip.downcase)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def compact_deep(value)
|
|
146
|
+
case value
|
|
147
|
+
when Hash
|
|
148
|
+
value.each_with_object({}) do |(key, nested), acc|
|
|
149
|
+
compacted = compact_deep(nested)
|
|
150
|
+
next if blank_value?(compacted)
|
|
151
|
+
|
|
152
|
+
acc[key] = compacted
|
|
153
|
+
end
|
|
154
|
+
when Array
|
|
155
|
+
value.map { |item| compact_deep(item) }.reject { |item| blank_value?(item) }
|
|
156
|
+
else
|
|
157
|
+
value
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def blank_value?(value)
|
|
162
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def hashed_email(user)
|
|
166
|
+
email = safe_call(user, :email)
|
|
167
|
+
hash_value(email)
|
|
168
|
+
end
|
|
169
|
+
private_class_method :hashed_email
|
|
170
|
+
|
|
171
|
+
def filter_argument(argument, filter)
|
|
172
|
+
case argument
|
|
173
|
+
when Hash
|
|
174
|
+
filter.filter(argument)
|
|
175
|
+
when Array
|
|
176
|
+
argument.map { |nested| filter_argument(nested, filter) }
|
|
177
|
+
else
|
|
178
|
+
argument
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
private_class_method :filter_argument
|
|
182
|
+
|
|
183
|
+
def header_value(headers, key)
|
|
184
|
+
return nil unless headers.is_a?(Hash)
|
|
185
|
+
|
|
186
|
+
headers[key].presence || headers[key.downcase].presence || headers[key.upcase].presence
|
|
187
|
+
end
|
|
188
|
+
private_class_method :header_value
|
|
189
|
+
|
|
190
|
+
def parse_traceparent(traceparent)
|
|
191
|
+
return [ nil, nil, nil ] if traceparent.to_s.empty?
|
|
192
|
+
|
|
193
|
+
parts = traceparent.to_s.split("-")
|
|
194
|
+
return [ nil, nil, nil ] unless parts.size == 4
|
|
195
|
+
|
|
196
|
+
trace_id = parts[1].to_s
|
|
197
|
+
span_id = parts[2].to_s
|
|
198
|
+
flags = parts[3].to_s
|
|
199
|
+
sampled = flags.end_with?("01")
|
|
200
|
+
|
|
201
|
+
[ trace_id.presence, span_id.presence, sampled ]
|
|
202
|
+
rescue StandardError
|
|
203
|
+
[ nil, nil, nil ]
|
|
204
|
+
end
|
|
205
|
+
private_class_method :parse_traceparent
|
|
206
|
+
|
|
207
|
+
def normalize_flags_hash(raw)
|
|
208
|
+
case raw
|
|
209
|
+
when Hash
|
|
210
|
+
raw.each_with_object({}) do |(key, value), acc|
|
|
211
|
+
acc[key.to_s] = value
|
|
212
|
+
end
|
|
213
|
+
else
|
|
214
|
+
{}
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
private_class_method :normalize_flags_hash
|
|
218
|
+
|
|
219
|
+
def normalize_dependency_list(raw)
|
|
220
|
+
list = case raw
|
|
221
|
+
when Array then raw
|
|
222
|
+
when Hash then [ raw ]
|
|
223
|
+
else []
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
list.map do |item|
|
|
227
|
+
next unless item.is_a?(Hash)
|
|
228
|
+
|
|
229
|
+
{
|
|
230
|
+
name: item[:name] || item["name"],
|
|
231
|
+
host: item[:host] || item["host"],
|
|
232
|
+
method: item[:method] || item["method"],
|
|
233
|
+
status: item[:status] || item["status"],
|
|
234
|
+
durationMs: item[:durationMs] || item["durationMs"] || item[:duration_ms] || item["duration_ms"],
|
|
235
|
+
kind: item[:kind] || item["kind"],
|
|
236
|
+
error: item[:error] || item["error"]
|
|
237
|
+
}.compact
|
|
238
|
+
end.compact
|
|
239
|
+
end
|
|
240
|
+
private_class_method :normalize_dependency_list
|
|
241
|
+
|
|
242
|
+
def configuration_value(key, fallback = nil)
|
|
243
|
+
return fallback unless Logister.respond_to?(:configuration)
|
|
244
|
+
|
|
245
|
+
Logister.configuration.public_send(key)
|
|
246
|
+
rescue StandardError
|
|
247
|
+
fallback
|
|
248
|
+
end
|
|
249
|
+
private_class_method :configuration_value
|
|
250
|
+
|
|
251
|
+
def call_resolver(resolver, **kwargs)
|
|
252
|
+
if resolver.arity == 1
|
|
253
|
+
resolver.call(kwargs)
|
|
254
|
+
else
|
|
255
|
+
resolver.call(**kwargs)
|
|
256
|
+
end
|
|
257
|
+
rescue ArgumentError
|
|
258
|
+
resolver.call(kwargs[:request], kwargs[:env], kwargs[:user])
|
|
259
|
+
end
|
|
260
|
+
private_class_method :call_resolver
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module Logister
|
|
2
|
+
module ContextStore
|
|
3
|
+
REQUEST_SCOPE_KEY = :__logister_request_scope
|
|
4
|
+
MAX_REQUEST_SUMMARIES = 200
|
|
5
|
+
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def reset_request_scope!
|
|
9
|
+
Thread.current[REQUEST_SCOPE_KEY] = {
|
|
10
|
+
breadcrumbs: [],
|
|
11
|
+
dependencies: []
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add_breadcrumb(category:, message:, data: {}, level: "info", timestamp: Time.now.utc.iso8601)
|
|
16
|
+
scope = request_scope
|
|
17
|
+
breadcrumbs = scope[:breadcrumbs]
|
|
18
|
+
breadcrumbs << {
|
|
19
|
+
category: category.to_s,
|
|
20
|
+
message: message.to_s,
|
|
21
|
+
level: level.to_s,
|
|
22
|
+
timestamp: timestamp,
|
|
23
|
+
data: sanitize_hash(data)
|
|
24
|
+
}.compact
|
|
25
|
+
trim_collection!(breadcrumbs, max_breadcrumbs)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def breadcrumbs
|
|
29
|
+
request_scope[:breadcrumbs].dup
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def add_dependency(name:, host: nil, method: nil, status: nil, duration_ms: nil, kind: nil, data: {})
|
|
33
|
+
scope = request_scope
|
|
34
|
+
deps = scope[:dependencies]
|
|
35
|
+
deps << sanitize_hash(
|
|
36
|
+
{
|
|
37
|
+
name: name.to_s.presence,
|
|
38
|
+
host: host.to_s.presence,
|
|
39
|
+
method: method.to_s.presence,
|
|
40
|
+
status: status,
|
|
41
|
+
durationMs: duration_ms && duration_ms.to_f.round(2),
|
|
42
|
+
kind: kind.to_s.presence,
|
|
43
|
+
data: sanitize_hash(data)
|
|
44
|
+
}.compact
|
|
45
|
+
)
|
|
46
|
+
trim_collection!(deps, max_dependencies)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def dependencies
|
|
50
|
+
request_scope[:dependencies].dup
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def store_request_summary(request_id, summary)
|
|
54
|
+
return if request_id.to_s.empty?
|
|
55
|
+
|
|
56
|
+
cache = request_summaries
|
|
57
|
+
cache[request_id.to_s] = sanitize_hash(summary)
|
|
58
|
+
trim_hash!(cache, MAX_REQUEST_SUMMARIES)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def request_summary(request_id)
|
|
62
|
+
return nil if request_id.to_s.empty?
|
|
63
|
+
|
|
64
|
+
request_summaries[request_id.to_s]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def clear_request_summary(request_id)
|
|
68
|
+
request_summaries.delete(request_id.to_s)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def add_manual_dependency(**kwargs)
|
|
72
|
+
add_dependency(**kwargs)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def add_manual_breadcrumb(**kwargs)
|
|
76
|
+
add_breadcrumb(**kwargs)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def request_scope
|
|
80
|
+
Thread.current[REQUEST_SCOPE_KEY] ||= {
|
|
81
|
+
breadcrumbs: [],
|
|
82
|
+
dependencies: []
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
private_class_method :request_scope
|
|
86
|
+
|
|
87
|
+
def request_summaries
|
|
88
|
+
Thread.current[:__logister_request_summaries] ||= {}
|
|
89
|
+
end
|
|
90
|
+
private_class_method :request_summaries
|
|
91
|
+
|
|
92
|
+
def sanitize_hash(value)
|
|
93
|
+
return {} unless value.is_a?(Hash)
|
|
94
|
+
|
|
95
|
+
value.each_with_object({}) do |(key, nested), acc|
|
|
96
|
+
acc[key] = nested
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
private_class_method :sanitize_hash
|
|
100
|
+
|
|
101
|
+
def max_breadcrumbs
|
|
102
|
+
config_value(:max_breadcrumbs, 40).to_i.clamp(1, 500)
|
|
103
|
+
end
|
|
104
|
+
private_class_method :max_breadcrumbs
|
|
105
|
+
|
|
106
|
+
def max_dependencies
|
|
107
|
+
config_value(:max_dependencies, 20).to_i.clamp(1, 500)
|
|
108
|
+
end
|
|
109
|
+
private_class_method :max_dependencies
|
|
110
|
+
|
|
111
|
+
def trim_collection!(array, max_size)
|
|
112
|
+
overflow = array.size - max_size
|
|
113
|
+
array.shift(overflow) if overflow.positive?
|
|
114
|
+
end
|
|
115
|
+
private_class_method :trim_collection!
|
|
116
|
+
|
|
117
|
+
def trim_hash!(hash, max_size)
|
|
118
|
+
overflow = hash.size - max_size
|
|
119
|
+
return unless overflow.positive?
|
|
120
|
+
|
|
121
|
+
hash.keys.first(overflow).each { |key| hash.delete(key) }
|
|
122
|
+
end
|
|
123
|
+
private_class_method :trim_hash!
|
|
124
|
+
|
|
125
|
+
def config_value(key, fallback)
|
|
126
|
+
return fallback unless Logister.respond_to?(:configuration)
|
|
127
|
+
|
|
128
|
+
Logister.configuration.public_send(key)
|
|
129
|
+
rescue StandardError
|
|
130
|
+
fallback
|
|
131
|
+
end
|
|
132
|
+
private_class_method :config_value
|
|
133
|
+
end
|
|
134
|
+
end
|
data/lib/logister/middleware.rb
CHANGED
|
@@ -1,83 +1,202 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require 'socket'
|
|
1
|
+
require_relative "context_helpers"
|
|
2
|
+
require_relative "context_store"
|
|
4
3
|
|
|
5
4
|
module Logister
|
|
6
5
|
class Middleware
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
FILTERED = '[FILTERED]'
|
|
6
|
+
FILTERED_HEADER_PLACEHOLDER = "[FILTERED]".freeze
|
|
7
|
+
SENSITIVE_HEADERS = %w[authorization cookie set-cookie x-api-key x-csrf-token].freeze
|
|
10
8
|
|
|
11
9
|
def initialize(app)
|
|
12
10
|
@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
|
|
18
11
|
end
|
|
19
12
|
|
|
20
13
|
def call(env)
|
|
14
|
+
Logister::ContextStore.reset_request_scope!
|
|
15
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
21
16
|
@app.call(env)
|
|
22
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
|
+
|
|
23
21
|
Logister.report_error(
|
|
24
22
|
e,
|
|
25
|
-
context:
|
|
26
|
-
request: build_request_context(env),
|
|
27
|
-
app: @app_context
|
|
28
|
-
}
|
|
23
|
+
context: request_context
|
|
29
24
|
)
|
|
30
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!
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
private
|
|
34
33
|
|
|
35
|
-
def build_request_context(env)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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)
|
|
47
129
|
end
|
|
48
130
|
|
|
49
|
-
|
|
131
|
+
headers.sort.to_h
|
|
50
132
|
end
|
|
51
133
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
56
144
|
end
|
|
57
145
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
forwarded = env['HTTP_X_FORWARDED_FOR']
|
|
61
|
-
return env['REMOTE_ADDR'] if forwarded.nil? || forwarded.empty?
|
|
146
|
+
def filter_header_value(name, value)
|
|
147
|
+
return FILTERED_HEADER_PLACEHOLDER if SENSITIVE_HEADERS.include?(name.to_s.downcase)
|
|
62
148
|
|
|
63
|
-
|
|
64
|
-
first ? first.strip : env['REMOTE_ADDR']
|
|
149
|
+
value
|
|
65
150
|
end
|
|
66
151
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
152
|
+
def current_user(env)
|
|
153
|
+
controller = env["action_controller.instance"]
|
|
154
|
+
return nil unless controller
|
|
155
|
+
|
|
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)
|
|
72
160
|
end
|
|
73
161
|
rescue StandardError
|
|
74
|
-
|
|
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
|
|
75
177
|
end
|
|
76
178
|
|
|
77
|
-
def
|
|
78
|
-
|
|
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)
|
|
79
183
|
rescue StandardError
|
|
80
|
-
|
|
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
|
|
81
200
|
end
|
|
82
201
|
end
|
|
83
202
|
end
|
data/lib/logister/railtie.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
require 'rails/railtie'
|
|
2
|
+
require_relative 'active_job_reporter'
|
|
4
3
|
|
|
5
4
|
module Logister
|
|
6
5
|
class Railtie < Rails::Railtie
|
|
@@ -26,6 +25,13 @@ module Logister
|
|
|
26
25
|
copy_setting(app, config, :capture_db_metrics)
|
|
27
26
|
copy_setting(app, config, :db_metric_min_duration_ms)
|
|
28
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)
|
|
29
35
|
end
|
|
30
36
|
end
|
|
31
37
|
|
|
@@ -37,13 +43,23 @@ module Logister
|
|
|
37
43
|
Logister::SqlSubscriber.install!
|
|
38
44
|
end
|
|
39
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
|
+
|
|
40
56
|
private
|
|
41
57
|
|
|
42
58
|
def copy_setting(app, config, key)
|
|
43
|
-
value = app.config.logister.
|
|
59
|
+
value = app.config.logister.send(key)
|
|
44
60
|
return if value.nil?
|
|
45
61
|
|
|
46
|
-
config.
|
|
62
|
+
config.send("#{key}=", value)
|
|
47
63
|
end
|
|
48
64
|
end
|
|
49
65
|
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
|
data/lib/logister/version.rb
CHANGED
data/lib/logister.rb
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
require_relative 'logister/version'
|
|
4
2
|
require_relative 'logister/configuration'
|
|
5
3
|
require_relative 'logister/client'
|
|
6
4
|
require_relative 'logister/reporter'
|
|
5
|
+
require_relative 'logister/context_helpers'
|
|
6
|
+
require_relative 'logister/context_store'
|
|
7
7
|
require_relative 'logister/middleware'
|
|
8
8
|
require_relative 'logister/sql_subscriber'
|
|
9
|
+
require_relative 'logister/request_subscriber'
|
|
9
10
|
|
|
10
11
|
module Logister
|
|
11
12
|
class << self
|
|
@@ -30,14 +31,6 @@ module Logister
|
|
|
30
31
|
reporter.report_metric(**kwargs)
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
def set_user(id: nil, email: nil, name: nil, **extra)
|
|
34
|
-
reporter.set_user(id: id, email: email, name: name, **extra)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def clear_user
|
|
38
|
-
reporter.clear_user
|
|
39
|
-
end
|
|
40
|
-
|
|
41
34
|
def flush(timeout: 2)
|
|
42
35
|
reporter.flush(timeout: timeout)
|
|
43
36
|
end
|
|
@@ -45,6 +38,27 @@ module Logister
|
|
|
45
38
|
def shutdown
|
|
46
39
|
reporter.shutdown
|
|
47
40
|
end
|
|
41
|
+
|
|
42
|
+
def add_breadcrumb(category:, message:, data: {}, level: "info")
|
|
43
|
+
ContextStore.add_manual_breadcrumb(
|
|
44
|
+
category: category,
|
|
45
|
+
message: message,
|
|
46
|
+
data: data,
|
|
47
|
+
level: level
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def add_dependency(name:, host: nil, method: nil, status: nil, duration_ms: nil, kind: nil, data: {})
|
|
52
|
+
ContextStore.add_manual_dependency(
|
|
53
|
+
name: name,
|
|
54
|
+
host: host,
|
|
55
|
+
method: method,
|
|
56
|
+
status: status,
|
|
57
|
+
duration_ms: duration_ms,
|
|
58
|
+
kind: kind,
|
|
59
|
+
data: data
|
|
60
|
+
)
|
|
61
|
+
end
|
|
48
62
|
end
|
|
49
63
|
end
|
|
50
64
|
|
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.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Logister
|
|
@@ -50,11 +50,15 @@ files:
|
|
|
50
50
|
- lib/generators/logister/templates/logister.rb
|
|
51
51
|
- lib/logister-ruby.rb
|
|
52
52
|
- lib/logister.rb
|
|
53
|
+
- lib/logister/active_job_reporter.rb
|
|
53
54
|
- lib/logister/client.rb
|
|
54
55
|
- lib/logister/configuration.rb
|
|
56
|
+
- lib/logister/context_helpers.rb
|
|
57
|
+
- lib/logister/context_store.rb
|
|
55
58
|
- lib/logister/middleware.rb
|
|
56
59
|
- lib/logister/railtie.rb
|
|
57
60
|
- lib/logister/reporter.rb
|
|
61
|
+
- lib/logister/request_subscriber.rb
|
|
58
62
|
- lib/logister/sql_subscriber.rb
|
|
59
63
|
- lib/logister/version.rb
|
|
60
64
|
- logister-ruby.gemspec
|