closeyourit-ruby 0.9.4 → 0.10.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 +6 -0
- data/lib/closeyourit/background_worker.rb +28 -9
- data/lib/closeyourit/breadcrumb.rb +2 -2
- data/lib/closeyourit/breadcrumb_buffer.rb +2 -2
- data/lib/closeyourit/client.rb +42 -29
- data/lib/closeyourit/configuration.rb +105 -101
- data/lib/closeyourit/event.rb +6 -6
- data/lib/closeyourit/events/error_event.rb +16 -16
- data/lib/closeyourit/events/job_metric_event.rb +8 -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 +12 -4
- 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 +33 -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 +32 -52
- 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 +47 -48
- data/lib/closeyourit/sidekiq/error_handler.rb +2 -2
- data/lib/closeyourit/sidekiq/job_metrics_middleware.rb +10 -11
- data/lib/closeyourit/stats.rb +8 -8
- data/lib/closeyourit/subscribers/job_performance.rb +30 -19
- 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 +25 -20
- data/lib/closeyourit/usage_registry.rb +17 -16
- data/lib/closeyourit/version.rb +1 -1
- data/lib/closeyourit-ruby.rb +125 -125
- metadata +1 -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_/,
|
|
@@ -27,7 +27,7 @@ module CloseYourIt
|
|
|
27
27
|
].freeze
|
|
28
28
|
|
|
29
29
|
attr_accessor :endpoint_url, :token, :project_id, :environment, :before_send, :on_diagnostic,
|
|
30
|
-
:async_threads, :background_worker_max_queue,
|
|
30
|
+
:async_threads, :background_worker_max_queue, :shutdown_timeout,
|
|
31
31
|
:slow_query_threshold_ms, :slow_method_threshold_ms,
|
|
32
32
|
:send_pii, :obfuscate_sql, :send_server_name,
|
|
33
33
|
:capture_query_bindings, :capture_method_arguments,
|
|
@@ -57,136 +57,140 @@ 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
|
-
#
|
|
71
|
-
|
|
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
|
+
@shutdown_timeout = Transport::MAX_REQUEST_SECONDS
|
|
72
|
+
|
|
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).
|
|
72
76
|
@trap_signals = false
|
|
73
77
|
|
|
74
78
|
@slow_query_threshold_ms = 100
|
|
75
79
|
@slow_method_threshold_ms = 200
|
|
76
|
-
#
|
|
77
|
-
#
|
|
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.
|
|
78
82
|
@excluded_query_patterns = DEFAULT_EXCLUDED_QUERY_PATTERNS.dup
|
|
79
83
|
|
|
80
84
|
@send_pii = false
|
|
81
85
|
@obfuscate_sql = true
|
|
82
86
|
@send_server_name = true
|
|
83
87
|
|
|
84
|
-
#
|
|
88
|
+
# HTTP request context (method/url/header allowlist). Body/query/IP only with send_pii.
|
|
85
89
|
@capture_request = true
|
|
86
90
|
@request_header_allowlist = DEFAULT_REQUEST_HEADER_ALLOWLIST.dup
|
|
87
91
|
|
|
88
|
-
#
|
|
92
|
+
# Source lines around each stacktrace frame (pre/context/post). 0 = disabled.
|
|
89
93
|
@context_lines = 3
|
|
90
94
|
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
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.
|
|
94
98
|
@capture_request_body = true
|
|
95
99
|
|
|
96
|
-
# Breadcrumbs:
|
|
100
|
+
# Breadcrumbs: history (obfuscated queries, custom events) attached to the error.
|
|
97
101
|
@breadcrumbs_enabled = true
|
|
98
102
|
@max_breadcrumbs = 100
|
|
99
103
|
|
|
100
|
-
#
|
|
104
|
+
# Probabilistic sampling of errors/messages (1.0 = send everything, 0.0 = nothing).
|
|
101
105
|
@sample_rate = 1.0
|
|
102
106
|
|
|
103
|
-
#
|
|
107
|
+
# Captures handled errors (Rails.error.report) and ActiveJob/Sidekiq errors (otherwise lost).
|
|
104
108
|
@capture_handled_errors = true
|
|
105
109
|
@report_active_job_errors = true
|
|
106
110
|
|
|
107
|
-
#
|
|
111
|
+
# Captures parameter values — opt-in, default OFF (privacy). Binds/arguments may contain PII.
|
|
108
112
|
@capture_query_bindings = false
|
|
109
113
|
@capture_method_arguments = false
|
|
110
114
|
|
|
111
|
-
#
|
|
115
|
+
# Structured logs (CloseYourIt.log / .logger). Dedicated master switch + sampling + batching.
|
|
112
116
|
@logs_enabled = true
|
|
113
117
|
@logs_sample_rate = 1.0
|
|
114
118
|
@logs_batch_size = 50
|
|
115
119
|
@logs_flush_interval = 5
|
|
116
120
|
|
|
117
|
-
# CYSK-29 —
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
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.
|
|
121
125
|
@usage_enabled = true
|
|
122
126
|
@usage_flush_interval = 300
|
|
123
127
|
@usage_max_symbols = 2000
|
|
124
|
-
#
|
|
128
|
+
# Opt-in Rails.logger broadcast → CloseYourIt.log (default OFF; sends only ≥ threshold).
|
|
125
129
|
@capture_rails_logs = false
|
|
126
130
|
@logs_min_level = :info
|
|
127
|
-
#
|
|
128
|
-
# CloseYourIt.log/.logger,
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
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).
|
|
132
136
|
@capture_rails_logs_min_level = :warn
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
#
|
|
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.
|
|
136
140
|
@excluded_log_patterns = []
|
|
137
141
|
|
|
138
|
-
# Performance issue detection (
|
|
139
|
-
# OPT-IN, default OFF:
|
|
140
|
-
#
|
|
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.
|
|
141
145
|
@detect_performance_issues = false
|
|
142
|
-
@n_plus_one_threshold = 10 #
|
|
143
|
-
@query_count_threshold = 100 #
|
|
144
|
-
@query_time_threshold_ms = 500 #
|
|
145
|
-
@slow_request_threshold_ms = 1000 #
|
|
146
|
-
@slow_external_threshold_ms = 1000 #
|
|
147
|
-
@capture_external_http = true #
|
|
148
|
-
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
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.
|
|
155
159
|
@monitor_jobs = true
|
|
156
|
-
@slow_job_threshold_ms = 5000 #
|
|
157
|
-
@job_queue_latency_threshold_ms = 60_000 #
|
|
158
|
-
@jobs_sample_rate = 1.0 #
|
|
159
|
-
|
|
160
|
-
#
|
|
161
|
-
# Net::HTTP. OPT-IN, default OFF:
|
|
162
|
-
# per
|
|
163
|
-
#
|
|
164
|
-
# (
|
|
165
|
-
# CloseYourIt →
|
|
160
|
+
@slow_job_threshold_ms = 5000 # perform duration beyond which = slow_job
|
|
161
|
+
@job_queue_latency_threshold_ms = 60_000 # enqueue→execution wait beyond which = job_queue_latency
|
|
162
|
+
@jobs_sample_rate = 1.0 # fraction of over-threshold candidates actually sent
|
|
163
|
+
|
|
164
|
+
# W3C trace context propagation (traceparent/tracestate) to external services called via
|
|
165
|
+
# Net::HTTP. OPT-IN, default OFF: injecting outgoing headers crosses a trust boundary and must be
|
|
166
|
+
# decided per app. It is limited PER DESTINATION by the allowlist (exact hosts, case-insensitive,
|
|
167
|
+
# or Regexp for subdomains); empty list = no destination. Never to unlisted hosts, never as
|
|
168
|
+
# `baggage` (which may carry PII). Inbound, a valid traceparent becomes the trace_id of the
|
|
169
|
+
# CloseYourIt events → the request's errors/metrics correlate with the distributed trace (CYRB-15).
|
|
166
170
|
@propagate_trace_context = false
|
|
167
171
|
@trace_propagation_allowlist = []
|
|
168
172
|
|
|
169
|
-
#
|
|
170
|
-
#
|
|
173
|
+
# Project root: base for the relative frame filename (cross-SDK culprit). Lazy: auto-detected
|
|
174
|
+
# from Rails.root or Dir.pwd on first access when not set explicitly.
|
|
171
175
|
@project_root = nil
|
|
172
176
|
|
|
173
177
|
@filter_parameters = []
|
|
174
178
|
@scrub_message_patterns = []
|
|
175
179
|
end
|
|
176
180
|
|
|
177
|
-
#
|
|
181
|
+
# Classes/strings → name (String); Regexps stay Regexps (pattern match on name/message).
|
|
178
182
|
def excluded_exceptions=(list)
|
|
179
183
|
@excluded_exceptions = Array(list).map { |item| item.is_a?(Regexp) ? item : item.to_s }
|
|
180
184
|
end
|
|
181
185
|
|
|
182
|
-
#
|
|
183
|
-
#
|
|
186
|
+
# Rails.logger broadcast patterns to drop. Strings become Regexps (literal match on the text):
|
|
187
|
+
# whoever writes `config.excluded_log_patterns = ["Rendered layout"]` means exactly that.
|
|
184
188
|
def excluded_log_patterns=(list)
|
|
185
189
|
@excluded_log_patterns = Array(list).map { |item| item.is_a?(Regexp) ? item : Regexp.new(Regexp.escape(item.to_s)) }
|
|
186
190
|
end
|
|
187
191
|
|
|
188
|
-
#
|
|
189
|
-
# (
|
|
192
|
+
# Queries not to measure. Same normalization as excluded_log_patterns: String = literal text
|
|
193
|
+
# (a table name is written that way, not as a pattern), Regexp = pattern.
|
|
190
194
|
def excluded_query_patterns=(list)
|
|
191
195
|
@excluded_query_patterns = Array(list).map { |item| item.is_a?(Regexp) ? item : Regexp.new(Regexp.escape(item.to_s)) }
|
|
192
196
|
end
|
|
@@ -195,8 +199,8 @@ module CloseYourIt
|
|
|
195
199
|
@filter_parameters = Array(list)
|
|
196
200
|
end
|
|
197
201
|
|
|
198
|
-
#
|
|
199
|
-
#
|
|
202
|
+
# Destinations allowed to receive the W3C trace headers. String = exact host (case-insensitive
|
|
203
|
+
# match), Regexp = pattern (for subdomains/host families). Empty list = none.
|
|
200
204
|
def trace_propagation_allowlist=(list)
|
|
201
205
|
@trace_propagation_allowlist = Array(list)
|
|
202
206
|
end
|
|
@@ -209,8 +213,8 @@ module CloseYourIt
|
|
|
209
213
|
environment.to_s == "production"
|
|
210
214
|
end
|
|
211
215
|
|
|
212
|
-
#
|
|
213
|
-
#
|
|
216
|
+
# The client sends only with complete credentials (endpoint + token + project_id) and a secure
|
|
217
|
+
# transport (http:// allowed outside production).
|
|
214
218
|
def enabled?
|
|
215
219
|
return false if blank?(endpoint_url) || blank?(token) || blank?(project_id)
|
|
216
220
|
return false if insecure_endpoint? && production?
|
|
@@ -218,8 +222,8 @@ module CloseYourIt
|
|
|
218
222
|
true
|
|
219
223
|
end
|
|
220
224
|
|
|
221
|
-
#
|
|
222
|
-
#
|
|
225
|
+
# Logs configuration warnings (e.g. http:// endpoint, malformed project_id/endpoint).
|
|
226
|
+
# Never raises: consistent with the client's no-op philosophy. Called by `CloseYourIt.init`.
|
|
223
227
|
def validate!
|
|
224
228
|
CloseYourIt.internal_logger.warn(insecure_endpoint_message) if insecure_endpoint?
|
|
225
229
|
CloseYourIt.internal_logger.warn(malformed_project_id_message) if malformed_project_id?
|
|
@@ -227,22 +231,22 @@ module CloseYourIt
|
|
|
227
231
|
self
|
|
228
232
|
end
|
|
229
233
|
|
|
230
|
-
#
|
|
234
|
+
# Effective release: the one set, otherwise auto-detected (deploy/CI ENV or git).
|
|
231
235
|
def release
|
|
232
236
|
return @release unless @release.nil?
|
|
233
237
|
|
|
234
238
|
@release = detect_release
|
|
235
239
|
end
|
|
236
240
|
|
|
237
|
-
#
|
|
238
|
-
#
|
|
241
|
+
# Effective project root: the one set, otherwise auto-detected (Rails.root or Dir.pwd).
|
|
242
|
+
# Used to make `frame.filename` relative (culprit comparable across SDKs — CYRB-4).
|
|
239
243
|
def project_root
|
|
240
244
|
return @project_root unless @project_root.nil?
|
|
241
245
|
|
|
242
246
|
@project_root = detect_project_root
|
|
243
247
|
end
|
|
244
248
|
|
|
245
|
-
# Rails.root
|
|
249
|
+
# Rails.root when the app runs under Rails, otherwise the working directory. Never raises.
|
|
246
250
|
def detect_project_root
|
|
247
251
|
return ::Rails.root.to_s if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
|
|
248
252
|
|
|
@@ -251,12 +255,12 @@ module CloseYourIt
|
|
|
251
255
|
Dir.pwd
|
|
252
256
|
end
|
|
253
257
|
|
|
254
|
-
#
|
|
255
|
-
#
|
|
258
|
+
# A semver tag (optional `v`) is preferred to the short SHA as release: it converges with what the
|
|
259
|
+
# CI records (it tags), while the short SHA creates duplicate releases on the backend (CYRB-9).
|
|
256
260
|
SEMVER_TAG = /\Av?\d+\.\d+\.\d+([-+.].+)?\z/
|
|
257
261
|
|
|
258
|
-
#
|
|
259
|
-
#
|
|
262
|
+
# Release auto-detection: first a semver tag (APP_GIT_TAG/GIT_TAG), then the short SHA from the
|
|
263
|
+
# deploy/CI env or from git. Never raises.
|
|
260
264
|
def detect_release
|
|
261
265
|
detect_tag ||
|
|
262
266
|
ENV["KAMAL_VERSION"] ||
|
|
@@ -271,8 +275,8 @@ module CloseYourIt
|
|
|
271
275
|
|
|
272
276
|
private
|
|
273
277
|
|
|
274
|
-
# `.git`
|
|
275
|
-
#
|
|
278
|
+
# `.git` is a directory in a normal checkout and a file in a worktree → File.directory? is false
|
|
279
|
+
# in worktrees, so tests do not spawn git subprocesses (deterministic).
|
|
276
280
|
def git_revision
|
|
277
281
|
return nil unless File.directory?(".git")
|
|
278
282
|
|
|
@@ -282,9 +286,9 @@ module CloseYourIt
|
|
|
282
286
|
nil
|
|
283
287
|
end
|
|
284
288
|
|
|
285
|
-
#
|
|
286
|
-
# (v
|
|
287
|
-
#
|
|
289
|
+
# Semver tag from APP_GIT_TAG then GIT_TAG: accepts only a non-blank value shaped like semver
|
|
290
|
+
# (optional v + MAJOR.MINOR.PATCH + optional suffix). Rejects `unknown`, empty, branch names,
|
|
291
|
+
# etc. → nil, so detect_release falls back to the SHA chain.
|
|
288
292
|
def detect_tag
|
|
289
293
|
tag = ENV["APP_GIT_TAG"] || ENV["GIT_TAG"]
|
|
290
294
|
return nil if blank?(tag)
|
|
@@ -299,17 +303,17 @@ module CloseYourIt
|
|
|
299
303
|
!uri.nil? && uri.scheme != "https"
|
|
300
304
|
end
|
|
301
305
|
|
|
302
|
-
#
|
|
303
|
-
#
|
|
306
|
+
# Warns when project_id is set but does not look like a UUID (the typical mistake is pasting a
|
|
307
|
+
# slug/name instead of the id). It does not block: the server is the authority on validity.
|
|
304
308
|
def malformed_project_id?
|
|
305
309
|
!blank?(project_id) && !UUID_FORMAT.match?(project_id.to_s)
|
|
306
310
|
end
|
|
307
311
|
|
|
308
312
|
def malformed_project_id_message
|
|
309
|
-
"CloseYourIt: project_id (#{project_id})
|
|
313
|
+
"CloseYourIt: project_id (#{project_id}) is not shaped like a UUID — check that you copied the right id."
|
|
310
314
|
end
|
|
311
315
|
|
|
312
|
-
#
|
|
316
|
+
# Warns when endpoint_url is set but not parsable or has no host.
|
|
313
317
|
def malformed_endpoint?
|
|
314
318
|
return false if blank?(endpoint_url)
|
|
315
319
|
|
|
@@ -318,12 +322,12 @@ module CloseYourIt
|
|
|
318
322
|
end
|
|
319
323
|
|
|
320
324
|
def malformed_endpoint_message
|
|
321
|
-
"CloseYourIt: endpoint_url (#{endpoint_url})
|
|
325
|
+
"CloseYourIt: endpoint_url (#{endpoint_url}) is not a valid URL (missing host)."
|
|
322
326
|
end
|
|
323
327
|
|
|
324
328
|
def insecure_endpoint_message
|
|
325
|
-
tail = production? ? "
|
|
326
|
-
"CloseYourIt: endpoint_url
|
|
329
|
+
tail = production? ? "Telemetry DISABLED in production." : "Allowed only in development."
|
|
330
|
+
"CloseYourIt: endpoint_url uses insecure http:// (#{endpoint_url}) — the token would travel in clear text. #{tail}"
|
|
327
331
|
end
|
|
328
332
|
|
|
329
333
|
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)
|