logister-ruby 0.2.1 → 0.2.3
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/LICENSE +21 -0
- data/README.md +72 -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/reporter.rb +63 -0
- data/lib/logister/request_subscriber.rb +104 -0
- data/lib/logister/version.rb +1 -1
- data/lib/logister.rb +32 -6
- data/logister-ruby.gemspec +2 -2
- metadata +8 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd9ec67090820ac7b825afcbfd87a6a75f26033b0153daf8ee60b44a6d6fbbf3
|
|
4
|
+
data.tar.gz: 2175055728eb9dcc9bc9ed0acf4b3640f52900bcd35933fdd92db6ebbc1dfc3a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc1412c538be57e39a12e3f7c6939d1e710aa387490575ddf9af7df8ccda96cae3faafd9256348e18208ce63ebc0922f1696ae6b6d22aaeaf279c2b555960eee
|
|
7
|
+
data.tar.gz: 92276f407d774dc3010447cba8b5f0aea0c878ad8d30ba428b46585da3afbdf1dd9f0eb11b6cc62e89afa72e6433292eab19898551c5749a378d11a66aa9e646
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Logister
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
`logister-ruby` sends application errors and custom metrics to `logister.org`.
|
|
4
4
|
|
|
5
|
+
## Self-hosted backend
|
|
6
|
+
|
|
7
|
+
Use the open source Logister app repository to self-host the ingestion UI/API backend:
|
|
8
|
+
|
|
9
|
+
- App source: https://github.com/taimoorq/logister
|
|
10
|
+
|
|
5
11
|
## Install
|
|
6
12
|
|
|
7
13
|
```ruby
|
|
@@ -23,6 +29,21 @@ Logister.configure do |config|
|
|
|
23
29
|
config.environment = Rails.env
|
|
24
30
|
config.service = Rails.application.class.module_parent_name.underscore
|
|
25
31
|
config.release = ENV["RELEASE_SHA"]
|
|
32
|
+
|
|
33
|
+
# Optional richer context hooks
|
|
34
|
+
config.anonymize_ip = false
|
|
35
|
+
config.max_breadcrumbs = 40
|
|
36
|
+
config.max_dependencies = 20
|
|
37
|
+
config.capture_sql_breadcrumbs = true
|
|
38
|
+
config.sql_breadcrumb_min_duration_ms = 25.0
|
|
39
|
+
|
|
40
|
+
config.feature_flags_resolver = lambda do |request:, user:, **|
|
|
41
|
+
{ new_checkout: user&.respond_to?(:beta?) && user.beta? }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
config.dependency_resolver = lambda do |**|
|
|
45
|
+
[] # or return [{ name:, host:, method:, status:, durationMs:, kind: }]
|
|
46
|
+
end
|
|
26
47
|
end
|
|
27
48
|
```
|
|
28
49
|
|
|
@@ -55,6 +76,7 @@ end
|
|
|
55
76
|
## Rails auto-reporting
|
|
56
77
|
|
|
57
78
|
If Rails is present, the gem installs middleware that reports unhandled exceptions automatically.
|
|
79
|
+
It also attaches richer context (trace IDs, route/response/performance info, breadcrumbs, dependency calls, and user metadata when available).
|
|
58
80
|
|
|
59
81
|
## Database load metrics (ActiveRecord)
|
|
60
82
|
|
|
@@ -70,6 +92,37 @@ end
|
|
|
70
92
|
|
|
71
93
|
This emits metric events with `message: "db.query"` and context fields such as `duration_ms`, `name`, `sql`, and `binds_count`.
|
|
72
94
|
|
|
95
|
+
## Breadcrumbs and dependencies
|
|
96
|
+
|
|
97
|
+
You can add manual breadcrumbs and dependency calls that will be attached to captured errors:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
Logister.add_breadcrumb(
|
|
101
|
+
category: "checkout",
|
|
102
|
+
message: "Starting payment authorization",
|
|
103
|
+
data: { order_id: 123 }
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
Logister.add_dependency(
|
|
107
|
+
name: "stripe.charge",
|
|
108
|
+
host: "api.stripe.com",
|
|
109
|
+
method: "POST",
|
|
110
|
+
status: 200,
|
|
111
|
+
duration_ms: 184.7,
|
|
112
|
+
kind: "http"
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The gem also captures request and SQL breadcrumbs automatically in Rails.
|
|
117
|
+
|
|
118
|
+
## ActiveJob error context
|
|
119
|
+
|
|
120
|
+
Failed ActiveJob executions are auto-reported with `job` context:
|
|
121
|
+
- job class/id/queue/retries/schedule
|
|
122
|
+
- filtered job arguments (using `filter_parameters`)
|
|
123
|
+
- runtime/deployment metadata
|
|
124
|
+
- breadcrumbs/dependency calls collected during the job
|
|
125
|
+
|
|
73
126
|
## Manual reporting
|
|
74
127
|
|
|
75
128
|
```ruby
|
|
@@ -81,6 +134,25 @@ Logister.report_metric(
|
|
|
81
134
|
context: { duration_ms: 123 },
|
|
82
135
|
tags: { region: "us-east-1" }
|
|
83
136
|
)
|
|
137
|
+
|
|
138
|
+
Logister.report_transaction(
|
|
139
|
+
name: "POST /checkout",
|
|
140
|
+
duration_ms: 184.7,
|
|
141
|
+
status: 200,
|
|
142
|
+
context: { trace_id: "trace-123", request_id: "req-123" }
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
Logister.report_log(
|
|
146
|
+
message: "payment provider timeout",
|
|
147
|
+
level: "warn",
|
|
148
|
+
context: { trace_id: "trace-123", request_id: "req-123", user_id: 42 }
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
Logister.report_check_in(
|
|
152
|
+
slug: "nightly-reconcile",
|
|
153
|
+
status: "ok",
|
|
154
|
+
expected_interval_seconds: 900
|
|
155
|
+
)
|
|
84
156
|
```
|
|
85
157
|
|
|
86
158
|
## Release
|
|
@@ -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
|
data/lib/logister/reporter.rb
CHANGED
|
@@ -84,6 +84,69 @@ module Logister
|
|
|
84
84
|
@client.publish(payload)
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
def report_transaction(name:, duration_ms:, level: 'info', context: {}, tags: {}, fingerprint: nil, status: nil)
|
|
88
|
+
return false if ignored_environment?
|
|
89
|
+
return false if ignored_path?(context)
|
|
90
|
+
|
|
91
|
+
payload = build_payload(
|
|
92
|
+
event_type: 'transaction',
|
|
93
|
+
level: level,
|
|
94
|
+
message: name,
|
|
95
|
+
fingerprint: fingerprint || metric_fingerprint("tx:#{name}"),
|
|
96
|
+
context: context.merge(
|
|
97
|
+
transaction_name: name,
|
|
98
|
+
duration_ms: duration_ms,
|
|
99
|
+
status: status,
|
|
100
|
+
tags: tags
|
|
101
|
+
).compact
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
payload = apply_before_notify(payload)
|
|
105
|
+
return false unless payload
|
|
106
|
+
|
|
107
|
+
@client.publish(payload)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def report_log(message:, level: 'info', context: {}, tags: {}, fingerprint: nil)
|
|
111
|
+
return false if ignored_environment?
|
|
112
|
+
return false if ignored_path?(context)
|
|
113
|
+
|
|
114
|
+
payload = build_payload(
|
|
115
|
+
event_type: 'log',
|
|
116
|
+
level: level,
|
|
117
|
+
message: message,
|
|
118
|
+
fingerprint: fingerprint || metric_fingerprint("log:#{message.to_s.lines.first}"),
|
|
119
|
+
context: context.merge(tags: tags)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
payload = apply_before_notify(payload)
|
|
123
|
+
return false unless payload
|
|
124
|
+
|
|
125
|
+
@client.publish(payload)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def report_check_in(slug:, status: 'ok', expected_interval_seconds: 300, duration_ms: nil, context: {}, level: nil)
|
|
129
|
+
return false if ignored_environment?
|
|
130
|
+
|
|
131
|
+
payload = build_payload(
|
|
132
|
+
event_type: 'check_in',
|
|
133
|
+
level: level || (status.to_s == 'error' ? 'error' : 'info'),
|
|
134
|
+
message: slug,
|
|
135
|
+
fingerprint: metric_fingerprint("checkin:#{slug}"),
|
|
136
|
+
context: context.merge(
|
|
137
|
+
check_in_slug: slug,
|
|
138
|
+
check_in_status: status,
|
|
139
|
+
expected_interval_seconds: expected_interval_seconds,
|
|
140
|
+
duration_ms: duration_ms
|
|
141
|
+
).compact
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
payload = apply_before_notify(payload)
|
|
145
|
+
return false unless payload
|
|
146
|
+
|
|
147
|
+
@client.publish(payload)
|
|
148
|
+
end
|
|
149
|
+
|
|
87
150
|
# Store user info for the current thread so it is automatically attached to
|
|
88
151
|
# every error reported during this request.
|
|
89
152
|
#
|
|
@@ -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,12 +31,16 @@ module Logister
|
|
|
30
31
|
reporter.report_metric(**kwargs)
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
def
|
|
34
|
-
reporter.
|
|
34
|
+
def report_transaction(**kwargs)
|
|
35
|
+
reporter.report_transaction(**kwargs)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def report_log(**kwargs)
|
|
39
|
+
reporter.report_log(**kwargs)
|
|
35
40
|
end
|
|
36
41
|
|
|
37
|
-
def
|
|
38
|
-
reporter.
|
|
42
|
+
def report_check_in(**kwargs)
|
|
43
|
+
reporter.report_check_in(**kwargs)
|
|
39
44
|
end
|
|
40
45
|
|
|
41
46
|
def flush(timeout: 2)
|
|
@@ -45,6 +50,27 @@ module Logister
|
|
|
45
50
|
def shutdown
|
|
46
51
|
reporter.shutdown
|
|
47
52
|
end
|
|
53
|
+
|
|
54
|
+
def add_breadcrumb(category:, message:, data: {}, level: "info")
|
|
55
|
+
ContextStore.add_manual_breadcrumb(
|
|
56
|
+
category: category,
|
|
57
|
+
message: message,
|
|
58
|
+
data: data,
|
|
59
|
+
level: level
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def add_dependency(name:, host: nil, method: nil, status: nil, duration_ms: nil, kind: nil, data: {})
|
|
64
|
+
ContextStore.add_manual_dependency(
|
|
65
|
+
name: name,
|
|
66
|
+
host: host,
|
|
67
|
+
method: method,
|
|
68
|
+
status: status,
|
|
69
|
+
duration_ms: duration_ms,
|
|
70
|
+
kind: kind,
|
|
71
|
+
data: data
|
|
72
|
+
)
|
|
73
|
+
end
|
|
48
74
|
end
|
|
49
75
|
end
|
|
50
76
|
|
data/logister-ruby.gemspec
CHANGED
|
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
|
|
9
9
|
spec.summary = 'Send Rails errors and metrics to logister.org'
|
|
10
10
|
spec.description = 'Client gem for reporting errors and custom metrics from Ruby and Rails apps to logister.org'
|
|
11
|
-
spec.homepage = 'https://logister
|
|
11
|
+
spec.homepage = 'https://github.com/taimoorq/logister-ruby'
|
|
12
12
|
spec.license = 'MIT'
|
|
13
13
|
spec.required_ruby_version = '>= 3.1.0'
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
|
|
|
16
16
|
spec.metadata['source_code_uri'] = 'https://github.com/taimoorq/logister-ruby'
|
|
17
17
|
|
|
18
18
|
spec.files = Dir.chdir(__dir__) do
|
|
19
|
-
Dir.glob('lib/**/*') + ['README.md', 'logister-ruby.gemspec']
|
|
19
|
+
Dir.glob('lib/**/*') + ['README.md', 'LICENSE', 'logister-ruby.gemspec']
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
spec.require_paths = ['lib']
|
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.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Logister
|
|
@@ -45,24 +45,29 @@ executables: []
|
|
|
45
45
|
extensions: []
|
|
46
46
|
extra_rdoc_files: []
|
|
47
47
|
files:
|
|
48
|
+
- LICENSE
|
|
48
49
|
- README.md
|
|
49
50
|
- lib/generators/logister/install_generator.rb
|
|
50
51
|
- lib/generators/logister/templates/logister.rb
|
|
51
52
|
- lib/logister-ruby.rb
|
|
52
53
|
- lib/logister.rb
|
|
54
|
+
- lib/logister/active_job_reporter.rb
|
|
53
55
|
- lib/logister/client.rb
|
|
54
56
|
- lib/logister/configuration.rb
|
|
57
|
+
- lib/logister/context_helpers.rb
|
|
58
|
+
- lib/logister/context_store.rb
|
|
55
59
|
- lib/logister/middleware.rb
|
|
56
60
|
- lib/logister/railtie.rb
|
|
57
61
|
- lib/logister/reporter.rb
|
|
62
|
+
- lib/logister/request_subscriber.rb
|
|
58
63
|
- lib/logister/sql_subscriber.rb
|
|
59
64
|
- lib/logister/version.rb
|
|
60
65
|
- logister-ruby.gemspec
|
|
61
|
-
homepage: https://logister
|
|
66
|
+
homepage: https://github.com/taimoorq/logister-ruby
|
|
62
67
|
licenses:
|
|
63
68
|
- MIT
|
|
64
69
|
metadata:
|
|
65
|
-
homepage_uri: https://logister
|
|
70
|
+
homepage_uri: https://github.com/taimoorq/logister-ruby
|
|
66
71
|
source_code_uri: https://github.com/taimoorq/logister-ruby
|
|
67
72
|
rdoc_options: []
|
|
68
73
|
require_paths:
|