closeyourit-ruby 0.11.0 → 0.13.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 +4 -4
- data/README.md +51 -9
- data/lib/closeyourit/configuration.rb +9 -1
- data/lib/closeyourit/events/slow_method_event.rb +4 -2
- data/lib/closeyourit/open_dashboard/metric.rb +108 -0
- data/lib/closeyourit/open_dashboard/middleware.rb +45 -0
- data/lib/closeyourit/open_dashboard/report.rb +45 -0
- data/lib/closeyourit/open_dashboard/settings.rb +36 -0
- data/lib/closeyourit/open_dashboard.rb +33 -0
- data/lib/closeyourit/rails/railtie.rb +17 -0
- data/lib/closeyourit/scope.rb +3 -1
- data/lib/closeyourit/version.rb +1 -1
- data/lib/closeyourit-ruby.rb +1 -0
- metadata +9 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c46a549b542395326541c9def86cc019d5cd9620fef9c8f4584c90543ca1b179
|
|
4
|
+
data.tar.gz: 5ac2011a99d7be1d09a751334832154cf1d23765fe8527333adecda9862f50c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 95b4271a533b85cb3d9cf0bf718f08b07a87ce180cd00c03fcc82c30cf24047bc240d1ad572be09bac04402b00ecb728e87250905d28666d443fd22d65111981
|
|
7
|
+
data.tar.gz: 9210728ff21f26c59b49a31007a068f1f3dd334214c2f67f5eb7aa9ce7b418ecdec3f6c1c9cc6b2935e950cded2252ebc417183c3c4518b7d8a4ca130d9a9a44
|
data/README.md
CHANGED
|
@@ -52,7 +52,7 @@ In CloseYourIt, area **Member → Project → tokens**, crea un token: ottieni i
|
|
|
52
52
|
```ruby
|
|
53
53
|
# config/initializers/closeyourit.rb
|
|
54
54
|
CloseYourIt.init do |c|
|
|
55
|
-
c.endpoint_url = ENV["CLOSEYOURIT_ENDPOINT_URL"] # es. https://
|
|
55
|
+
c.endpoint_url = ENV["CLOSEYOURIT_ENDPOINT_URL"] # es. https://app.closeyour.it (host canonico)
|
|
56
56
|
c.token = ENV["CLOSEYOURIT_TOKEN"] # Bearer secret del Projects::Token
|
|
57
57
|
c.project_id = ENV["CLOSEYOURIT_PROJECT_ID"] # UUID del progetto su CloseYourIt
|
|
58
58
|
c.environment = Rails.env
|
|
@@ -62,9 +62,8 @@ end
|
|
|
62
62
|
**Senza `endpoint_url` / `token` / `project_id` la gemma è no-op** (nessun invio, nessun overhead). In
|
|
63
63
|
`production` un `endpoint_url` `http://` viene rifiutato (no-op + warning): il token viaggerebbe in chiaro.
|
|
64
64
|
|
|
65
|
-
> **Endpoint:** usa l'host canonico **`https://
|
|
66
|
-
>
|
|
67
|
-
> direttamente a www risparmia un hop per ogni evento.
|
|
65
|
+
> **Endpoint:** usa l'host canonico **`https://app.closeyour.it`**. Le installazioni già configurate su
|
|
66
|
+
> `https://www.closeyour.it` continuano a funzionare: l'ingest su `www` resta inoltrato all'app.
|
|
68
67
|
|
|
69
68
|
### Opzioni
|
|
70
69
|
|
|
@@ -84,7 +83,9 @@ end
|
|
|
84
83
|
| `slow_query_threshold_ms` | `100` | Soglia query lente |
|
|
85
84
|
| `excluded_query_patterns` | `solid_queue_`, `solid_cache_`, `solid_cable_` | Query da NON misurare come rallentamento — **Regexp** (pattern) o **String** (testo letterale). Vale solo per la misura: breadcrumb e profiling N+1 vedono tutto |
|
|
86
85
|
| `slow_method_threshold_ms` | `200` | Soglia metodi lenti |
|
|
86
|
+
| `capture_method_arguments` | `false` | Allega gli argomenti di `measure`/`monitor` ai `slow_method`. **Attenzione**: gli argomenti sono spesso dati personali; la gemma redige chiavi sensibili, credenziali e `scrub_message_patterns`, non il resto |
|
|
87
87
|
| `capture_request` | `true` | Cattura il contesto HTTP della richiesta (method/url/header allowlist) |
|
|
88
|
+
| `capture_request_body` | `true` | Allega i parametri del body (`request.data`) all'errore, scrubbati. Vale **solo con `send_pii = true`**: con la privacy spenta il body non parte mai |
|
|
88
89
|
| `request_header_allowlist` | `Accept`, `Content-Type`, `User-Agent`, `Referer` | Header inviati (mai Authorization/Cookie) |
|
|
89
90
|
| `breadcrumbs_enabled` | `true` | Cronologia (query offuscate + `add_breadcrumb`) allegata all'errore |
|
|
90
91
|
| `max_breadcrumbs` | `100` | Dimensione max del ring buffer breadcrumbs |
|
|
@@ -92,7 +93,7 @@ end
|
|
|
92
93
|
| `report_active_job_errors` | `true` | Cattura gli errori dei job ActiveJob/Solid Queue/Sidekiq |
|
|
93
94
|
| `send_pii` | `false` | Master switch PII |
|
|
94
95
|
| `obfuscate_sql` | `true` | Maschera i literal nello SQL |
|
|
95
|
-
| `filter_parameters` | `[]` | Chiavi extra da redarre (
|
|
96
|
+
| `filter_parameters` | `[]` | Chiavi extra da redarre, in aggiunta a `Rails.application.config.filter_parameters` (unite automaticamente dalla Railtie) |
|
|
96
97
|
| `scrub_message_patterns` | `[]` | Regexp **aggiuntive** da redarre da messaggi d'eccezione **e da `message` dei log**; le credenziali `Bearer`/`Basic`/`Token` sono già redatte di serie |
|
|
97
98
|
| `logs_enabled` | `true` | Master switch dello stream di log strutturato |
|
|
98
99
|
| `logs_min_level` | `:info` | Soglia minima: i log sotto (es. `debug`) non vengono inviati |
|
|
@@ -155,7 +156,9 @@ end
|
|
|
155
156
|
|
|
156
157
|
Un Rack middleware allega a ogni evento il contesto HTTP della richiesta: **method**, **url** (senza
|
|
157
158
|
query string) e gli **header dell'allowlist** (`request_header_allowlist`, mai Authorization/Cookie).
|
|
158
|
-
Query string e
|
|
159
|
+
Query string, IP e **parametri del body** (`request.data`, scrubbati, estratti solo quando scoppia
|
|
160
|
+
l'errore; `capture_request_body` li spegne) solo con `send_pii`. Lo scope è resettato a fine richiesta
|
|
161
|
+
(niente bleed tra request).
|
|
159
162
|
|
|
160
163
|
### Background job + errori handled (automatico, con Rails)
|
|
161
164
|
|
|
@@ -312,7 +315,10 @@ end
|
|
|
312
315
|
```
|
|
313
316
|
|
|
314
317
|
Viene inviato un `slow_method` solo se la durata supera `slow_method_threshold_ms`. **Gli argomenti
|
|
315
|
-
del metodo non vengono
|
|
318
|
+
del metodo non vengono inviati** (solo label, durata, file:riga) salvo `capture_method_arguments = true`:
|
|
319
|
+
in quel caso partono con `inspect` troncato, redatti per chiave sensibile (kwargs), credenziali
|
|
320
|
+
`Bearer`/`Basic`/`Token` e `scrub_message_patterns`. Tutto il resto arriva in chiaro: accendilo solo
|
|
321
|
+
su metodi che non ricevono dati personali.
|
|
316
322
|
|
|
317
323
|
## Performance issues (opt-in)
|
|
318
324
|
|
|
@@ -395,6 +401,40 @@ ActiveJob dentro Sidekiq è osservato una sola volta quando il subscriber Active
|
|
|
395
401
|
Sampling, disabilitazione o perdita del transport impediscono di considerare questi log un censimento.
|
|
396
402
|
Il raggruppamento backend v1 separa framework, nome, coda e misura; i gruppi legacy restano intatti.
|
|
397
403
|
|
|
404
|
+
## Open Dashboard (opt-in)
|
|
405
|
+
|
|
406
|
+
Il servizio mostra i suoi numeri (utenti, abbonamenti, incassi…) a chi ha la chiave, su
|
|
407
|
+
`GET /open-dashboard`. Lo standard completo: contratto `contracts/open-dashboard/v1`.
|
|
408
|
+
|
|
409
|
+
```ruby
|
|
410
|
+
# config/initializers/open_dashboard.rb
|
|
411
|
+
CloseYourIt::OpenDashboard.configure do |od|
|
|
412
|
+
od.token = ENV["OPEN_DASHBOARD_TOKEN"] # without it the route does not exist
|
|
413
|
+
od.name = "scontino"
|
|
414
|
+
od.app = "scontino-rails"
|
|
415
|
+
od.url = "https://scontino.it"
|
|
416
|
+
od.locale = "it"
|
|
417
|
+
|
|
418
|
+
od.metric "users.registered", label: "Utenti registrati", group: "Utenti", type: :count do |since|
|
|
419
|
+
{ value: User.count, previous: User.where(created_at: ...since).count }
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
od.metric "revenue.mrr", label: "Incasso mensile ricorrente", group: "Incassi",
|
|
423
|
+
type: :money, currency: "EUR" do
|
|
424
|
+
Subscription.active.sum(:amount_cents)
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
- `since` è l'inizio del periodo di confronto (`period:`, default `"30d"`; anche `1d`, `7d`, `90d`, `365d`).
|
|
430
|
+
- Il blocco restituisce un numero, oppure `{ value:, previous: }`.
|
|
431
|
+
- Tipi: `count` e `money` (interi; `money` in centesimi con `currency`), `percent` (0–100), `duration` (secondi).
|
|
432
|
+
- `better: :down` per i numeri che vanno bene quando scendono.
|
|
433
|
+
- Una metrica che solleva arriva come «non disponibile» e l'errore finisce in CloseYourIt; le altre restano.
|
|
434
|
+
- I blocchi girano a ogni richiesta e chi legge aspetta al massimo 10 secondi: i numeri lenti vanno precalcolati.
|
|
435
|
+
- Una dichiarazione sbagliata (chiave doppia, tipo sconosciuto, `money` senza valuta) blocca l'avvio.
|
|
436
|
+
- Solo totali: mai dati di singole persone.
|
|
437
|
+
|
|
398
438
|
## Privacy & PII
|
|
399
439
|
|
|
400
440
|
Privacy-by-default (`send_pii = false`). In sintesi:
|
|
@@ -406,8 +446,10 @@ Privacy-by-default (`send_pii = false`). In sintesi:
|
|
|
406
446
|
(`Authorization: Bearer …`, `Basic …`, `Token …`) sono redatte **di serie**, senza configurazione:
|
|
407
447
|
chiave e schema restano leggibili, sparisce il valore → `Authorization: Bearer [FILTERED]`. Il resto
|
|
408
448
|
è redigibile con `scrub_message_patterns` / `before_send`.
|
|
409
|
-
- **
|
|
410
|
-
|
|
449
|
+
- **Solo con `send_pii`**: query string, IP, email/username dell'utente e parametri del body
|
|
450
|
+
(`capture_request_body`). Con la privacy spenta non partono, qualunque sia il resto della configurazione.
|
|
451
|
+
- **Mai inviati**: variabili locali dei frame, cookie/Authorization, token (che viaggia solo nell'header
|
|
452
|
+
su HTTPS). Gli argomenti dei metodi solo con `capture_method_arguments` (opt-in, vedi sopra).
|
|
411
453
|
|
|
412
454
|
> Rischio residuo: `exception.value` e i nomi tabella/colonna nello SQL possono contenere dati di
|
|
413
455
|
> dominio. Usa `before_send`/`scrub_message_patterns` per azzerarli. Dettaglio in [`PDR.md` §9](PDR.md).
|
|
@@ -94,7 +94,8 @@ module CloseYourIt
|
|
|
94
94
|
|
|
95
95
|
# Request body params in the event (`request.data`), extracted LAZILY only when the error
|
|
96
96
|
# happens, sanitized and scrubbed (denylist + filter_parameters). The backend scrubs again
|
|
97
|
-
# defensively. Uploads → placeholder, 64 KB cap.
|
|
97
|
+
# defensively. Uploads → placeholder, 64 KB cap. Gated by send_pii: with it OFF the body
|
|
98
|
+
# is never sent, whatever this flag says (CYRB-28).
|
|
98
99
|
@capture_request_body = true
|
|
99
100
|
|
|
100
101
|
# Breadcrumbs: history (obfuscated queries, custom events) attached to the error.
|
|
@@ -200,6 +201,13 @@ module CloseYourIt
|
|
|
200
201
|
@filter_parameters = Array(list)
|
|
201
202
|
end
|
|
202
203
|
|
|
204
|
+
# Adds the host app's `Rails.application.config.filter_parameters` (strings/symbols/regexps) to the
|
|
205
|
+
# gem list, without duplicates. Rails filter procs are dropped: they mutate values in place, while
|
|
206
|
+
# the Scrubber matches keys only (CYRB-28).
|
|
207
|
+
def merge_filter_parameters(list)
|
|
208
|
+
@filter_parameters |= Array(list).reject { |item| item.respond_to?(:call) }
|
|
209
|
+
end
|
|
210
|
+
|
|
203
211
|
# Destinations allowed to receive the W3C trace headers. String = exact host (case-insensitive
|
|
204
212
|
# match), Regexp = pattern (for subdomains/host families). Empty list = none.
|
|
205
213
|
def trace_propagation_allowlist=(list)
|
|
@@ -42,7 +42,9 @@ module CloseYourIt
|
|
|
42
42
|
private
|
|
43
43
|
|
|
44
44
|
# Arguments — ONLY with capture_method_arguments (opt-in). Positional by index; kwargs by name with
|
|
45
|
-
# sensitive key scrubbing (denylist password/token/…). Value = truncated inspect (JSON-safe)
|
|
45
|
+
# sensitive key scrubbing (denylist password/token/…). Value = truncated inspect (JSON-safe),
|
|
46
|
+
# passed through the message scrubber (auth schemes + scrub_message_patterns) like exception
|
|
47
|
+
# messages, so a token inside a positional argument never leaves the app (CYRB-28).
|
|
46
48
|
def arguments
|
|
47
49
|
return nil unless @configuration.capture_method_arguments
|
|
48
50
|
|
|
@@ -54,7 +56,7 @@ module CloseYourIt
|
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
def safe_value(value)
|
|
57
|
-
inspected = value.inspect.to_s
|
|
59
|
+
inspected = @scrubber.scrub_message(value.inspect.to_s)
|
|
58
60
|
inspected.length > 120 ? "#{inspected[0, 117]}…" : inspected
|
|
59
61
|
end
|
|
60
62
|
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloseYourIt
|
|
4
|
+
module OpenDashboard
|
|
5
|
+
# Raised at boot when the service declares a metric the standard does not allow.
|
|
6
|
+
class ConfigurationError < StandardError; end
|
|
7
|
+
|
|
8
|
+
# One metric of the Open Dashboard standard. Rules are checked when the service boots, so a bad
|
|
9
|
+
# declaration fails the deploy instead of producing an invalid response in production.
|
|
10
|
+
class Metric
|
|
11
|
+
TYPES = %w[count money percent duration].freeze
|
|
12
|
+
INTEGER_TYPES = %w[count money].freeze
|
|
13
|
+
PERIODS = { "1d" => 1, "7d" => 7, "30d" => 30, "90d" => 90, "365d" => 365 }.freeze
|
|
14
|
+
BETTER = %w[up down].freeze
|
|
15
|
+
KEY_FORMAT = /\A[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+\z/
|
|
16
|
+
CURRENCY_FORMAT = /\A[A-Z]{3}\z/
|
|
17
|
+
UNAVAILABLE = "unavailable"
|
|
18
|
+
SECONDS_PER_DAY = 86_400
|
|
19
|
+
|
|
20
|
+
attr_reader :key
|
|
21
|
+
|
|
22
|
+
def initialize(key, label:, group:, type:, currency: nil, period: "30d", better: nil, &block)
|
|
23
|
+
@key = key.to_s
|
|
24
|
+
@label = label.to_s
|
|
25
|
+
@group = group.to_s
|
|
26
|
+
@type = type.to_s
|
|
27
|
+
@currency = currency
|
|
28
|
+
@period = period.to_s
|
|
29
|
+
@better = better&.to_s
|
|
30
|
+
@block = block
|
|
31
|
+
validate!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The block gets the start of the comparison period. A failure never breaks the other metrics:
|
|
35
|
+
# it is reported to CloseYourIt and the metric goes out as unavailable.
|
|
36
|
+
def to_h(now)
|
|
37
|
+
since = now - (PERIODS.fetch(@period) * SECONDS_PER_DAY)
|
|
38
|
+
identity.merge(values(@block.call(since)))
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
CloseYourIt.capture_exception(e, handled: true)
|
|
41
|
+
identity.merge(value: nil, error: UNAVAILABLE)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def identity
|
|
47
|
+
{ key: @key, label: @label, group: @group, type: @type, currency: @currency, better: @better }.compact
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def values(result)
|
|
51
|
+
return { value: number(result) } unless result.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
out = { value: number(result.fetch(:value)) }
|
|
54
|
+
out.merge!(previous: number(result[:previous]), period: @period) unless result[:previous].nil?
|
|
55
|
+
out
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns the value to emit: an Integer for count/money, a Float for percent/duration.
|
|
59
|
+
def number(value)
|
|
60
|
+
reject("value must be numeric") unless value.is_a?(Numeric)
|
|
61
|
+
reject("value must be finite") unless !value.respond_to?(:finite?) || value.finite?
|
|
62
|
+
|
|
63
|
+
normalised = INTEGER_TYPES.include?(@type) ? whole(value) : Float(value)
|
|
64
|
+
check_range(normalised)
|
|
65
|
+
normalised
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def whole(value)
|
|
69
|
+
reject("#{@type} must be an integer") unless value == value.to_i
|
|
70
|
+
|
|
71
|
+
value.to_i
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def check_range(value)
|
|
75
|
+
reject("count must not be negative") if @type == "count" && value.negative?
|
|
76
|
+
reject("percent must be between 0 and 100") if @type == "percent" && !value.between?(0, 100)
|
|
77
|
+
reject("duration must not be negative") if @type == "duration" && value.negative?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def reject(message)
|
|
81
|
+
raise TypeError, "open_dashboard metric #{@key}: #{message}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def validate!
|
|
85
|
+
fail_with("key must be lowercase and dotted, e.g. users.registered") unless KEY_FORMAT.match?(@key)
|
|
86
|
+
fail_with("label is blank") if @label.strip.empty?
|
|
87
|
+
fail_with("group is blank") if @group.strip.empty?
|
|
88
|
+
fail_with("type must be one of #{TYPES.join(', ')}") unless TYPES.include?(@type)
|
|
89
|
+
validate_currency!
|
|
90
|
+
fail_with("period must be one of #{PERIODS.keys.join(', ')}") unless PERIODS.key?(@period)
|
|
91
|
+
fail_with("better must be up or down") if @better && !BETTER.include?(@better)
|
|
92
|
+
fail_with("a block computing the value is required") unless @block
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def validate_currency!
|
|
96
|
+
if @type == "money"
|
|
97
|
+
fail_with("money needs an ISO 4217 currency, e.g. EUR") unless CURRENCY_FORMAT.match?(@currency.to_s)
|
|
98
|
+
elsif @currency
|
|
99
|
+
fail_with("currency is allowed only on money")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def fail_with(message)
|
|
104
|
+
raise ConfigurationError, "open_dashboard metric #{@key}: #{message}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
module CloseYourIt
|
|
8
|
+
module OpenDashboard
|
|
9
|
+
# Pure Rack: answers GET /open-dashboard and passes everything else to the app. Without a token
|
|
10
|
+
# the request also goes to the app, so the route simply does not exist (404 from the app).
|
|
11
|
+
class Middleware
|
|
12
|
+
JSON_HEADERS = { "content-type" => "application/json", "cache-control" => "no-store" }.freeze
|
|
13
|
+
BEARER = "Bearer "
|
|
14
|
+
|
|
15
|
+
def initialize(app)
|
|
16
|
+
@app = app
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(env)
|
|
20
|
+
settings = OpenDashboard.settings
|
|
21
|
+
return @app.call(env) unless dashboard_request?(env) && settings&.enabled?
|
|
22
|
+
return [ 401, { "cache-control" => "no-store" }, [] ] unless authorized?(env["HTTP_AUTHORIZATION"], settings.token)
|
|
23
|
+
|
|
24
|
+
[ 200, JSON_HEADERS.dup, [ JSON.generate(Report.new(settings).to_h) ] ]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def dashboard_request?(env)
|
|
30
|
+
env["REQUEST_METHOD"] == "GET" && env["PATH_INFO"] == PATH
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Hashing both sides gives equal lengths, so the comparison time does not leak the token length.
|
|
34
|
+
def authorized?(header, token)
|
|
35
|
+
header = header.to_s
|
|
36
|
+
return false unless header.start_with?(BEARER)
|
|
37
|
+
|
|
38
|
+
OpenSSL.fixed_length_secure_compare(
|
|
39
|
+
Digest::SHA256.digest(header.delete_prefix(BEARER)),
|
|
40
|
+
Digest::SHA256.digest(token.to_s)
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module CloseYourIt
|
|
6
|
+
module OpenDashboard
|
|
7
|
+
# Builds the response body. Service identity comes from the same ENV as GET /version, so the
|
|
8
|
+
# dashboard and the deploy smoke test always agree on which build is live.
|
|
9
|
+
class Report
|
|
10
|
+
UNKNOWN = "unknown"
|
|
11
|
+
|
|
12
|
+
def initialize(settings, now: Time.now.utc)
|
|
13
|
+
@settings = settings
|
|
14
|
+
@now = now
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
{
|
|
19
|
+
open_dashboard: SPEC_VERSION,
|
|
20
|
+
service: service,
|
|
21
|
+
locale: @settings.locale,
|
|
22
|
+
generated_at: @now.getutc.iso8601,
|
|
23
|
+
metrics: @settings.metrics.map { |metric| metric.to_h(@now) }
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def service
|
|
30
|
+
{
|
|
31
|
+
name: @settings.name,
|
|
32
|
+
app: @settings.app,
|
|
33
|
+
version: present(ENV["APP_GIT_TAG"]) || UNKNOWN,
|
|
34
|
+
sha: present(ENV["APP_GIT_SHA"])&.slice(0, 7) || UNKNOWN,
|
|
35
|
+
environment: CloseYourIt.configuration.environment.to_s,
|
|
36
|
+
url: present(@settings.url)
|
|
37
|
+
}.compact
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def present(value)
|
|
41
|
+
value.to_s.strip.empty? ? nil : value.to_s
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloseYourIt
|
|
4
|
+
module OpenDashboard
|
|
5
|
+
# What the service declares in config/initializers/open_dashboard.rb.
|
|
6
|
+
class Settings
|
|
7
|
+
LOCALE_FORMAT = /\A[a-z]{2}(-[A-Z]{2})?\z/
|
|
8
|
+
|
|
9
|
+
attr_accessor :token, :name, :app, :url, :locale
|
|
10
|
+
attr_reader :metrics
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@metrics = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def metric(key, **options, &block)
|
|
17
|
+
if @metrics.any? { |existing| existing.key == key.to_s }
|
|
18
|
+
raise ConfigurationError, "open_dashboard metric #{key}: duplicated key"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@metrics << Metric.new(key, **options, &block)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# No token means the feature is off: the route does not exist.
|
|
25
|
+
def enabled? = !token.to_s.strip.empty?
|
|
26
|
+
|
|
27
|
+
def validate!
|
|
28
|
+
%i[name app].each do |field|
|
|
29
|
+
raise ConfigurationError, "open_dashboard: #{field} is required" if public_send(field).to_s.strip.empty?
|
|
30
|
+
end
|
|
31
|
+
raise ConfigurationError, "open_dashboard: locale must look like it or en-US" unless LOCALE_FORMAT.match?(locale.to_s)
|
|
32
|
+
raise ConfigurationError, "open_dashboard: at most #{MAX_METRICS} metrics" if @metrics.size > MAX_METRICS
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloseYourIt
|
|
4
|
+
# Open Dashboard standard: GET /open-dashboard returns who the service is and its business
|
|
5
|
+
# numbers, behind a per-service token. Contract: contracts/open-dashboard/v1.
|
|
6
|
+
module OpenDashboard
|
|
7
|
+
SPEC_VERSION = "1.0"
|
|
8
|
+
PATH = "/open-dashboard"
|
|
9
|
+
MAX_METRICS = 50
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# Kept separate from CloseYourIt.init: init rebuilds its configuration on every call, and the
|
|
13
|
+
# dashboard declaration must not disappear when telemetry is reconfigured.
|
|
14
|
+
def configure
|
|
15
|
+
candidate = Settings.new
|
|
16
|
+
yield candidate
|
|
17
|
+
candidate.validate!
|
|
18
|
+
@settings = candidate
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :settings
|
|
22
|
+
|
|
23
|
+
def reset!
|
|
24
|
+
@settings = nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
require_relative "open_dashboard/metric"
|
|
31
|
+
require_relative "open_dashboard/settings"
|
|
32
|
+
require_relative "open_dashboard/report"
|
|
33
|
+
require_relative "open_dashboard/middleware"
|
|
@@ -28,6 +28,16 @@ module CloseYourIt
|
|
|
28
28
|
)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Open Dashboard (GET /open-dashboard). Inside CaptureExceptions, so a crash while building the
|
|
32
|
+
# report is reported like any other request error. Inert until the app calls
|
|
33
|
+
# CloseYourIt::OpenDashboard.configure with a token.
|
|
34
|
+
initializer "closeyourit.open_dashboard" do |app|
|
|
35
|
+
app.config.middleware.insert_after(
|
|
36
|
+
CloseYourIt::Rails::CaptureExceptions,
|
|
37
|
+
CloseYourIt::OpenDashboard::Middleware
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
31
41
|
initializer "closeyourit.subscribe_slow_queries" do
|
|
32
42
|
subscriber = CloseYourIt::Subscribers::SlowQuery.new
|
|
33
43
|
|
|
@@ -100,6 +110,13 @@ module CloseYourIt
|
|
|
100
110
|
end
|
|
101
111
|
end
|
|
102
112
|
|
|
113
|
+
# Keys the app declared sensitive via config.filter_parameters are scrubbed by the gem too.
|
|
114
|
+
# `after: :load_config_initializers`: CloseYourIt.init (config/initializers/closeyourit.rb)
|
|
115
|
+
# replaces the configuration, so an earlier merge would be lost (CYRB-28).
|
|
116
|
+
initializer "closeyourit.merge_rails_filter_parameters", after: :load_config_initializers do |app|
|
|
117
|
+
CloseYourIt.configuration.merge_filter_parameters(app.config.filter_parameters)
|
|
118
|
+
end
|
|
119
|
+
|
|
103
120
|
# Captures Sidekiq job errors + measures duration/wait via server middleware (only when Sidekiq
|
|
104
121
|
# is present). The middleware is effectively a no-op when monitor_jobs is OFF (the decision
|
|
105
122
|
# lives in JobPerformance#record).
|
data/lib/closeyourit/scope.rb
CHANGED
|
@@ -139,8 +139,10 @@ module CloseYourIt
|
|
|
139
139
|
data ? @request.merge("data" => data) : @request
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
+
# Body params are user input (PII): sent only with send_pii AND capture_request_body (CYRB-28).
|
|
142
143
|
def request_body_data
|
|
143
|
-
|
|
144
|
+
config = CloseYourIt.configuration
|
|
145
|
+
return nil unless config.send_pii && config.capture_request_body
|
|
144
146
|
return nil if @rack_env.nil?
|
|
145
147
|
|
|
146
148
|
Rails::RequestBody.extract(@rack_env)
|
data/lib/closeyourit/version.rb
CHANGED
data/lib/closeyourit-ruby.rb
CHANGED
|
@@ -39,6 +39,7 @@ require_relative "closeyourit/rails/active_job_extension"
|
|
|
39
39
|
require_relative "closeyourit/rails/error_subscriber"
|
|
40
40
|
require_relative "closeyourit/sidekiq/error_handler"
|
|
41
41
|
require_relative "closeyourit/sidekiq/job_metrics_middleware"
|
|
42
|
+
require_relative "closeyourit/open_dashboard"
|
|
42
43
|
|
|
43
44
|
# CloseYourIt — telemetry client (errors + slow query/method statistics)
|
|
44
45
|
# that sends events to the CloseYourIt ingest endpoint.
|
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.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alessio Bussolari
|
|
@@ -67,6 +67,11 @@ files:
|
|
|
67
67
|
- lib/closeyourit/log_buffer.rb
|
|
68
68
|
- lib/closeyourit/log_device.rb
|
|
69
69
|
- lib/closeyourit/monitor.rb
|
|
70
|
+
- lib/closeyourit/open_dashboard.rb
|
|
71
|
+
- lib/closeyourit/open_dashboard/metric.rb
|
|
72
|
+
- lib/closeyourit/open_dashboard/middleware.rb
|
|
73
|
+
- lib/closeyourit/open_dashboard/report.rb
|
|
74
|
+
- lib/closeyourit/open_dashboard/settings.rb
|
|
70
75
|
- lib/closeyourit/performance/request_profile.rb
|
|
71
76
|
- lib/closeyourit/performance/rollup.rb
|
|
72
77
|
- lib/closeyourit/rails/active_job_extension.rb
|
|
@@ -90,12 +95,12 @@ files:
|
|
|
90
95
|
- lib/closeyourit/transport.rb
|
|
91
96
|
- lib/closeyourit/usage_registry.rb
|
|
92
97
|
- lib/closeyourit/version.rb
|
|
93
|
-
homepage: https://github.com/bussolabs/closeyourit-ruby
|
|
98
|
+
homepage: https://github.com/bussolabs/closeyourit-ruby-community
|
|
94
99
|
licenses:
|
|
95
100
|
- MIT
|
|
96
101
|
metadata:
|
|
97
|
-
homepage_uri: https://github.com/bussolabs/closeyourit-ruby
|
|
98
|
-
source_code_uri: https://github.com/bussolabs/closeyourit-ruby
|
|
102
|
+
homepage_uri: https://github.com/bussolabs/closeyourit-ruby-community
|
|
103
|
+
source_code_uri: https://github.com/bussolabs/closeyourit-ruby-community
|
|
99
104
|
rdoc_options: []
|
|
100
105
|
require_paths:
|
|
101
106
|
- lib
|