closeyourit-ruby 0.10.0 → 0.11.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 +16 -1
- data/lib/closeyourit/background_worker.rb +14 -14
- data/lib/closeyourit/breadcrumb.rb +2 -2
- data/lib/closeyourit/breadcrumb_buffer.rb +2 -2
- data/lib/closeyourit/client.rb +24 -24
- data/lib/closeyourit/configuration.rb +104 -103
- data/lib/closeyourit/event.rb +6 -6
- data/lib/closeyourit/events/error_event.rb +16 -16
- data/lib/closeyourit/events/job_metric_event.rb +11 -8
- data/lib/closeyourit/events/log_event.rb +17 -17
- data/lib/closeyourit/events/message_event.rb +4 -4
- data/lib/closeyourit/events/performance_issue_event.rb +4 -4
- data/lib/closeyourit/events/slow_method_event.rb +6 -6
- data/lib/closeyourit/events/slow_query_event.rb +4 -4
- data/lib/closeyourit/instrumenter.rb +6 -6
- data/lib/closeyourit/job_context.rb +61 -0
- data/lib/closeyourit/line_cache.rb +4 -4
- data/lib/closeyourit/log_buffer.rb +10 -10
- data/lib/closeyourit/log_device.rb +18 -18
- data/lib/closeyourit/monitor.rb +2 -2
- data/lib/closeyourit/performance/request_profile.rb +5 -5
- data/lib/closeyourit/performance/rollup.rb +3 -3
- data/lib/closeyourit/rails/active_job_extension.rb +36 -31
- data/lib/closeyourit/rails/capture_exceptions.rb +3 -3
- data/lib/closeyourit/rails/error_subscriber.rb +4 -4
- data/lib/closeyourit/rails/log_broadcast.rb +12 -12
- data/lib/closeyourit/rails/net_http_patch.rb +22 -22
- data/lib/closeyourit/rails/query_source.rb +3 -3
- data/lib/closeyourit/rails/railtie.rb +29 -65
- data/lib/closeyourit/rails/request_body.rb +7 -7
- data/lib/closeyourit/rails/request_context.rb +27 -27
- data/lib/closeyourit/scope.rb +29 -29
- data/lib/closeyourit/scrubber.rb +50 -50
- data/lib/closeyourit/sidekiq/error_handler.rb +2 -2
- data/lib/closeyourit/sidekiq/job_metrics_middleware.rb +39 -17
- data/lib/closeyourit/stats.rb +8 -8
- data/lib/closeyourit/subscribers/job_performance.rb +102 -24
- data/lib/closeyourit/subscribers/request_performance.rb +4 -4
- data/lib/closeyourit/subscribers/slow_query.rb +42 -20
- data/lib/closeyourit/trace_context.rb +22 -22
- data/lib/closeyourit/transport.rb +22 -22
- data/lib/closeyourit/usage_registry.rb +17 -16
- data/lib/closeyourit/version.rb +1 -1
- data/lib/closeyourit-ruby.rb +125 -125
- metadata +2 -1
|
@@ -4,22 +4,22 @@ require "uri"
|
|
|
4
4
|
require "concurrent"
|
|
5
5
|
|
|
6
6
|
module CloseYourIt
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# Holds all client options. Built by `CloseYourIt.init { |c| ... }`.
|
|
8
|
+
# Without `endpoint_url`/`token`/`project_id` (or with `http://` in production) the client is a no-op.
|
|
9
9
|
class Configuration
|
|
10
10
|
DEFAULT_EXCLUDED_EXCEPTIONS = %w[
|
|
11
11
|
ActionController::RoutingError
|
|
12
12
|
ActiveRecord::RecordNotFound
|
|
13
13
|
].freeze
|
|
14
14
|
|
|
15
|
-
#
|
|
15
|
+
# HTTP headers captured in the request context (never Authorization/Cookie → no PII/secrets).
|
|
16
16
|
DEFAULT_REQUEST_HEADER_ALLOWLIST = %w[Accept Content-Type User-Agent Referer].freeze
|
|
17
17
|
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
18
|
+
# Solid stack service tables: they are framework infrastructure, not application code. A slow
|
|
19
|
+
# query on them cannot be fixed by reading your own repo — it only says the database is under
|
|
20
|
+
# contention, which the app's queries already show. Excluded by DEFAULT from slowdown measurement
|
|
21
|
+
# because otherwise they flood it: on closeyourit-rails on 2026-07-30 they were about 2,740
|
|
22
|
+
# samples out of 4,400 (62%), all between 250 and 655 ms (CYRB-18).
|
|
23
23
|
DEFAULT_EXCLUDED_QUERY_PATTERNS = [
|
|
24
24
|
/\bsolid_queue_/,
|
|
25
25
|
/\bsolid_cache_/,
|
|
@@ -41,7 +41,7 @@ module CloseYourIt
|
|
|
41
41
|
:query_time_threshold_ms, :slow_request_threshold_ms, :slow_external_threshold_ms,
|
|
42
42
|
:capture_external_http, :trap_signals,
|
|
43
43
|
:monitor_jobs, :slow_job_threshold_ms, :job_queue_latency_threshold_ms,
|
|
44
|
-
:jobs_sample_rate, :propagate_trace_context,
|
|
44
|
+
:jobs_sample_rate, :job_lifecycle_logs, :propagate_trace_context,
|
|
45
45
|
:usage_enabled, :usage_flush_interval, :usage_max_symbols
|
|
46
46
|
attr_writer :release, :project_root
|
|
47
47
|
attr_reader :excluded_exceptions, :excluded_log_patterns, :excluded_query_patterns,
|
|
@@ -57,140 +57,141 @@ module CloseYourIt
|
|
|
57
57
|
@excluded_exceptions = DEFAULT_EXCLUDED_EXCEPTIONS.dup
|
|
58
58
|
@before_send = nil
|
|
59
59
|
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
# (
|
|
63
|
-
#
|
|
60
|
+
# Opt-in diagnostic hook: `->(event, details) { ... }` called at every stage of an event's
|
|
61
|
+
# lifecycle (:enqueue, :send, :drop, :timeout, :shutdown) with details free of sensitive data
|
|
62
|
+
# (e.g. `{ reason: :queue_full }`, `{ status: 429 }`). Local and non-recursive: it sends no
|
|
63
|
+
# telemetry and cannot trigger self-monitoring loops (see CloseYourIt.notify_diagnostic, CYRB-12).
|
|
64
64
|
@on_diagnostic = nil
|
|
65
65
|
|
|
66
66
|
@async_threads = default_threads
|
|
67
67
|
@background_worker_max_queue = 30
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
68
|
+
# How long to wait, on process exit, for in-flight sends to complete. Default: the duration cap
|
|
69
|
+
# of ONE POST, so the last batch is not abandoned halfway (CYRB-24).
|
|
70
|
+
# Lower it if process exit must be faster than delivery.
|
|
71
71
|
@shutdown_timeout = Transport::MAX_REQUEST_SECONDS
|
|
72
72
|
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
73
|
+
# Intercepts SIGTERM to guarantee the end-of-life flush (deploy/Kamal): by default SIGTERM ends
|
|
74
|
+
# the process WITHOUT running at_exit. OPT-IN because it overrides any TERM handler of the host
|
|
75
|
+
# app. See CloseYourIt.shutdown / register_shutdown_flush (CYRB-5).
|
|
76
76
|
@trap_signals = false
|
|
77
77
|
|
|
78
78
|
@slow_query_threshold_ms = 100
|
|
79
79
|
@slow_method_threshold_ms = 200
|
|
80
|
-
#
|
|
81
|
-
#
|
|
80
|
+
# Queries NOT to measure as slowdowns (match on the SQL text). Default: the Solid stack service
|
|
81
|
+
# tables. Whoever really wants to measure them clears the list.
|
|
82
82
|
@excluded_query_patterns = DEFAULT_EXCLUDED_QUERY_PATTERNS.dup
|
|
83
83
|
|
|
84
84
|
@send_pii = false
|
|
85
85
|
@obfuscate_sql = true
|
|
86
86
|
@send_server_name = true
|
|
87
87
|
|
|
88
|
-
#
|
|
88
|
+
# HTTP request context (method/url/header allowlist). Body/query/IP only with send_pii.
|
|
89
89
|
@capture_request = true
|
|
90
90
|
@request_header_allowlist = DEFAULT_REQUEST_HEADER_ALLOWLIST.dup
|
|
91
91
|
|
|
92
|
-
#
|
|
92
|
+
# Source lines around each stacktrace frame (pre/context/post). 0 = disabled.
|
|
93
93
|
@context_lines = 3
|
|
94
94
|
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
95
|
+
# Request body params in the event (`request.data`), extracted LAZILY only when the error
|
|
96
|
+
# happens, sanitized and scrubbed (denylist + filter_parameters). The backend scrubs again
|
|
97
|
+
# defensively. Uploads → placeholder, 64 KB cap.
|
|
98
98
|
@capture_request_body = true
|
|
99
99
|
|
|
100
|
-
# Breadcrumbs:
|
|
100
|
+
# Breadcrumbs: history (obfuscated queries, custom events) attached to the error.
|
|
101
101
|
@breadcrumbs_enabled = true
|
|
102
102
|
@max_breadcrumbs = 100
|
|
103
103
|
|
|
104
|
-
#
|
|
104
|
+
# Probabilistic sampling of errors/messages (1.0 = send everything, 0.0 = nothing).
|
|
105
105
|
@sample_rate = 1.0
|
|
106
106
|
|
|
107
|
-
#
|
|
107
|
+
# Captures handled errors (Rails.error.report) and ActiveJob/Sidekiq errors (otherwise lost).
|
|
108
108
|
@capture_handled_errors = true
|
|
109
109
|
@report_active_job_errors = true
|
|
110
110
|
|
|
111
|
-
#
|
|
111
|
+
# Captures parameter values — opt-in, default OFF (privacy). Binds/arguments may contain PII.
|
|
112
112
|
@capture_query_bindings = false
|
|
113
113
|
@capture_method_arguments = false
|
|
114
114
|
|
|
115
|
-
#
|
|
115
|
+
# Structured logs (CloseYourIt.log / .logger). Dedicated master switch + sampling + batching.
|
|
116
116
|
@logs_enabled = true
|
|
117
117
|
@logs_sample_rate = 1.0
|
|
118
118
|
@logs_batch_size = 50
|
|
119
119
|
@logs_flush_interval = 5
|
|
120
120
|
|
|
121
|
-
# CYSK-29 —
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
121
|
+
# CYSK-29 — usage telemetry: which routes/jobs/keys actually run. The payload carries no user
|
|
122
|
+
# data by construction (route = Controller#action, never the URL), so the default is ON:
|
|
123
|
+
# thirty days of optional collection taught us that optional means never.
|
|
124
|
+
# The registry is emptied on every flush: the cap limits DISTINCT symbols per window.
|
|
125
125
|
@usage_enabled = true
|
|
126
126
|
@usage_flush_interval = 300
|
|
127
127
|
@usage_max_symbols = 2000
|
|
128
|
-
#
|
|
128
|
+
# Opt-in Rails.logger broadcast → CloseYourIt.log (default OFF; sends only ≥ threshold).
|
|
129
129
|
@capture_rails_logs = false
|
|
130
130
|
@logs_min_level = :info
|
|
131
|
-
#
|
|
132
|
-
# CloseYourIt.log/.logger,
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
#
|
|
131
|
+
# DEDICATED threshold of the Rails.logger broadcast, separate from logs_min_level (which governs
|
|
132
|
+
# CloseYourIt.log/.logger, where the dev chooses what to log). Default :warn — conservative:
|
|
133
|
+
# without it, in high-traffic production EVERY framework info line (Started GET, Rendered, ...)
|
|
134
|
+
# would flood the stream with tens of thousands of log entries/min (CYRB-7). Whoever also wants
|
|
135
|
+
# the framework info lines lowers it explicitly (e.g. :info).
|
|
136
136
|
@capture_rails_logs_min_level = :warn
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
137
|
+
# Rails.logger broadcast noise that is NOT an exception, so excluded_exceptions cannot cover it:
|
|
138
|
+
# repeated framework or gem lines. Regexp on the message text, empty by default. Applies ONLY
|
|
139
|
+
# to the automatic broadcast, never to an explicit CloseYourIt.log.
|
|
140
140
|
@excluded_log_patterns = []
|
|
141
141
|
|
|
142
|
-
# Performance issue detection (
|
|
143
|
-
# OPT-IN, default OFF:
|
|
144
|
-
#
|
|
142
|
+
# Performance issue detection (aggregate verdicts: N+1, slow request, slow external HTTP).
|
|
143
|
+
# OPT-IN, default OFF: it profiles EVERY query of the request → non-negligible overhead, enable it
|
|
144
|
+
# deliberately per app. Thresholds are conservative (little noise). See Performance::Rollup.
|
|
145
145
|
@detect_performance_issues = false
|
|
146
|
-
@n_plus_one_threshold = 10 #
|
|
147
|
-
@query_count_threshold = 100 #
|
|
148
|
-
@query_time_threshold_ms = 500 #
|
|
149
|
-
@slow_request_threshold_ms = 1000 #
|
|
150
|
-
@slow_external_threshold_ms = 1000 #
|
|
151
|
-
@capture_external_http = true #
|
|
152
|
-
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
146
|
+
@n_plus_one_threshold = 10 # same fingerprint+call-site run > N times in one request
|
|
147
|
+
@query_count_threshold = 100 # too many total queries in one request
|
|
148
|
+
@query_time_threshold_ms = 500 # total DB time per request beyond which = high_query_count
|
|
149
|
+
@slow_request_threshold_ms = 1000 # total request duration
|
|
150
|
+
@slow_external_threshold_ms = 1000 # single external HTTP call
|
|
151
|
+
@capture_external_http = true # instruments Net::HTTP (only with detect_performance_issues)
|
|
152
|
+
|
|
153
|
+
# Background job metrics: execution duration and queue wait (queue latency) for ActiveJob and
|
|
154
|
+
# Sidekiq. ON by default (unlike detect_performance_issues): visibility of slow/late/retried jobs
|
|
155
|
+
# must not require manual instrumentation in every app (CYRB-14), and the overhead is low — one
|
|
156
|
+
# notification per job, not profiling of every query. Noise is kept down by the thresholds
|
|
157
|
+
# (normal jobs under threshold = no metric) and by sampling. The label is the job class name;
|
|
158
|
+
# arguments are NEVER sent.
|
|
159
159
|
@monitor_jobs = true
|
|
160
|
-
@
|
|
161
|
-
@
|
|
162
|
-
@
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
160
|
+
@job_lifecycle_logs = false
|
|
161
|
+
@slow_job_threshold_ms = 5000 # perform duration beyond which = slow_job
|
|
162
|
+
@job_queue_latency_threshold_ms = 60_000 # enqueue→execution wait beyond which = job_queue_latency
|
|
163
|
+
@jobs_sample_rate = 1.0 # fraction of over-threshold candidates actually sent
|
|
164
|
+
|
|
165
|
+
# W3C trace context propagation (traceparent/tracestate) to external services called via
|
|
166
|
+
# Net::HTTP. OPT-IN, default OFF: injecting outgoing headers crosses a trust boundary and must be
|
|
167
|
+
# decided per app. It is limited PER DESTINATION by the allowlist (exact hosts, case-insensitive,
|
|
168
|
+
# or Regexp for subdomains); empty list = no destination. Never to unlisted hosts, never as
|
|
169
|
+
# `baggage` (which may carry PII). Inbound, a valid traceparent becomes the trace_id of the
|
|
170
|
+
# CloseYourIt events → the request's errors/metrics correlate with the distributed trace (CYRB-15).
|
|
170
171
|
@propagate_trace_context = false
|
|
171
172
|
@trace_propagation_allowlist = []
|
|
172
173
|
|
|
173
|
-
#
|
|
174
|
-
#
|
|
174
|
+
# Project root: base for the relative frame filename (cross-SDK culprit). Lazy: auto-detected
|
|
175
|
+
# from Rails.root or Dir.pwd on first access when not set explicitly.
|
|
175
176
|
@project_root = nil
|
|
176
177
|
|
|
177
178
|
@filter_parameters = []
|
|
178
179
|
@scrub_message_patterns = []
|
|
179
180
|
end
|
|
180
181
|
|
|
181
|
-
#
|
|
182
|
+
# Classes/strings → name (String); Regexps stay Regexps (pattern match on name/message).
|
|
182
183
|
def excluded_exceptions=(list)
|
|
183
184
|
@excluded_exceptions = Array(list).map { |item| item.is_a?(Regexp) ? item : item.to_s }
|
|
184
185
|
end
|
|
185
186
|
|
|
186
|
-
#
|
|
187
|
-
#
|
|
187
|
+
# Rails.logger broadcast patterns to drop. Strings become Regexps (literal match on the text):
|
|
188
|
+
# whoever writes `config.excluded_log_patterns = ["Rendered layout"]` means exactly that.
|
|
188
189
|
def excluded_log_patterns=(list)
|
|
189
190
|
@excluded_log_patterns = Array(list).map { |item| item.is_a?(Regexp) ? item : Regexp.new(Regexp.escape(item.to_s)) }
|
|
190
191
|
end
|
|
191
192
|
|
|
192
|
-
#
|
|
193
|
-
# (
|
|
193
|
+
# Queries not to measure. Same normalization as excluded_log_patterns: String = literal text
|
|
194
|
+
# (a table name is written that way, not as a pattern), Regexp = pattern.
|
|
194
195
|
def excluded_query_patterns=(list)
|
|
195
196
|
@excluded_query_patterns = Array(list).map { |item| item.is_a?(Regexp) ? item : Regexp.new(Regexp.escape(item.to_s)) }
|
|
196
197
|
end
|
|
@@ -199,8 +200,8 @@ module CloseYourIt
|
|
|
199
200
|
@filter_parameters = Array(list)
|
|
200
201
|
end
|
|
201
202
|
|
|
202
|
-
#
|
|
203
|
-
#
|
|
203
|
+
# Destinations allowed to receive the W3C trace headers. String = exact host (case-insensitive
|
|
204
|
+
# match), Regexp = pattern (for subdomains/host families). Empty list = none.
|
|
204
205
|
def trace_propagation_allowlist=(list)
|
|
205
206
|
@trace_propagation_allowlist = Array(list)
|
|
206
207
|
end
|
|
@@ -213,8 +214,8 @@ module CloseYourIt
|
|
|
213
214
|
environment.to_s == "production"
|
|
214
215
|
end
|
|
215
216
|
|
|
216
|
-
#
|
|
217
|
-
#
|
|
217
|
+
# The client sends only with complete credentials (endpoint + token + project_id) and a secure
|
|
218
|
+
# transport (http:// allowed outside production).
|
|
218
219
|
def enabled?
|
|
219
220
|
return false if blank?(endpoint_url) || blank?(token) || blank?(project_id)
|
|
220
221
|
return false if insecure_endpoint? && production?
|
|
@@ -222,8 +223,8 @@ module CloseYourIt
|
|
|
222
223
|
true
|
|
223
224
|
end
|
|
224
225
|
|
|
225
|
-
#
|
|
226
|
-
#
|
|
226
|
+
# Logs configuration warnings (e.g. http:// endpoint, malformed project_id/endpoint).
|
|
227
|
+
# Never raises: consistent with the client's no-op philosophy. Called by `CloseYourIt.init`.
|
|
227
228
|
def validate!
|
|
228
229
|
CloseYourIt.internal_logger.warn(insecure_endpoint_message) if insecure_endpoint?
|
|
229
230
|
CloseYourIt.internal_logger.warn(malformed_project_id_message) if malformed_project_id?
|
|
@@ -231,22 +232,22 @@ module CloseYourIt
|
|
|
231
232
|
self
|
|
232
233
|
end
|
|
233
234
|
|
|
234
|
-
#
|
|
235
|
+
# Effective release: the one set, otherwise auto-detected (deploy/CI ENV or git).
|
|
235
236
|
def release
|
|
236
237
|
return @release unless @release.nil?
|
|
237
238
|
|
|
238
239
|
@release = detect_release
|
|
239
240
|
end
|
|
240
241
|
|
|
241
|
-
#
|
|
242
|
-
#
|
|
242
|
+
# Effective project root: the one set, otherwise auto-detected (Rails.root or Dir.pwd).
|
|
243
|
+
# Used to make `frame.filename` relative (culprit comparable across SDKs — CYRB-4).
|
|
243
244
|
def project_root
|
|
244
245
|
return @project_root unless @project_root.nil?
|
|
245
246
|
|
|
246
247
|
@project_root = detect_project_root
|
|
247
248
|
end
|
|
248
249
|
|
|
249
|
-
# Rails.root
|
|
250
|
+
# Rails.root when the app runs under Rails, otherwise the working directory. Never raises.
|
|
250
251
|
def detect_project_root
|
|
251
252
|
return ::Rails.root.to_s if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
|
|
252
253
|
|
|
@@ -255,12 +256,12 @@ module CloseYourIt
|
|
|
255
256
|
Dir.pwd
|
|
256
257
|
end
|
|
257
258
|
|
|
258
|
-
#
|
|
259
|
-
#
|
|
259
|
+
# A semver tag (optional `v`) is preferred to the short SHA as release: it converges with what the
|
|
260
|
+
# CI records (it tags), while the short SHA creates duplicate releases on the backend (CYRB-9).
|
|
260
261
|
SEMVER_TAG = /\Av?\d+\.\d+\.\d+([-+.].+)?\z/
|
|
261
262
|
|
|
262
|
-
#
|
|
263
|
-
#
|
|
263
|
+
# Release auto-detection: first a semver tag (APP_GIT_TAG/GIT_TAG), then the short SHA from the
|
|
264
|
+
# deploy/CI env or from git. Never raises.
|
|
264
265
|
def detect_release
|
|
265
266
|
detect_tag ||
|
|
266
267
|
ENV["KAMAL_VERSION"] ||
|
|
@@ -275,8 +276,8 @@ module CloseYourIt
|
|
|
275
276
|
|
|
276
277
|
private
|
|
277
278
|
|
|
278
|
-
# `.git`
|
|
279
|
-
#
|
|
279
|
+
# `.git` is a directory in a normal checkout and a file in a worktree → File.directory? is false
|
|
280
|
+
# in worktrees, so tests do not spawn git subprocesses (deterministic).
|
|
280
281
|
def git_revision
|
|
281
282
|
return nil unless File.directory?(".git")
|
|
282
283
|
|
|
@@ -286,9 +287,9 @@ module CloseYourIt
|
|
|
286
287
|
nil
|
|
287
288
|
end
|
|
288
289
|
|
|
289
|
-
#
|
|
290
|
-
# (v
|
|
291
|
-
#
|
|
290
|
+
# Semver tag from APP_GIT_TAG then GIT_TAG: accepts only a non-blank value shaped like semver
|
|
291
|
+
# (optional v + MAJOR.MINOR.PATCH + optional suffix). Rejects `unknown`, empty, branch names,
|
|
292
|
+
# etc. → nil, so detect_release falls back to the SHA chain.
|
|
292
293
|
def detect_tag
|
|
293
294
|
tag = ENV["APP_GIT_TAG"] || ENV["GIT_TAG"]
|
|
294
295
|
return nil if blank?(tag)
|
|
@@ -303,17 +304,17 @@ module CloseYourIt
|
|
|
303
304
|
!uri.nil? && uri.scheme != "https"
|
|
304
305
|
end
|
|
305
306
|
|
|
306
|
-
#
|
|
307
|
-
#
|
|
307
|
+
# Warns when project_id is set but does not look like a UUID (the typical mistake is pasting a
|
|
308
|
+
# slug/name instead of the id). It does not block: the server is the authority on validity.
|
|
308
309
|
def malformed_project_id?
|
|
309
310
|
!blank?(project_id) && !UUID_FORMAT.match?(project_id.to_s)
|
|
310
311
|
end
|
|
311
312
|
|
|
312
313
|
def malformed_project_id_message
|
|
313
|
-
"CloseYourIt: project_id (#{project_id})
|
|
314
|
+
"CloseYourIt: project_id (#{project_id}) is not shaped like a UUID — check that you copied the right id."
|
|
314
315
|
end
|
|
315
316
|
|
|
316
|
-
#
|
|
317
|
+
# Warns when endpoint_url is set but not parsable or has no host.
|
|
317
318
|
def malformed_endpoint?
|
|
318
319
|
return false if blank?(endpoint_url)
|
|
319
320
|
|
|
@@ -322,12 +323,12 @@ module CloseYourIt
|
|
|
322
323
|
end
|
|
323
324
|
|
|
324
325
|
def malformed_endpoint_message
|
|
325
|
-
"CloseYourIt: endpoint_url (#{endpoint_url})
|
|
326
|
+
"CloseYourIt: endpoint_url (#{endpoint_url}) is not a valid URL (missing host)."
|
|
326
327
|
end
|
|
327
328
|
|
|
328
329
|
def insecure_endpoint_message
|
|
329
|
-
tail = production? ? "
|
|
330
|
-
"CloseYourIt: endpoint_url
|
|
330
|
+
tail = production? ? "Telemetry DISABLED in production." : "Allowed only in development."
|
|
331
|
+
"CloseYourIt: endpoint_url uses insecure http:// (#{endpoint_url}) — the token would travel in clear text. #{tail}"
|
|
331
332
|
end
|
|
332
333
|
|
|
333
334
|
def parsed_endpoint
|
data/lib/closeyourit/event.rb
CHANGED
|
@@ -4,8 +4,8 @@ require "time"
|
|
|
4
4
|
require "socket"
|
|
5
5
|
|
|
6
6
|
module CloseYourIt
|
|
7
|
-
# Base
|
|
8
|
-
# (
|
|
7
|
+
# Base of telemetry events. Subclasses implement `#to_h` and `#ingest_path`
|
|
8
|
+
# (the API path the event is sent to: errors → /events, metrics → /metrics).
|
|
9
9
|
class Event
|
|
10
10
|
def initialize(configuration)
|
|
11
11
|
@configuration = configuration
|
|
@@ -13,11 +13,11 @@ module CloseYourIt
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def to_h
|
|
16
|
-
raise NotImplementedError, "#{self.class}
|
|
16
|
+
raise NotImplementedError, "#{self.class} must implement #to_h"
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def ingest_path(_project_id)
|
|
20
|
-
raise NotImplementedError, "#{self.class}
|
|
20
|
+
raise NotImplementedError, "#{self.class} must implement #ingest_path"
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
private
|
|
@@ -42,8 +42,8 @@ module CloseYourIt
|
|
|
42
42
|
hash.reject { |_key, value| value.nil? }
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
45
|
+
# Recursive merge: nested Hashes are merged (e.g. `contexts.runtime` kept while the scope adds
|
|
46
|
+
# `contexts.active_job`), scalars are overwritten.
|
|
47
47
|
def deep_merge(base, override)
|
|
48
48
|
base.merge(override) do |_key, old_value, new_value|
|
|
49
49
|
if old_value.is_a?(Hash) && new_value.is_a?(Hash)
|
|
@@ -7,9 +7,9 @@ require_relative "../scrubber"
|
|
|
7
7
|
require_relative "../line_cache"
|
|
8
8
|
|
|
9
9
|
module CloseYourIt
|
|
10
|
-
#
|
|
11
|
-
# (Errors::Ingest::Normalize).
|
|
12
|
-
# `exception.values`
|
|
10
|
+
# Turns a Ruby exception into the **Sentry event payload** the CloseYourIt backend ingests
|
|
11
|
+
# (Errors::Ingest::Normalize). Uses `backtrace_locations` (no regex) and puts the cause chain in
|
|
12
|
+
# `exception.values` ordered from the outermost to the main one (Sentry: values.last = the crash).
|
|
13
13
|
class ErrorEvent < Event
|
|
14
14
|
def self.from_exception(exception, configuration:, handled: false, level: "error", contexts: nil)
|
|
15
15
|
new(exception, configuration, handled: handled, level: level, contexts: contexts)
|
|
@@ -30,7 +30,7 @@ module CloseYourIt
|
|
|
30
30
|
"timestamp" => @occurred_at,
|
|
31
31
|
"platform" => "ruby",
|
|
32
32
|
"level" => @level,
|
|
33
|
-
#
|
|
33
|
+
# Log↔error correlation: same trace_id as the logs of the same request (populated by the Scope).
|
|
34
34
|
"trace_id" => CloseYourIt::Scope.current.trace_id,
|
|
35
35
|
"environment" => environment,
|
|
36
36
|
"release" => @configuration.release,
|
|
@@ -39,9 +39,9 @@ module CloseYourIt
|
|
|
39
39
|
"contexts" => { "runtime" => { "name" => "ruby", "version" => RUBY_VERSION } },
|
|
40
40
|
"sdk" => sdk
|
|
41
41
|
)
|
|
42
|
-
#
|
|
42
|
+
# Merges the per-request/job context (user/tags/extra/contexts/request) collected in the Scope.
|
|
43
43
|
merged = deep_merge(base, CloseYourIt::Scope.current.to_event_hash)
|
|
44
|
-
#
|
|
44
|
+
# Extra context passed explicitly (e.g. rails_error from the ErrorReporter).
|
|
45
45
|
@contexts ? deep_merge(merged, { "contexts" => @contexts }) : merged
|
|
46
46
|
end
|
|
47
47
|
|
|
@@ -51,7 +51,7 @@ module CloseYourIt
|
|
|
51
51
|
|
|
52
52
|
private
|
|
53
53
|
|
|
54
|
-
# Cause
|
|
54
|
+
# Cause chain → Sentry array: outermost cause first, main exception LAST.
|
|
55
55
|
def exception_values
|
|
56
56
|
chain = []
|
|
57
57
|
seen = []
|
|
@@ -73,10 +73,10 @@ module CloseYourIt
|
|
|
73
73
|
}
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
# Sentry-style:
|
|
77
|
-
# + snippet
|
|
78
|
-
# `filename`
|
|
79
|
-
# path
|
|
76
|
+
# Sentry-style: most recent frame last; keys filename/function/lineno/in_app/abs_path
|
|
77
|
+
# + source snippet (pre_context/context_line/post_context) when the file is readable.
|
|
78
|
+
# `filename` is relative/basename (culprit comparable across SDKs — CYRB-4); `abs_path` is the
|
|
79
|
+
# absolute path for the detail view and for reading the context lines' source.
|
|
80
80
|
def frames(locations)
|
|
81
81
|
return [] if locations.nil?
|
|
82
82
|
|
|
@@ -94,15 +94,15 @@ module CloseYourIt
|
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
# Path
|
|
98
|
-
# (
|
|
99
|
-
#
|
|
100
|
-
# "filename in function"
|
|
97
|
+
# Path to display (Sentry `filename`): relative to project_root if the file is inside the app
|
|
98
|
+
# (e.g. "app/models/user.rb"), otherwise the basename (gem/stdlib/external paths → "base.rb").
|
|
99
|
+
# Aligns Ruby with the other SDKs (Dart/JS send basename/relative), so the culprit
|
|
100
|
+
# "filename in function" is comparable at a glance on polyglot projects. See CYRB-4.
|
|
101
101
|
def relative_filename(path)
|
|
102
102
|
return path if path.nil?
|
|
103
103
|
|
|
104
104
|
root = @configuration.project_root.to_s
|
|
105
|
-
prefix = root.empty? ? nil : File.join(root, "") # root
|
|
105
|
+
prefix = root.empty? ? nil : File.join(root, "") # root with trailing separator, portable
|
|
106
106
|
return File.basename(path) unless prefix && path.start_with?(prefix)
|
|
107
107
|
|
|
108
108
|
relative = path[prefix.length..]
|
|
@@ -4,15 +4,15 @@ require "securerandom"
|
|
|
4
4
|
require_relative "../event"
|
|
5
5
|
|
|
6
6
|
module CloseYourIt
|
|
7
|
-
#
|
|
8
|
-
# (`subtype=slow_job`)
|
|
9
|
-
#
|
|
7
|
+
# `kind=performance_issue` metric for a background job that exceeded a DURATION threshold
|
|
8
|
+
# (`subtype=slow_job`) or a queue WAIT threshold (`subtype=job_queue_latency`). Sent on the metrics
|
|
9
|
+
# pipeline (`/api/v1/projects/:id/metrics`), like slow_query/slow_method and performance verdicts.
|
|
10
10
|
#
|
|
11
|
-
# `label`
|
|
12
|
-
# label/
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# Rollup.
|
|
11
|
+
# `label` is the job CLASS name — never the arguments (privacy: like slow_method it sends only
|
|
12
|
+
# label/duration). `attempt` makes retries visible; `trace_id` (= ActiveJob job_id / Sidekiq jid)
|
|
13
|
+
# correlates the metric with logs and errors of the same job. `duration_ms` is the subtype's
|
|
14
|
+
# measured value (perform duration for slow_job, queue wait for job_queue_latency) — same
|
|
15
|
+
# convention as the Rollup. Nil fields are omitted.
|
|
16
16
|
class JobMetricEvent < Event
|
|
17
17
|
def initialize(attrs, configuration)
|
|
18
18
|
super(configuration)
|
|
@@ -33,6 +33,9 @@ module CloseYourIt
|
|
|
33
33
|
"queue" => @attrs[:queue],
|
|
34
34
|
"adapter" => @attrs[:adapter],
|
|
35
35
|
"attempt" => @attrs[:attempt],
|
|
36
|
+
"contexts" => (@attrs[:job_context] && { "job" => @attrs[:job_context].merge(
|
|
37
|
+
"measurement" => (@attrs[:subtype] == "slow_job" ? "duration" : "queue_wait")
|
|
38
|
+
) }),
|
|
36
39
|
"sdk" => sdk
|
|
37
40
|
)
|
|
38
41
|
end
|