closeyourit-ruby 0.8.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1482102089ba9e319a50f2d3822b63fc76ddc9a63c6c629f1d7cdc53022ca362
4
- data.tar.gz: f312feb36b22702dca3109c62f623ba4484d25caf82dc9b25c807fe89f2ea2ba
3
+ metadata.gz: a23acf258deb222638d8d9bd85f57ffc6fbf36429f9c87b50896814b0245e2e3
4
+ data.tar.gz: 7e0cb657fde07728331a131932242fb58685929af5ac88ffc7bfdf707ba3e301
5
5
  SHA512:
6
- metadata.gz: 4f6a4d467f80966599d057452927a75c17126cb01def6de0d1e365cb0fa5dcbd458122e3b860b05906d35bb307b29eed0d8e9ee9b5afb1b4008170dd3462205d
7
- data.tar.gz: 6f4a5ee0fb8dea5b160c1447624d1f409a1a36df63700c46dc66390c9d4fc2ab932e04a1c50c2dacec9c0202f73af7b697250786bfd52f8d6303fdaaebfdbff8
6
+ metadata.gz: 964551d2a915c4c150dd586362dd110255f361bf8c93e20f8060de7f7c253f737d25d48f1776108f5556f6739e2c909af83dc71c4f1e568ad09797a99a1a755e
7
+ data.tar.gz: b371074aed62189a65b68f8ce3ad9725f96650b6f11fd871bfd228efdf30c3c2c6331fb93990869230c7e211d3191c3690f609f5873503cf1736a44e37bcc28c
@@ -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.2"
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.2
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: