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.
- checksums.yaml +4 -4
- data/lib/closeyourit/background_worker.rb +14 -14
- data/lib/closeyourit/breadcrumb.rb +2 -2
- data/lib/closeyourit/breadcrumb_buffer.rb +2 -2
- data/lib/closeyourit/client.rb +24 -24
- data/lib/closeyourit/configuration.rb +102 -102
- data/lib/closeyourit/event.rb +6 -6
- data/lib/closeyourit/events/error_event.rb +16 -16
- data/lib/closeyourit/events/job_metric_event.rb +8 -8
- data/lib/closeyourit/events/log_event.rb +17 -17
- data/lib/closeyourit/events/message_event.rb +4 -4
- data/lib/closeyourit/events/performance_issue_event.rb +4 -4
- data/lib/closeyourit/events/slow_method_event.rb +6 -6
- data/lib/closeyourit/events/slow_query_event.rb +4 -4
- data/lib/closeyourit/instrumenter.rb +6 -6
- data/lib/closeyourit/line_cache.rb +4 -4
- data/lib/closeyourit/log_buffer.rb +10 -10
- data/lib/closeyourit/log_device.rb +18 -18
- data/lib/closeyourit/monitor.rb +2 -2
- data/lib/closeyourit/performance/request_profile.rb +5 -5
- data/lib/closeyourit/performance/rollup.rb +3 -3
- data/lib/closeyourit/rails/active_job_extension.rb +33 -31
- data/lib/closeyourit/rails/capture_exceptions.rb +3 -3
- data/lib/closeyourit/rails/error_subscriber.rb +4 -4
- data/lib/closeyourit/rails/log_broadcast.rb +12 -12
- data/lib/closeyourit/rails/net_http_patch.rb +22 -22
- data/lib/closeyourit/rails/query_source.rb +3 -3
- data/lib/closeyourit/rails/railtie.rb +32 -52
- data/lib/closeyourit/rails/request_body.rb +7 -7
- data/lib/closeyourit/rails/request_context.rb +27 -27
- data/lib/closeyourit/scope.rb +29 -29
- data/lib/closeyourit/scrubber.rb +47 -48
- data/lib/closeyourit/sidekiq/error_handler.rb +2 -2
- data/lib/closeyourit/sidekiq/job_metrics_middleware.rb +10 -11
- data/lib/closeyourit/stats.rb +8 -8
- data/lib/closeyourit/subscribers/job_performance.rb +30 -19
- data/lib/closeyourit/subscribers/request_performance.rb +4 -4
- data/lib/closeyourit/subscribers/slow_query.rb +42 -20
- data/lib/closeyourit/trace_context.rb +22 -22
- data/lib/closeyourit/transport.rb +22 -22
- data/lib/closeyourit/usage_registry.rb +17 -16
- data/lib/closeyourit/version.rb +1 -1
- data/lib/closeyourit-ruby.rb +125 -125
- metadata +1 -1
|
@@ -5,10 +5,10 @@ require_relative "../performance/rollup"
|
|
|
5
5
|
|
|
6
6
|
module CloseYourIt
|
|
7
7
|
module Subscribers
|
|
8
|
-
#
|
|
9
|
-
# Scope
|
|
10
|
-
#
|
|
11
|
-
# ActiveSupport::Notifications
|
|
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
|
-
#
|
|
10
|
-
# (
|
|
11
|
-
#
|
|
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
|
-
#
|
|
54
|
+
# Telemetry must never disturb the host query.
|
|
35
55
|
nil
|
|
36
56
|
end
|
|
37
57
|
|
|
38
|
-
# Breadcrumb
|
|
39
|
-
#
|
|
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
|
-
#
|
|
72
|
+
# Telemetry must never disturb the host query.
|
|
53
73
|
nil
|
|
54
74
|
end
|
|
55
75
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
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
|
-
#
|
|
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
|
|
83
|
-
#
|
|
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:
|
|
86
|
-
#
|
|
87
|
-
# - profile:
|
|
88
|
-
#
|
|
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
|
-
#
|
|
91
|
-
#
|
|
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
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# "
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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
|
|
13
|
-
# `rest`
|
|
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:
|
|
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
|
|
33
|
-
# value:
|
|
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
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# bridge
|
|
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
|
-
#
|
|
66
|
-
# (8
|
|
67
|
-
# `sampled`
|
|
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
|
-
#
|
|
80
|
-
#
|
|
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
|
|
96
|
-
#
|
|
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
|
-
#
|
|
102
|
-
#
|
|
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
|
-
#
|
|
10
|
-
#
|
|
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
|
|
15
|
-
#
|
|
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
|
-
#
|
|
19
|
-
#
|
|
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
|
-
#
|
|
24
|
-
#
|
|
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)}
|
|
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
|
-
#
|
|
84
|
-
# (
|
|
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
|
|
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
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
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
|
-
#
|
|
111
|
-
#
|
|
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
|
-
#
|
|
118
|
-
#
|
|
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
|
-
#
|
|
124
|
-
#
|
|
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 —
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
-
#
|
|
11
|
-
# -
|
|
12
|
-
#
|
|
13
|
-
# -
|
|
14
|
-
#
|
|
15
|
-
# -
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
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
|
-
#
|
|
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 #
|
|
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
|
-
#
|
|
65
|
-
# TimerTask.
|
|
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
|
|
data/lib/closeyourit/version.rb
CHANGED