closeyourit-ruby 0.10.0 → 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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/closeyourit/background_worker.rb +14 -14
  3. data/lib/closeyourit/breadcrumb.rb +2 -2
  4. data/lib/closeyourit/breadcrumb_buffer.rb +2 -2
  5. data/lib/closeyourit/client.rb +24 -24
  6. data/lib/closeyourit/configuration.rb +102 -102
  7. data/lib/closeyourit/event.rb +6 -6
  8. data/lib/closeyourit/events/error_event.rb +16 -16
  9. data/lib/closeyourit/events/job_metric_event.rb +8 -8
  10. data/lib/closeyourit/events/log_event.rb +17 -17
  11. data/lib/closeyourit/events/message_event.rb +4 -4
  12. data/lib/closeyourit/events/performance_issue_event.rb +4 -4
  13. data/lib/closeyourit/events/slow_method_event.rb +6 -6
  14. data/lib/closeyourit/events/slow_query_event.rb +4 -4
  15. data/lib/closeyourit/instrumenter.rb +6 -6
  16. data/lib/closeyourit/line_cache.rb +4 -4
  17. data/lib/closeyourit/log_buffer.rb +10 -10
  18. data/lib/closeyourit/log_device.rb +18 -18
  19. data/lib/closeyourit/monitor.rb +2 -2
  20. data/lib/closeyourit/performance/request_profile.rb +5 -5
  21. data/lib/closeyourit/performance/rollup.rb +3 -3
  22. data/lib/closeyourit/rails/active_job_extension.rb +33 -31
  23. data/lib/closeyourit/rails/capture_exceptions.rb +3 -3
  24. data/lib/closeyourit/rails/error_subscriber.rb +4 -4
  25. data/lib/closeyourit/rails/log_broadcast.rb +12 -12
  26. data/lib/closeyourit/rails/net_http_patch.rb +22 -22
  27. data/lib/closeyourit/rails/query_source.rb +3 -3
  28. data/lib/closeyourit/rails/railtie.rb +32 -52
  29. data/lib/closeyourit/rails/request_body.rb +7 -7
  30. data/lib/closeyourit/rails/request_context.rb +27 -27
  31. data/lib/closeyourit/scope.rb +29 -29
  32. data/lib/closeyourit/scrubber.rb +47 -48
  33. data/lib/closeyourit/sidekiq/error_handler.rb +2 -2
  34. data/lib/closeyourit/sidekiq/job_metrics_middleware.rb +10 -11
  35. data/lib/closeyourit/stats.rb +8 -8
  36. data/lib/closeyourit/subscribers/job_performance.rb +30 -19
  37. data/lib/closeyourit/subscribers/request_performance.rb +4 -4
  38. data/lib/closeyourit/subscribers/slow_query.rb +42 -20
  39. data/lib/closeyourit/trace_context.rb +22 -22
  40. data/lib/closeyourit/transport.rb +22 -22
  41. data/lib/closeyourit/usage_registry.rb +17 -16
  42. data/lib/closeyourit/version.rb +1 -1
  43. data/lib/closeyourit-ruby.rb +125 -125
  44. metadata +1 -1
@@ -5,10 +5,10 @@ require_relative "../performance/rollup"
5
5
 
6
6
  module CloseYourIt
7
7
  module Subscribers
8
- # A fine richiesta (process_action.action_controller) trasforma il RequestProfile accumulato nello
9
- # Scope in verdetti performance_issue e li spedisce (fire-and-forget). Lo Scope — e quindi il
10
- # profilo — viene azzerato subito dopo da RequestContext#call (ensure). Logica pura: il wiring ad
11
- # ActiveSupport::Notifications vive nel Railtie.
8
+ # At the end of the request (process_action.action_controller) turns the RequestProfile collected
9
+ # in the Scope into performance_issue verdicts and sends them (fire-and-forget). The Scope — and so
10
+ # the profile — is reset right after by RequestContext#call (ensure). Pure logic: the wiring to
11
+ # ActiveSupport::Notifications lives in the Railtie.
12
12
  class RequestPerformance
13
13
  def initialize(configuration = nil)
14
14
  @configuration = configuration
@@ -6,9 +6,9 @@ require_relative "../scope"
6
6
 
7
7
  module CloseYourIt
8
8
  module Subscribers
9
- # Riceve i dati di un evento `sql.active_record` e, se la query supera la soglia
10
- # (escludendo SCHEMA/CACHE/TRANSACTION), invia un evento `slow_query`.
11
- # Logica pura: il wiring ad ActiveSupport::Notifications vive nel Railtie.
9
+ # Receives the data of a `sql.active_record` event and, if the query exceeds the threshold
10
+ # (excluding SCHEMA/CACHE/TRANSACTION), sends a `slow_query` event.
11
+ # Pure logic: the wiring to ActiveSupport::Notifications lives in the Railtie.
12
12
  class SlowQuery
13
13
  IGNORED_NAMES = %w[SCHEMA CACHE TRANSACTION].freeze
14
14
 
@@ -16,6 +16,26 @@ module CloseYourIt
16
16
  @configuration = configuration
17
17
  end
18
18
 
19
+ # Entry point of the Railtie's sql.active_record block. `source` is a callable: the call site costs
20
+ # a full backtrace plus the Rails cleaner, so it is resolved at most once and only when needed.
21
+ def notify(payload, duration_ms, source)
22
+ resolved = false
23
+ site = nil
24
+ lazy_source = lambda do
25
+ site = source.call unless resolved
26
+ resolved = true
27
+ site
28
+ end
29
+ name = payload[:name]
30
+ sql = payload[:sql]
31
+ cached = payload.fetch(:cached, false)
32
+ record(name: name, duration_ms: duration_ms, sql: sql, cached: cached,
33
+ connection: payload[:connection], binds: payload[:binds],
34
+ type_casted_binds: payload[:type_casted_binds], source: lazy_source)
35
+ breadcrumb(name: name, sql: sql, duration_ms: duration_ms, cached: cached)
36
+ profile(name: name, sql: sql, duration_ms: duration_ms, cached: cached, source: lazy_source)
37
+ end
38
+
19
39
  def record(name:, duration_ms:, sql:, cached: false, connection: nil,
20
40
  binds: nil, type_casted_binds: nil, source: nil)
21
41
  config = @configuration || CloseYourIt.configuration
@@ -25,18 +45,18 @@ module CloseYourIt
25
45
 
26
46
  event = SlowQueryEvent.new(
27
47
  { name: name, sql: sql, cached: cached, connection: connection,
28
- binds: binds, type_casted_binds: type_casted_binds, source: source },
48
+ binds: binds, type_casted_binds: type_casted_binds, source: resolve(source) },
29
49
  duration_ms,
30
50
  config
31
51
  )
32
52
  CloseYourIt.capture_event(event)
33
53
  rescue StandardError
34
- # La telemetria non deve mai disturbare la query ospite.
54
+ # Telemetry must never disturb the host query.
35
55
  nil
36
56
  end
37
57
 
38
- # Breadcrumb per OGNI query non di sistema (non solo lente): SQL offuscato, niente bind.
39
- # Dà la cronologia "quali query prima del crash" allegata all'evento d'errore.
58
+ # Breadcrumb for EVERY non-system query (not only slow ones): obfuscated SQL, no binds.
59
+ # Gives the "which queries before the crash" history attached to the error event.
40
60
  def breadcrumb(name:, sql:, duration_ms:, cached: false)
41
61
  config = @configuration || CloseYourIt.configuration
42
62
  return if ignored_name?(name)
@@ -49,12 +69,12 @@ module CloseYourIt
49
69
  data: { "name" => name, "duration_ms" => duration_ms.to_f.round(2), "cached" => cached }
50
70
  )
51
71
  rescue StandardError
52
- # La telemetria non deve mai disturbare la query ospite.
72
+ # Telemetry must never disturb the host query.
53
73
  nil
54
74
  end
55
75
 
56
- # Spinge OGNI query non di sistema nel RequestProfile dello Scope (per la detection N+1 a fine
57
- # richiesta). Solo se detect_performance_issues: fingerprint = SQL offuscato + call-site.
76
+ # Pushes EVERY non-system query into the Scope's RequestProfile (for N+1 detection at the end of
77
+ # the request). Only with detect_performance_issues: fingerprint = obfuscated SQL + call site.
58
78
  def profile(name:, sql:, duration_ms:, cached: false, source: nil)
59
79
  config = @configuration || CloseYourIt.configuration
60
80
  return unless config.detect_performance_issues
@@ -62,15 +82,17 @@ module CloseYourIt
62
82
 
63
83
  CloseYourIt::Scope.current.performance_profile.add_query(
64
84
  fingerprint: scrubber(config).obfuscate_sql(sql),
65
- source: source, duration_ms: duration_ms, cached: cached
85
+ source: resolve(source), duration_ms: duration_ms, cached: cached
66
86
  )
67
87
  rescue StandardError
68
- # La telemetria non deve mai disturbare la query ospite.
88
+ # Telemetry must never disturb the host query.
69
89
  nil
70
90
  end
71
91
 
72
92
  private
73
93
 
94
+ def resolve(source) = source.respond_to?(:call) ? source.call : source
95
+
74
96
  def scrubber(config)
75
97
  @scrubber ||= Scrubber.new(config)
76
98
  end
@@ -79,16 +101,16 @@ module CloseYourIt
79
101
  name.nil? || IGNORED_NAMES.include?(name)
80
102
  end
81
103
 
82
- # Query esclusa dalla MISURA dei rallentamenti (config.excluded_query_patterns). Il filtro sta
83
- # solo qui, non in #breadcrumb né in #profile:
104
+ # Query excluded from slowdown MEASUREMENT (config.excluded_query_patterns). The filter lives
105
+ # only here, not in #breadcrumb nor in #profile:
84
106
  #
85
- # - breadcrumb: la cronologia "quali query prima del crash" ha valore diagnostico anche quando
86
- # la query è del framework — nasconderla renderebbe la sequenza incompleta e bugiarda;
87
- # - profile: è per-richiesta e serve la detection N+1, dove una tabella di servizio letta molte
88
- # volte è essa stessa un sintomo da vedere.
107
+ # - breadcrumb: the "which queries before the crash" history has diagnostic value even when the
108
+ # query belongs to the framework — hiding it would make the sequence incomplete and misleading;
109
+ # - profile: it is per-request and serves N+1 detection, where a service table read many times
110
+ # is itself a symptom worth seeing.
89
111
  #
90
- # Un rallentamento va misurato se qualcuno può intervenire; una breadcrumb va tenuta se aiuta a
91
- # capire. Sono due domande diverse, e questa lista risponde solo alla prima.
112
+ # A slowdown is worth measuring if someone can act on it; a breadcrumb is worth keeping if it
113
+ # helps understanding. They are two different questions, and this list answers only the first.
92
114
  def excluded_sql?(config, sql)
93
115
  return false if sql.nil?
94
116
 
@@ -3,14 +3,14 @@
3
3
  require "securerandom"
4
4
 
5
5
  module CloseYourIt
6
- # Contesto di trace W3C (`traceparent`/`tracestate`, https://www.w3.org/TR/trace-context/).
7
- # NON è un tracer: non apre span né misura tempi. È un "propagation bridge" minimale (il ticket:
8
- # "Non costruire tracing custom completo") — adotta un contesto entrante valido facendo pass-through
9
- # di trace-id/parent-id/flag, oppure ne genera uno root, e sa serializzarsi negli header d'uscita.
10
- # Volutamente separato dai formati proprietari: sullo standard, senza dipendere da un vendor.
6
+ # W3C trace context (`traceparent`/`tracestate`, https://www.w3.org/TR/trace-context/).
7
+ # It is NOT a tracer: it opens no spans and measures no times. It is a minimal "propagation bridge"
8
+ # (the ticket: "do not build full custom tracing") — it adopts a valid incoming context passing
9
+ # through trace-id/parent-id/flags, or generates a root one, and serializes into outgoing headers.
10
+ # Deliberately separate from proprietary formats: standard-based, with no vendor dependency.
11
11
  class TraceContext
12
- # traceparent = version "-" trace-id "-" parent-id "-" trace-flags (55 char per la versione 00).
13
- # `rest` cattura eventuali campi futuri: ammessi solo da versioni > 00 (forward-compat), vietati su 00.
12
+ # traceparent = version "-" trace-id "-" parent-id "-" trace-flags (55 chars for version 00).
13
+ # `rest` captures any future fields: allowed only for versions > 00 (forward-compat), forbidden on 00.
14
14
  TRACEPARENT = /
15
15
  \A
16
16
  (?<version>[0-9a-f]{2})-
@@ -27,10 +27,10 @@ module CloseYourIt
27
27
  ZERO_PARENT_ID = ("0" * 16).freeze
28
28
  FLAG_SAMPLED = 0x01
29
29
 
30
- # tracestate: lista di membri `key=value` separati da virgola, max 32 (W3C §3.3.1).
30
+ # tracestate: comma-separated list of `key=value` members, max 32 (W3C §3.3.1).
31
31
  TRACESTATE_MAX_MEMBERS = 32
32
- # key: lowercase alnum iniziale + set ristretto (incluso `@`/`/` per le chiavi tenant@vendor).
33
- # value: caratteri stampabili 0x20–0x7E esclusi `,` (0x2C) e `=` (0x3D).
32
+ # key: leading lowercase alnum + restricted set (including `@`/`/` for tenant@vendor keys).
33
+ # value: printable characters 0x20–0x7E except `,` (0x2C) and `=` (0x3D).
34
34
  TRACESTATE_MEMBER = %r{\A[a-z0-9][a-z0-9_\-*/@]*=[\x20-\x2b\x2d-\x3c\x3e-\x7e]+\z}
35
35
 
36
36
  attr_reader :trace_id, :parent_id, :flags, :tracestate
@@ -43,9 +43,9 @@ module CloseYourIt
43
43
  end
44
44
 
45
45
  class << self
46
- # Adotta un traceparent entrante. Ritorna nil se malformato (→ il chiamante genera un root o
47
- # lascia il contesto assente). Pass-through: mantiene trace-id/parent-id/flag verbatim così il
48
- # bridge non inventa span. Il tracestate viene sanificato (membri invalidi scartati, cap a 32).
46
+ # Adopts an incoming traceparent. Returns nil when malformed (→ the caller generates a root or
47
+ # leaves the context absent). Pass-through: keeps trace-id/parent-id/flags verbatim so the
48
+ # bridge invents no spans. The tracestate is sanitized (invalid members dropped, capped at 32).
49
49
  def parse(traceparent, tracestate = nil)
50
50
  match = TRACEPARENT.match(traceparent.to_s.strip)
51
51
  return nil unless match
@@ -62,9 +62,9 @@ module CloseYourIt
62
62
  )
63
63
  end
64
64
 
65
- # Nuovo contesto root (nessun traceparent entrante valido). Genera trace-id (16 byte) e parent-id
66
- # (8 byte) casuali — SecureRandom è fork-safe, così un worker forkato non riusa gli id del padre.
67
- # `sampled` fissa il flag: un root che apriamo noi traccia di default.
65
+ # New root context (no valid incoming traceparent). Generates random trace-id (16 bytes) and
66
+ # parent-id (8 bytes) — SecureRandom is fork-safe, so a forked worker does not reuse the parent's
67
+ # ids. `sampled` sets the flag: a root we open traces by default.
68
68
  def generate(sampled: true)
69
69
  new(
70
70
  trace_id: SecureRandom.hex(16),
@@ -76,8 +76,8 @@ module CloseYourIt
76
76
 
77
77
  private
78
78
 
79
- # Trattiene solo i membri ben formati, nell'ordine originale, fino a 32. Ritorna nil se non ne
80
- # resta nessuno → così non propaghiamo mai un tracestate spazzatura o sovradimensionato.
79
+ # Keeps only well-formed members, in the original order, up to 32. Returns nil if none are left
80
+ # → so we never propagate a garbage or oversized tracestate.
81
81
  def sanitize_tracestate(tracestate)
82
82
  return nil if tracestate.nil?
83
83
 
@@ -92,14 +92,14 @@ module CloseYourIt
92
92
  (flags & FLAG_SAMPLED) != 0
93
93
  end
94
94
 
95
- # traceparent d'uscita, sempre versione 00 (l'unica che sappiamo emettere). I flag sono ri-emessi
96
- # per intero (i bit riservati vanno propagati as-is), formattati su due cifre esadecimali.
95
+ # Outgoing traceparent, always version 00 (the only one we can emit). Flags are re-emitted in full
96
+ # (reserved bits must be propagated as-is), formatted as two hex digits.
97
97
  def traceparent
98
98
  format("%s-%s-%s-%02x", CURRENT_VERSION, trace_id, parent_id, flags & 0xff)
99
99
  end
100
100
 
101
- # Header di propagazione W3C: traceparent (+ tracestate se presente). MAI `baggage`: può contenere
102
- # contesto interno/PII e non deve varcare il confine (ticket: "baggage sensibile non riceve header").
101
+ # W3C propagation headers: traceparent (+ tracestate if present). NEVER `baggage`: it may carry
102
+ # internal context/PII and must not cross the boundary (ticket: "sensitive baggage gets no header").
103
103
  def headers
104
104
  result = { "traceparent" => traceparent }
105
105
  result["tracestate"] = tracestate if tracestate && !tracestate.empty?
@@ -6,22 +6,22 @@ require "uri"
6
6
  require "timeout"
7
7
 
8
8
  module CloseYourIt
9
- # Spedisce un payload a un path di ingest (errori → /events, metriche → /metrics) via HTTP POST
10
- # con `Authorization: Bearer`. Mai solleva: ogni errore di rete è loggato e ingoiato.
9
+ # Sends a payload to an ingest path (errors → /events, metrics → /metrics) via HTTP POST
10
+ # with `Authorization: Bearer`. Never raises: every network error is logged and swallowed.
11
11
  class Transport
12
12
  OPEN_TIMEOUT = 2
13
13
  READ_TIMEOUT = 3
14
- # Net::HTTP non segue i redirect: l'host canonico può rispondere 301 (es. apex → www).
15
- # Ri-POSTiamo a Location preservando metodo + body, così l'evento non si perde in silenzio.
14
+ # Net::HTTP does not follow redirects: the canonical host may answer 301 (e.g. apex → www).
15
+ # We re-POST to Location keeping method + body, so the event is not silently lost.
16
16
  MAX_REDIRECTS = 2
17
17
 
18
- # Durata massima di UNA send_event: apertura + lettura, ripetute a ogni redirect seguito. È il
19
- # tetto che il drain di fine vita deve poter aspettare, altrimenti abbandona un invio ancora sano
18
+ # Maximum duration of ONE send_event: open + read, repeated for every followed redirect. It is the
19
+ # cap the end-of-life drain must be able to wait for, otherwise it abandons a still healthy send
20
20
  # (CYRB-24).
21
21
  MAX_REQUEST_SECONDS = (OPEN_TIMEOUT + READ_TIMEOUT) * (MAX_REDIRECTS + 1)
22
22
 
23
- # Un timeout di rete (apertura o lettura) è un fallimento d'invio speciale: lo isoliamo dai non-2xx
24
- # e dagli altri errori di rete perché segnala tipicamente problemi di connettività (CYRB-12).
23
+ # A network timeout (open or read) is a special send failure: we keep it apart from non-2xx and
24
+ # other network errors because it typically signals connectivity problems (CYRB-12).
25
25
  TIMEOUT_ERRORS = [ Net::OpenTimeout, Net::ReadTimeout, Timeout::Error ].freeze
26
26
 
27
27
  def initialize(configuration)
@@ -35,7 +35,7 @@ module CloseYourIt
35
35
  CloseYourIt.notify_diagnostic(:send, status: response.code.to_i)
36
36
  else
37
37
  CloseYourIt.stats.increment(:failed)
38
- CloseYourIt.internal_logger.warn("CloseYourIt transport: HTTP #{response.code}#{error_detail(response)} su #{path}")
38
+ CloseYourIt.internal_logger.warn("CloseYourIt transport: HTTP #{response.code}#{error_detail(response)} on #{path}")
39
39
  CloseYourIt.notify_diagnostic(:drop, reason: :response, status: response.code.to_i)
40
40
  end
41
41
  response
@@ -80,8 +80,8 @@ module CloseYourIt
80
80
  http.read_timeout = READ_TIMEOUT
81
81
 
82
82
  request = Net::HTTP::Post.new(uri.request_uri)
83
- # Il Bearer è un segreto d'ingest: non ri-inviarlo dopo un redirect verso un host diverso
84
- # (o un downgrade https→http), per non consegnarlo a un endpoint non fidato.
83
+ # The Bearer is an ingest secret: do not resend it after a redirect to a different host
84
+ # (or an https→http downgrade), so it is never handed to an untrusted endpoint.
85
85
  request["Authorization"] = "Bearer #{@configuration.token}" if authorize
86
86
  request["Content-Type"] = "application/json"
87
87
  request["User-Agent"] = "closeyourit-ruby/#{VERSION}"
@@ -90,16 +90,16 @@ module CloseYourIt
90
90
  http.request(request)
91
91
  end
92
92
 
93
- # Location può essere assoluto (https://www.…) o relativo (/api/…): risolvilo sull'URI corrente.
93
+ # Location may be absolute (https://www.…) or relative (/api/…): resolve it against the current URI.
94
94
  def redirect_uri(current, location)
95
95
  target = URI.parse(location)
96
96
  target.relative? ? current + target : target
97
97
  end
98
98
 
99
- # Il Bearer va ri-inviato solo se il redirect resta sull'autorità dell'endpoint configurato:
100
- # stesso host (o la sua variante www), stessa porta effettiva e senza downgrade https→http.
101
- # La porta fa parte del confine di fiducia (CYRB-22): sullo stesso server una porta diversa è
102
- # un servizio distinto, che non deve ricevere il segreto d'ingest.
99
+ # The Bearer is resent only if the redirect stays on the configured endpoint's authority:
100
+ # same host (or its www variant), same effective port and no https→http downgrade.
101
+ # The port is part of the trust boundary (CYRB-22): on the same server a different port is a
102
+ # separate service, which must not receive the ingest secret.
103
103
  def same_authority?(origin, target)
104
104
  return false if origin.scheme == "https" && target.scheme != "https"
105
105
  return false unless canonical_host(origin) == canonical_host(target)
@@ -107,21 +107,21 @@ module CloseYourIt
107
107
  origin.port == target.port || canonical_upgrade?(origin, target)
108
108
  end
109
109
 
110
- # Un endpoint configurato in `http` che redirige a `https` resta fidato: la porta cambia solo
111
- # perché cambia lo schema (80 → 443), non perché punti a un servizio su porta non standard.
110
+ # An endpoint configured as `http` that redirects to `https` stays trusted: the port changes only
111
+ # because the scheme changes (80 → 443), not because it points to a service on a non-standard port.
112
112
  def canonical_upgrade?(origin, target)
113
113
  origin.scheme == "http" && target.scheme == "https" &&
114
114
  origin.port == origin.default_port && target.port == target.default_port
115
115
  end
116
116
 
117
- # Una porta esplicita pari alla default (`:443` su https) equivale a ometterla: `URI#port` la
118
- # normalizza già, qui resta solo da neutralizzare la variante www dell'host canonico.
117
+ # An explicit port equal to the default (`:443` on https) is the same as omitting it: `URI#port`
118
+ # already normalizes it, only the www variant of the canonical host is left to neutralize here.
119
119
  def canonical_host(uri)
120
120
  uri.host.to_s.sub(/\Awww\./, "")
121
121
  end
122
122
 
123
- # Estrae " R…: messaggio" dall'envelope d'errore del backend, così il dev vede il codice d'errore
124
- # nel log diagnostico (non solo lo status HTTP). Silenzioso se il body non è l'envelope atteso.
123
+ # Extracts " R…: message" from the backend error envelope, so the dev sees the error code in the
124
+ # diagnostic log (not only the HTTP status). Silent if the body is not the expected envelope.
125
125
  def error_detail(response)
126
126
  body = JSON.parse(response.body.to_s)
127
127
  error = body["error"]
@@ -3,25 +3,26 @@
3
3
  require "concurrent"
4
4
 
5
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.
6
+ # CYSK-29 — usage telemetry: records WHICH symbols actually run (routes as `Controller#action`,
7
+ # jobs, literal custom keys) and flushes them in one POST every `usage_flush_interval` seconds per
8
+ # process. On the hot path: one lookup and one increment.
9
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.
10
+ # The rules that make the system immune to losses:
11
+ # - counts are INDICATIVE, only `last_seen_at` is load-bearing: a lost flush costs one window on a
12
+ # symbol that shows up again right after;
13
+ # - the registry is EMPTIED on every flush: the cap (`usage_max_symbols`) limits the distinct
14
+ # symbols seen in one window — a quantity tied to traffic, not to code size;
15
+ # - beyond the cap the flush carries `truncated: true`, and on the scanner side that flag
16
+ # DISQUALIFIES the kind;
17
+ # - no sampling, ever: sampling a route called 3 times a month fabricates exactly the false
18
+ # "never seen" the system exists to avoid;
19
+ # - `route` is `Controller#action`, NEVER the URL: no path, parameters, user, IP.
19
20
  class UsageRegistry
20
- # I simboli vengono da payload di framework o da stringhe letterali: il pattern è il contratto.
21
+ # Symbols come from framework payloads or literal strings: the pattern is the contract.
21
22
  SYMBOL_FORMAT = %r{\A[A-Za-z0-9_:#./-]{1,200}\z}
22
23
  KINDS = %w[route job custom].freeze
23
24
 
24
- attr_reader :timer # esposto per i test (verifica dell'intervallo configurato)
25
+ attr_reader :timer # exposed for tests (checks the configured interval)
25
26
 
26
27
  def initialize(client:, configuration:)
27
28
  @client = client
@@ -61,8 +62,8 @@ module CloseYourIt
61
62
 
62
63
  @client.flush_usage(**payload)
63
64
  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.
65
+ # The flush runs on the timer thread: an error must not propagate into the app nor kill the
66
+ # TimerTask. The load-bearing semantics is last_seen_at: a lost window falsifies nothing.
66
67
  CloseYourIt.internal_logger.error("CloseYourIt usage registry: #{e.class}: #{e.message}")
67
68
  end
68
69
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloseYourIt
4
- VERSION = "0.10.0"
4
+ VERSION = "0.10.2"
5
5
  end