closeyourit-ruby 0.12.1 → 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 +34 -0
- 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 +10 -0
- data/lib/closeyourit/version.rb +1 -1
- data/lib/closeyourit-ruby.rb +1 -0
- metadata +6 -1
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
|
@@ -401,6 +401,40 @@ ActiveJob dentro Sidekiq è osservato una sola volta quando il subscriber Active
|
|
|
401
401
|
Sampling, disabilitazione o perdita del transport impediscono di considerare questi log un censimento.
|
|
402
402
|
Il raggruppamento backend v1 separa framework, nome, coda e misura; i gruppi legacy restano intatti.
|
|
403
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
|
+
|
|
404
438
|
## Privacy & PII
|
|
405
439
|
|
|
406
440
|
Privacy-by-default (`send_pii = false`). In sintesi:
|
|
@@ -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
|
|
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
|