closeyourit-ruby 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +177 -0
- data/lib/closeyourit/background_worker.rb +45 -0
- data/lib/closeyourit/breadcrumb.rb +35 -0
- data/lib/closeyourit/breadcrumb_buffer.rb +32 -0
- data/lib/closeyourit/client.rb +30 -0
- data/lib/closeyourit/configuration.rb +168 -0
- data/lib/closeyourit/event.rb +57 -0
- data/lib/closeyourit/events/error_event.rb +94 -0
- data/lib/closeyourit/events/message_event.rb +35 -0
- data/lib/closeyourit/events/slow_method_event.rb +61 -0
- data/lib/closeyourit/events/slow_query_event.rb +61 -0
- data/lib/closeyourit/instrumenter.rb +29 -0
- data/lib/closeyourit/monitor.rb +34 -0
- data/lib/closeyourit/rails/active_job_extension.rb +42 -0
- data/lib/closeyourit/rails/capture_exceptions.rb +21 -0
- data/lib/closeyourit/rails/error_subscriber.rb +30 -0
- data/lib/closeyourit/rails/query_source.rb +17 -0
- data/lib/closeyourit/rails/railtie.rb +74 -0
- data/lib/closeyourit/rails/request_context.rb +75 -0
- data/lib/closeyourit/scope.rb +115 -0
- data/lib/closeyourit/scrubber.rb +71 -0
- data/lib/closeyourit/sidekiq/error_handler.rb +25 -0
- data/lib/closeyourit/subscribers/slow_query.rb +55 -0
- data/lib/closeyourit/transport.rb +47 -0
- data/lib/closeyourit/version.rb +5 -0
- data/lib/closeyourit-ruby.rb +193 -0
- metadata +84 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloseYourIt
|
|
4
|
+
module Sidekiq
|
|
5
|
+
# Error handler Sidekiq (registrato dal railtie solo se Sidekiq è presente). Sidekiq invoca
|
|
6
|
+
# `call(exception, context, config)` e NON ri-solleva → qui catturiamo e basta.
|
|
7
|
+
class ErrorHandler
|
|
8
|
+
def call(exception, context, _config = nil)
|
|
9
|
+
apply_job_scope(context)
|
|
10
|
+
CloseYourIt.capture_exception(exception, handled: false)
|
|
11
|
+
ensure
|
|
12
|
+
CloseYourIt::Scope.reset!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def apply_job_scope(context)
|
|
18
|
+
job = (context && context[:job]) || {}
|
|
19
|
+
CloseYourIt.set_tag("job.class", job["class"]) if job["class"]
|
|
20
|
+
CloseYourIt.set_tag("job.queue", job["queue"]) if job["queue"]
|
|
21
|
+
CloseYourIt.set_context("sidekiq", { "jid" => job["jid"] }) if job["jid"]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../events/slow_query_event"
|
|
4
|
+
require_relative "../scrubber"
|
|
5
|
+
|
|
6
|
+
module CloseYourIt
|
|
7
|
+
module Subscribers
|
|
8
|
+
# Riceve i dati di un evento `sql.active_record` e, se la query supera la soglia
|
|
9
|
+
# (escludendo SCHEMA/CACHE/TRANSACTION), invia un evento `slow_query`.
|
|
10
|
+
# Logica pura: il wiring ad ActiveSupport::Notifications vive nel Railtie.
|
|
11
|
+
class SlowQuery
|
|
12
|
+
IGNORED_NAMES = %w[SCHEMA CACHE TRANSACTION].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(configuration = nil)
|
|
15
|
+
@configuration = configuration
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def record(name:, duration_ms:, sql:, cached: false, connection: nil,
|
|
19
|
+
binds: nil, type_casted_binds: nil, source: nil)
|
|
20
|
+
config = @configuration || CloseYourIt.configuration
|
|
21
|
+
return if ignored_name?(name)
|
|
22
|
+
return if duration_ms < config.slow_query_threshold_ms
|
|
23
|
+
|
|
24
|
+
event = SlowQueryEvent.new(
|
|
25
|
+
{ name: name, sql: sql, cached: cached, connection: connection,
|
|
26
|
+
binds: binds, type_casted_binds: type_casted_binds, source: source },
|
|
27
|
+
duration_ms,
|
|
28
|
+
config
|
|
29
|
+
)
|
|
30
|
+
CloseYourIt.capture_event(event)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Breadcrumb per OGNI query non di sistema (non solo lente): SQL offuscato, niente bind.
|
|
34
|
+
# Dà la cronologia "quali query prima del crash" allegata all'evento d'errore.
|
|
35
|
+
def breadcrumb(name:, sql:, duration_ms:, cached: false)
|
|
36
|
+
config = @configuration || CloseYourIt.configuration
|
|
37
|
+
return if ignored_name?(name)
|
|
38
|
+
return unless config.breadcrumbs_enabled
|
|
39
|
+
|
|
40
|
+
CloseYourIt.add_breadcrumb(
|
|
41
|
+
category: "query",
|
|
42
|
+
type: "query",
|
|
43
|
+
message: Scrubber.new(config).obfuscate_sql(sql),
|
|
44
|
+
data: { "name" => name, "duration_ms" => duration_ms.to_f.round(2), "cached" => cached }
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def ignored_name?(name)
|
|
51
|
+
name.nil? || IGNORED_NAMES.include?(name)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module CloseYourIt
|
|
8
|
+
# Spedisce un payload a un path di ingest (errori → /events, metriche → /metrics) via HTTP POST
|
|
9
|
+
# con `Authorization: Bearer`. Mai solleva: ogni errore di rete è loggato e ingoiato.
|
|
10
|
+
class Transport
|
|
11
|
+
OPEN_TIMEOUT = 2
|
|
12
|
+
READ_TIMEOUT = 3
|
|
13
|
+
|
|
14
|
+
def initialize(configuration)
|
|
15
|
+
@configuration = configuration
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def send_event(payload, path:)
|
|
19
|
+
post(payload, path)
|
|
20
|
+
rescue StandardError => e
|
|
21
|
+
CloseYourIt.logger.error("CloseYourIt transport: #{e.class}: #{e.message}")
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def post(payload, path)
|
|
28
|
+
uri = URI.parse("#{base_url}#{path}")
|
|
29
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
30
|
+
http.use_ssl = uri.scheme == "https"
|
|
31
|
+
http.open_timeout = OPEN_TIMEOUT
|
|
32
|
+
http.read_timeout = READ_TIMEOUT
|
|
33
|
+
|
|
34
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
35
|
+
request["Authorization"] = "Bearer #{@configuration.token}"
|
|
36
|
+
request["Content-Type"] = "application/json"
|
|
37
|
+
request["User-Agent"] = "closeyourit-ruby/#{VERSION}"
|
|
38
|
+
request.body = JSON.generate(payload)
|
|
39
|
+
|
|
40
|
+
http.request(request)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def base_url
|
|
44
|
+
@configuration.endpoint_url.to_s.chomp("/")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
require_relative "closeyourit/version"
|
|
6
|
+
require_relative "closeyourit/configuration"
|
|
7
|
+
require_relative "closeyourit/breadcrumb"
|
|
8
|
+
require_relative "closeyourit/scope"
|
|
9
|
+
require_relative "closeyourit/scrubber"
|
|
10
|
+
require_relative "closeyourit/background_worker"
|
|
11
|
+
require_relative "closeyourit/transport"
|
|
12
|
+
require_relative "closeyourit/event"
|
|
13
|
+
require_relative "closeyourit/events/error_event"
|
|
14
|
+
require_relative "closeyourit/events/message_event"
|
|
15
|
+
require_relative "closeyourit/events/slow_query_event"
|
|
16
|
+
require_relative "closeyourit/events/slow_method_event"
|
|
17
|
+
require_relative "closeyourit/subscribers/slow_query"
|
|
18
|
+
require_relative "closeyourit/instrumenter"
|
|
19
|
+
require_relative "closeyourit/monitor"
|
|
20
|
+
require_relative "closeyourit/client"
|
|
21
|
+
require_relative "closeyourit/rails/capture_exceptions"
|
|
22
|
+
require_relative "closeyourit/rails/request_context"
|
|
23
|
+
require_relative "closeyourit/rails/active_job_extension"
|
|
24
|
+
require_relative "closeyourit/rails/error_subscriber"
|
|
25
|
+
require_relative "closeyourit/sidekiq/error_handler"
|
|
26
|
+
|
|
27
|
+
# CloseYourIt — client di telemetria (errori + statistiche di query/metodi lenti)
|
|
28
|
+
# che invia gli eventi all'endpoint di ingest di CloseYourIt.
|
|
29
|
+
#
|
|
30
|
+
# Entry point della gemma (file con trattino come `sentry-ruby`):
|
|
31
|
+
# `require "closeyourit-ruby"` carica il modulo `CloseYourIt`.
|
|
32
|
+
module CloseYourIt
|
|
33
|
+
# Eccezione base interna: usata per evitare loop (le nostre eccezioni non vengono catturate).
|
|
34
|
+
class Error < StandardError; end
|
|
35
|
+
|
|
36
|
+
CAPTURED_FLAG = :@__closeyourit_captured
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
# Configura il client. Senza token/endpoint → no-op.
|
|
40
|
+
def init
|
|
41
|
+
@configuration = Configuration.new
|
|
42
|
+
@client = nil
|
|
43
|
+
yield(@configuration) if block_given?
|
|
44
|
+
@configuration.validate!
|
|
45
|
+
@configuration
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def configuration
|
|
49
|
+
@configuration ||= Configuration.new
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def configured?
|
|
53
|
+
!@configuration.nil?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def enabled?
|
|
57
|
+
configuration.enabled?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Cattura un'eccezione e la spedisce (fire-and-forget). No-op se disabilitato,
|
|
61
|
+
# se l'eccezione è esclusa o già catturata.
|
|
62
|
+
def capture_exception(exception, handled: false, level: "error", contexts: nil)
|
|
63
|
+
return nil unless enabled?
|
|
64
|
+
return nil if ignored_exception?(exception)
|
|
65
|
+
return nil if exception_captured?(exception)
|
|
66
|
+
|
|
67
|
+
mark_captured(exception)
|
|
68
|
+
return nil unless sampled?
|
|
69
|
+
|
|
70
|
+
event = ErrorEvent.from_exception(
|
|
71
|
+
exception, configuration: configuration, handled: handled, level: level, contexts: contexts
|
|
72
|
+
)
|
|
73
|
+
client.capture_event(event)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Spedisce un evento già costruito (slow_query/slow_method).
|
|
77
|
+
def capture_event(event)
|
|
78
|
+
return nil unless enabled?
|
|
79
|
+
|
|
80
|
+
client.capture_event(event)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Invia un messaggio diagnostico esplicito (non un'eccezione). Soggetto a sampling + scope.
|
|
84
|
+
# CloseYourIt.capture_message("cache miss storm", level: "warning")
|
|
85
|
+
def capture_message(message, level: "info")
|
|
86
|
+
return nil unless enabled?
|
|
87
|
+
return nil unless sampled?
|
|
88
|
+
|
|
89
|
+
event = MessageEvent.new(message, level: level, configuration: configuration)
|
|
90
|
+
client.capture_event(event)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Cronometra un blocco e invia un slow_method se supera la soglia.
|
|
94
|
+
# CloseYourIt.measure("checkout.total") { ... }
|
|
95
|
+
def measure(label, &block)
|
|
96
|
+
Instrumenter.measure(label, &block)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# --- Scope per-richiesta/job (user/tags/extra/contexts) ---
|
|
100
|
+
# Arricchiscono l'evento corrente; resettati a fine richiesta/job da middleware e estensioni.
|
|
101
|
+
|
|
102
|
+
def set_user(attributes)
|
|
103
|
+
Scope.current.set_user(attributes)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def set_tag(key, value)
|
|
107
|
+
Scope.current.set_tag(key, value)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def set_tags(attributes)
|
|
111
|
+
Scope.current.set_tags(attributes)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def set_context(key, attributes)
|
|
115
|
+
Scope.current.set_context(key, attributes)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def set_extra(key, value)
|
|
119
|
+
Scope.current.set_extra(key, value)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def configure_scope
|
|
123
|
+
yield(Scope.current) if block_given?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def clear_scope
|
|
127
|
+
Scope.reset!
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Aggiunge una briciola di contesto (query, navigazione, evento custom) all'evento corrente.
|
|
131
|
+
# No-op se breadcrumbs disabilitati; `data` viene scrubato (denylist) prima di essere salvato.
|
|
132
|
+
def add_breadcrumb(message: nil, category: nil, type: "default", level: "info", data: {})
|
|
133
|
+
return nil unless configuration.breadcrumbs_enabled
|
|
134
|
+
|
|
135
|
+
scrubbed = data.nil? || data.empty? ? data : Scrubber.new(configuration).filter_params(data)
|
|
136
|
+
Scope.current.add_breadcrumb(
|
|
137
|
+
Breadcrumb.new(message: message, category: category, type: type, level: level, data: scrubbed)
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def logger
|
|
142
|
+
@logger ||= default_logger
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
attr_writer :logger
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def client
|
|
150
|
+
@client ||= Client.new(configuration)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def ignored_exception?(exception)
|
|
154
|
+
return true if exception.is_a?(CloseYourIt::Error)
|
|
155
|
+
|
|
156
|
+
names = exception.class.ancestors.grep(Class).map(&:name).compact
|
|
157
|
+
configuration.excluded_exceptions.any? do |matcher|
|
|
158
|
+
if matcher.is_a?(Regexp)
|
|
159
|
+
names.any? { |name| matcher.match?(name) } || matcher.match?(exception.message.to_s)
|
|
160
|
+
else
|
|
161
|
+
names.include?(matcher)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Sampling probabilistico: 1.0 invia sempre, 0.0 mai, intermedio via Random.rand.
|
|
167
|
+
def sampled?
|
|
168
|
+
rate = configuration.sample_rate.to_f
|
|
169
|
+
return true if rate >= 1.0
|
|
170
|
+
return false if rate <= 0.0
|
|
171
|
+
|
|
172
|
+
Random.rand < rate
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def exception_captured?(exception)
|
|
176
|
+
exception.instance_variable_defined?(CAPTURED_FLAG)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def mark_captured(exception)
|
|
180
|
+
exception.instance_variable_set(CAPTURED_FLAG, true)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def default_logger
|
|
184
|
+
::Logger.new($stdout).tap do |l|
|
|
185
|
+
l.level = ::Logger::WARN
|
|
186
|
+
l.progname = "CloseYourIt"
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Integrazione Rails automatica (solo se Rails è presente).
|
|
193
|
+
require_relative "closeyourit/rails/railtie" if defined?(::Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: closeyourit-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Alessio Bussolari
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: concurrent-ruby
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.3'
|
|
26
|
+
description: Gemma client che cattura eccezioni e le statistiche di query e metodi
|
|
27
|
+
lenti e le invia, fire-and-forget, all'endpoint di ingest di CloseYourIt.
|
|
28
|
+
email:
|
|
29
|
+
- hello@bussolarialessio.me
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- LICENSE.txt
|
|
35
|
+
- README.md
|
|
36
|
+
- lib/closeyourit-ruby.rb
|
|
37
|
+
- lib/closeyourit/background_worker.rb
|
|
38
|
+
- lib/closeyourit/breadcrumb.rb
|
|
39
|
+
- lib/closeyourit/breadcrumb_buffer.rb
|
|
40
|
+
- lib/closeyourit/client.rb
|
|
41
|
+
- lib/closeyourit/configuration.rb
|
|
42
|
+
- lib/closeyourit/event.rb
|
|
43
|
+
- lib/closeyourit/events/error_event.rb
|
|
44
|
+
- lib/closeyourit/events/message_event.rb
|
|
45
|
+
- lib/closeyourit/events/slow_method_event.rb
|
|
46
|
+
- lib/closeyourit/events/slow_query_event.rb
|
|
47
|
+
- lib/closeyourit/instrumenter.rb
|
|
48
|
+
- lib/closeyourit/monitor.rb
|
|
49
|
+
- lib/closeyourit/rails/active_job_extension.rb
|
|
50
|
+
- lib/closeyourit/rails/capture_exceptions.rb
|
|
51
|
+
- lib/closeyourit/rails/error_subscriber.rb
|
|
52
|
+
- lib/closeyourit/rails/query_source.rb
|
|
53
|
+
- lib/closeyourit/rails/railtie.rb
|
|
54
|
+
- lib/closeyourit/rails/request_context.rb
|
|
55
|
+
- lib/closeyourit/scope.rb
|
|
56
|
+
- lib/closeyourit/scrubber.rb
|
|
57
|
+
- lib/closeyourit/sidekiq/error_handler.rb
|
|
58
|
+
- lib/closeyourit/subscribers/slow_query.rb
|
|
59
|
+
- lib/closeyourit/transport.rb
|
|
60
|
+
- lib/closeyourit/version.rb
|
|
61
|
+
homepage: https://github.com/bussolabs/closeyourit-ruby
|
|
62
|
+
licenses:
|
|
63
|
+
- MIT
|
|
64
|
+
metadata:
|
|
65
|
+
homepage_uri: https://github.com/bussolabs/closeyourit-ruby
|
|
66
|
+
source_code_uri: https://github.com/bussolabs/closeyourit-ruby
|
|
67
|
+
rdoc_options: []
|
|
68
|
+
require_paths:
|
|
69
|
+
- lib
|
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '4.0'
|
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '0'
|
|
80
|
+
requirements: []
|
|
81
|
+
rubygems_version: 4.0.10
|
|
82
|
+
specification_version: 4
|
|
83
|
+
summary: Client di telemetria per CloseYourIt (errori + query/metodi lenti).
|
|
84
|
+
test_files: []
|