closeyourit-ruby 0.8.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1482102089ba9e319a50f2d3822b63fc76ddc9a63c6c629f1d7cdc53022ca362
4
- data.tar.gz: f312feb36b22702dca3109c62f623ba4484d25caf82dc9b25c807fe89f2ea2ba
3
+ metadata.gz: e7fb813ea37f0fb206ed31a5a960f795e364c6b23902d59b9b5d6e56ff4fb295
4
+ data.tar.gz: 926f4e714781796b43ad8c9eaf0e6bf52d7976599a188d3e3720637df623d2bb
5
5
  SHA512:
6
- metadata.gz: 4f6a4d467f80966599d057452927a75c17126cb01def6de0d1e365cb0fa5dcbd458122e3b860b05906d35bb307b29eed0d8e9ee9b5afb1b4008170dd3462205d
7
- data.tar.gz: 6f4a5ee0fb8dea5b160c1447624d1f409a1a36df63700c46dc66390c9d4fc2ab932e04a1c50c2dacec9c0202f73af7b697250786bfd52f8d6303fdaaebfdbff8
6
+ metadata.gz: d8d590e358c098c3061383a85f9e1593b0b574a0ee52fcb67bbdd76e215cf44076703d9a1917d52acc99e43dee0ba58113afd9516c44b1adaa807341c22cf3ee
7
+ data.tar.gz: e492934a7b0329fe0ba887dc45b05fa8c148ac3fb9665d866f95bdfe25b1cd114aa66bf95bca76cb83f3f16a96a34b4b24165152a247971e43888358e69291f9
data/README.md CHANGED
@@ -8,9 +8,9 @@ installa per inviare a CloseYourIt:
8
8
  - **Query/metodi lenti** → sul path `/api/v1/projects/:id/metrics` (pipeline metriche dedicata,
9
9
  raggruppate per signature con aggregati di durata) — ciò che Sentry/GlitchTip non danno bene.
10
10
 
11
- È il **client primario** di CloseYourIt: replica e migliora ciò che fa un SDK Sentry (request context,
12
- user/tag/contesto, breadcrumbs, errori nei background job, errori handled, sampling, messaggi) senza
13
- dipendere dagli SDK Sentry.
11
+ È il **client primario** di CloseYourIt e copre, senza dipendere dagli SDK Sentry, ciò per cui di
12
+ solito se ne installa uno: request context, user/tag/contesto, breadcrumbs, errori nei background job,
13
+ errori handled, sampling, messaggi.
14
14
 
15
15
  Caratteristiche:
16
16
  - **Contesto ricco** sull'errore: request HTTP (method/url/header), user/tag/contesto custom, breadcrumbs
@@ -24,6 +24,13 @@ Caratteristiche:
24
24
 
25
25
  > Design e contratto dati in [`PDR.md`](PDR.md).
26
26
 
27
+ > **Rispetto agli altri SDK.** Ruby, JavaScript, Dart e Python **non coprono le stesse funzioni**: le
28
+ > metriche dei background job e la telemetria d'uso oggi esistono solo qui, i `pageview` e la session
29
+ > replay solo in JavaScript, la coda offline su disco solo in Dart. Cosa c'è dove — con la versione
30
+ > minima e il sorgente che lo implementa — sta in una tabella sola, `compatibility/sdk-feature-parity.md`
31
+ > nel repo interno [closeyourit-docs](https://github.com/bussolabs/closeyourit-docs/blob/main/compatibility/sdk-feature-parity.md).
32
+ > Questo README descrive ciò che fa la gemma Ruby, non ciò che fanno gli altri SDK.
33
+
27
34
  ## Installazione
28
35
 
29
36
  Pubblicata su [RubyGems](https://rubygems.org/gems/closeyourit-ruby):
@@ -172,8 +179,8 @@ CloseYourIt.logger.warn("cache miss", key: cache_key)
172
179
  CloseYourIt.logger.error("job fallito", job: self.class.name)
173
180
  ```
174
181
 
175
- La **sorgente** del log (campo `logger`) si imposta con `.named` — child logger immutabile, stesso
176
- idioma di dart/js. Il logger radice resta invariato:
182
+ La **sorgente** del log (campo `logger`) si imposta con `.named` — child logger immutabile, come in
183
+ dart/js. Il logger radice resta invariato:
177
184
 
178
185
  ```ruby
179
186
  payments = CloseYourIt.logger.named("payments") # child con sorgente "payments"
@@ -186,7 +193,7 @@ come sorgente): `CloseYourIt.logger.warn("disco pieno", logger: "/dev/sda1")` me
186
193
 
187
194
  Ogni log sotto `logs_min_level` (default `:info`) è **scartato** prima di essere costruito o spedito —
188
195
  `CloseYourIt.log` e `CloseYourIt.logger.*` usano la mappa di livelli (`debug` < `info` < `warning` <
189
- `error` < `fatal`), identica agli SDK dart/js. Un `CloseYourIt.logger.debug(...)` in un loop caldo con
196
+ `error` < `fatal`), con gli stessi nomi di dart/js. Un `CloseYourIt.logger.debug(...)` in un loop caldo con
190
197
  la soglia di default non produce alcun invio.
191
198
 
192
199
  Con Rails, `capture_rails_logs = true` inoltra anche `Rails.logger` allo stream, ma con una soglia
@@ -78,6 +78,22 @@ module CloseYourIt
78
78
  payloads
79
79
  end
80
80
 
81
+ # CYSK-29 — il flush della telemetria d'uso: UNA POST per finestra, payload privo di dati utente
82
+ # per costruzione (route = Controller#action). Fire-and-forget via worker, come tutto il resto.
83
+ def flush_usage(symbols:, truncated:, window_started_at:, window_ended_at:)
84
+ payload = {
85
+ environment: @configuration.environment.to_s,
86
+ release: @configuration.release,
87
+ sdk: { name: "closeyourit-ruby", version: CloseYourIt::VERSION },
88
+ window_started_at: window_started_at, window_ended_at: window_ended_at,
89
+ truncated: truncated, symbols: symbols
90
+ }
91
+ path = "/api/v1/projects/#{@configuration.project_id}/usages"
92
+ accepted = @worker.perform { @transport.send_event(payload, path: path) }
93
+ CloseYourIt.notify_diagnostic(:enqueue, path: path, batch: symbols.size) if accepted
94
+ payload
95
+ end
96
+
81
97
  def shutdown
82
98
  @worker.shutdown
83
99
  end
@@ -41,7 +41,8 @@ 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, :propagate_trace_context,
45
+ :usage_enabled, :usage_flush_interval, :usage_max_symbols
45
46
  attr_writer :release, :project_root
46
47
  attr_reader :excluded_exceptions, :excluded_log_patterns, :excluded_query_patterns,
47
48
  :filter_parameters, :scrub_message_patterns, :trace_propagation_allowlist
@@ -112,6 +113,14 @@ module CloseYourIt
112
113
  @logs_sample_rate = 1.0
113
114
  @logs_batch_size = 50
114
115
  @logs_flush_interval = 5
116
+
117
+ # CYSK-29 — telemetria d'uso: quali rotte/job/chiavi girano davvero. Il payload è privo di
118
+ # dati utente per costruzione (route = Controller#action, mai l'URL), quindi il default è ON:
119
+ # trenta giorni di raccolta facoltativa hanno insegnato che facoltativo significa mai.
120
+ # Il registro si svuota a ogni flush: il tetto limita i simboli DISTINTI per finestra.
121
+ @usage_enabled = true
122
+ @usage_flush_interval = 300
123
+ @usage_max_symbols = 2000
115
124
  # Broadcast opt-in di Rails.logger → CloseYourIt.log (default OFF; spedisce solo ≥ soglia).
116
125
  @capture_rails_logs = false
117
126
  @logs_min_level = :info
@@ -68,6 +68,9 @@ module CloseYourIt
68
68
  ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
69
69
  event = ActiveSupport::Notifications::Event.new(*args)
70
70
  payload = event.payload
71
+ # CYSK-29 — telemetria d'uso: la rotta è `Controller#action`, MAI l'URL. Il registro fa
72
+ # una lookup e un increment; il gate usage_enabled sta dentro #record.
73
+ CloseYourIt.usage_registry.record("route", "#{payload[:controller]}##{payload[:action]}") if CloseYourIt.enabled?
71
74
  perf.record(
72
75
  route: "#{payload[:controller]}##{payload[:action]}",
73
76
  duration_ms: event.duration
@@ -104,6 +107,8 @@ module CloseYourIt
104
107
  ActiveSupport::Notifications.subscribe("perform.active_job") do |*args|
105
108
  event = ActiveSupport::Notifications::Event.new(*args)
106
109
  job = event.payload[:job]
110
+ # CYSK-29 — anche i job dichiarano di essere girati: kind `job`, simbolo = la classe.
111
+ CloseYourIt.usage_registry.record("job", job.class.name) if job && CloseYourIt.enabled?
107
112
  jobs.active_job_performed(job, event.duration) if job
108
113
  end
109
114
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module CloseYourIt
6
+ # CYSK-29 — la telemetria d'uso: registra QUALI simboli girano davvero (route come
7
+ # `Controller#action`, job, chiavi custom letterali) e li flusha in una POST ogni
8
+ # `usage_flush_interval` secondi per processo. Sul percorso caldo: una lookup e un increment.
9
+ #
10
+ # Le regole che rendono il sistema immune alle perdite:
11
+ # - i conteggi sono INDICATIVI, solo `last_seen_at` è portante: un flush perso costa una finestra
12
+ # su un simbolo che si rivede subito dopo;
13
+ # - il registro SI SVUOTA a ogni flush: il tetto (`usage_max_symbols`) limita i simboli distinti
14
+ # visti in una finestra — una grandezza legata al traffico, non alla dimensione del codice;
15
+ # - oltre il tetto il flush porta `truncated: true`, e lato scanner quel flag SQUALIFICA il kind;
16
+ # - nessun sampling, mai: un campionamento su una rotta chiamata 3 volte al mese fabbrica
17
+ # esattamente il falso «mai vista» che il sistema esiste per evitare;
18
+ # - `route` è `Controller#action`, MAI l'URL: niente path, parametri, user, IP.
19
+ class UsageRegistry
20
+ # I simboli vengono da payload di framework o da stringhe letterali: il pattern è il contratto.
21
+ SYMBOL_FORMAT = %r{\A[A-Za-z0-9_:#./-]{1,200}\z}
22
+ KINDS = %w[route job custom].freeze
23
+
24
+ attr_reader :timer # esposto per i test (verifica dell'intervallo configurato)
25
+
26
+ def initialize(client:, configuration:)
27
+ @client = client
28
+ @configuration = configuration
29
+ @mutex = Mutex.new
30
+ @symbols = {}
31
+ @truncated = false
32
+ @window_started_at = Time.now.utc
33
+ @timer = nil
34
+ end
35
+
36
+ def record(kind, symbol)
37
+ return unless @configuration.usage_enabled
38
+
39
+ kind = kind.to_s
40
+ symbol = symbol.to_s
41
+ return unless KINDS.include?(kind) && symbol.match?(SYMBOL_FORMAT)
42
+
43
+ @mutex.synchronize do
44
+ key = [ kind, symbol ]
45
+ entry = @symbols[key]
46
+ if entry
47
+ entry[:count] += 1
48
+ entry[:last_seen_at] = Time.now.utc
49
+ elsif @symbols.size >= @configuration.usage_max_symbols.to_i
50
+ @truncated = true
51
+ else
52
+ @symbols[key] = { count: 1, last_seen_at: Time.now.utc }
53
+ end
54
+ ensure_timer
55
+ end
56
+ end
57
+
58
+ def flush
59
+ payload = drain
60
+ return if payload[:symbols].empty? && !payload[:truncated]
61
+
62
+ @client.flush_usage(**payload)
63
+ rescue StandardError => e
64
+ # Il flush gira sul thread timer: un errore non deve propagare nell'app né uccidere il
65
+ # TimerTask. La semantica portante è last_seen_at: una finestra persa non falsifica niente.
66
+ CloseYourIt.internal_logger.error("CloseYourIt usage registry: #{e.class}: #{e.message}")
67
+ end
68
+
69
+ def shutdown
70
+ @timer&.shutdown
71
+ flush
72
+ end
73
+
74
+ private
75
+
76
+ def drain
77
+ @mutex.synchronize do
78
+ symbols = @symbols.map do |(kind, symbol), entry|
79
+ { kind: kind, symbol: symbol, count: entry[:count], last_seen_at: entry[:last_seen_at].iso8601 }
80
+ end
81
+ payload = { symbols: symbols, truncated: @truncated,
82
+ window_started_at: @window_started_at.iso8601, window_ended_at: Time.now.utc.iso8601 }
83
+ @symbols = {}
84
+ @truncated = false
85
+ @window_started_at = Time.now.utc
86
+ payload
87
+ end
88
+ end
89
+
90
+ def ensure_timer
91
+ return if @timer
92
+
93
+ interval = @configuration.usage_flush_interval.to_i
94
+ interval = 300 if interval <= 0
95
+ @timer = Concurrent::TimerTask.new(execution_interval: interval) { flush }
96
+ @timer.execute
97
+ end
98
+ end
99
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloseYourIt
4
- VERSION = "0.8.0"
4
+ VERSION = "0.9.3"
5
5
  end
@@ -23,6 +23,7 @@ require_relative "closeyourit/performance/request_profile"
23
23
  require_relative "closeyourit/performance/rollup"
24
24
  require_relative "closeyourit/log_device"
25
25
  require_relative "closeyourit/log_buffer"
26
+ require_relative "closeyourit/usage_registry"
26
27
  require_relative "closeyourit/subscribers/slow_query"
27
28
  require_relative "closeyourit/subscribers/request_performance"
28
29
  require_relative "closeyourit/subscribers/job_performance"
@@ -70,6 +71,7 @@ module CloseYourIt
70
71
  @configuration = Configuration.new
71
72
  @client = nil
72
73
  @log_buffer = nil
74
+ @usage_registry = nil
73
75
  @shutdown_notified = false # nuova sessione: :shutdown potrà essere notificato di nuovo
74
76
  yield(@configuration) if block_given?
75
77
  @configuration.validate!
@@ -264,7 +266,8 @@ module CloseYourIt
264
266
  # da CloseYourIt.init; con `config.trap_signals` viene raggiunto anche su SIGTERM. Idempotente:
265
267
  # richiamarlo è sicuro (buffer già vuoto, worker già fermo → no-op).
266
268
  def shutdown
267
- # Ordine critico: prima il buffer (accoda l'ultimo batch nel worker), poi il worker (lo drena).
269
+ # Ordine critico: prima i buffer (accodano l'ultimo batch nel worker), poi il worker (li drena).
270
+ @usage_registry&.shutdown
268
271
  @log_buffer&.shutdown
269
272
  @client&.shutdown
270
273
  # Riepilogo di fine-vita: l'app riceve lo snapshot dei contatori senza log rumorosi. Emesso una
@@ -323,6 +326,22 @@ module CloseYourIt
323
326
  nil
324
327
  end
325
328
 
329
+ # CYSK-29 — il registro della telemetria d'uso (una lookup + increment sul percorso caldo).
330
+ def usage_registry
331
+ ensure_current_process!
332
+ @usage_registry ||= UsageRegistry.new(client: client, configuration: configuration)
333
+ end
334
+
335
+ # CYSK-29 — dichiara che un pezzo di codice è stato ESEGUITO. `key` deve essere una stringa
336
+ # LETTERALE (mai interpolata con dati): è l'unico modo onesto di rispondere a «questo ramo viene
337
+ # mai preso?». No-op se la gemma non è configurata o usage_enabled è OFF.
338
+ def used(key)
339
+ return nil unless configured? && enabled?
340
+
341
+ usage_registry.record("custom", key)
342
+ nil
343
+ end
344
+
326
345
  private
327
346
 
328
347
  # Vero se il thread corrente sta eseguendo l'hook diagnostico: le API di telemetria diventano no-op
@@ -349,6 +368,7 @@ module CloseYourIt
349
368
  @log_buffer ||= LogBuffer.new(client: client, configuration: configuration)
350
369
  end
351
370
 
371
+
352
372
  # Rileva un fork confrontando il PID del processo in cui @client/@log_buffer sono stati materializzati
353
373
  # con quello corrente. In un figlio forkato i due oggetti sono ereditati dal padre, ma i loro thread
354
374
  # — il worker pool di Client e il TimerTask di LogBuffer — vivono solo nel padre (il fork copia il
@@ -371,6 +391,7 @@ module CloseYourIt
371
391
  def discard_inherited_client!
372
392
  @client = nil
373
393
  @log_buffer = nil
394
+ @usage_registry = nil
374
395
  end
375
396
 
376
397
  # I log seguono il master switch del client + il proprio flag dedicato.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: closeyourit-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessio Bussolari
@@ -87,6 +87,7 @@ files:
87
87
  - lib/closeyourit/subscribers/slow_query.rb
88
88
  - lib/closeyourit/trace_context.rb
89
89
  - lib/closeyourit/transport.rb
90
+ - lib/closeyourit/usage_registry.rb
90
91
  - lib/closeyourit/version.rb
91
92
  homepage: https://github.com/bussolabs/closeyourit-ruby
92
93
  licenses: