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
data/lib/closeyourit/event.rb
CHANGED
|
@@ -4,8 +4,8 @@ require "time"
|
|
|
4
4
|
require "socket"
|
|
5
5
|
|
|
6
6
|
module CloseYourIt
|
|
7
|
-
# Base
|
|
8
|
-
# (
|
|
7
|
+
# Base of telemetry events. Subclasses implement `#to_h` and `#ingest_path`
|
|
8
|
+
# (the API path the event is sent to: errors → /events, metrics → /metrics).
|
|
9
9
|
class Event
|
|
10
10
|
def initialize(configuration)
|
|
11
11
|
@configuration = configuration
|
|
@@ -13,11 +13,11 @@ module CloseYourIt
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def to_h
|
|
16
|
-
raise NotImplementedError, "#{self.class}
|
|
16
|
+
raise NotImplementedError, "#{self.class} must implement #to_h"
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def ingest_path(_project_id)
|
|
20
|
-
raise NotImplementedError, "#{self.class}
|
|
20
|
+
raise NotImplementedError, "#{self.class} must implement #ingest_path"
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
private
|
|
@@ -42,8 +42,8 @@ module CloseYourIt
|
|
|
42
42
|
hash.reject { |_key, value| value.nil? }
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
45
|
+
# Recursive merge: nested Hashes are merged (e.g. `contexts.runtime` kept while the scope adds
|
|
46
|
+
# `contexts.active_job`), scalars are overwritten.
|
|
47
47
|
def deep_merge(base, override)
|
|
48
48
|
base.merge(override) do |_key, old_value, new_value|
|
|
49
49
|
if old_value.is_a?(Hash) && new_value.is_a?(Hash)
|
|
@@ -7,9 +7,9 @@ require_relative "../scrubber"
|
|
|
7
7
|
require_relative "../line_cache"
|
|
8
8
|
|
|
9
9
|
module CloseYourIt
|
|
10
|
-
#
|
|
11
|
-
# (Errors::Ingest::Normalize).
|
|
12
|
-
# `exception.values`
|
|
10
|
+
# Turns a Ruby exception into the **Sentry event payload** the CloseYourIt backend ingests
|
|
11
|
+
# (Errors::Ingest::Normalize). Uses `backtrace_locations` (no regex) and puts the cause chain in
|
|
12
|
+
# `exception.values` ordered from the outermost to the main one (Sentry: values.last = the crash).
|
|
13
13
|
class ErrorEvent < Event
|
|
14
14
|
def self.from_exception(exception, configuration:, handled: false, level: "error", contexts: nil)
|
|
15
15
|
new(exception, configuration, handled: handled, level: level, contexts: contexts)
|
|
@@ -30,7 +30,7 @@ module CloseYourIt
|
|
|
30
30
|
"timestamp" => @occurred_at,
|
|
31
31
|
"platform" => "ruby",
|
|
32
32
|
"level" => @level,
|
|
33
|
-
#
|
|
33
|
+
# Log↔error correlation: same trace_id as the logs of the same request (populated by the Scope).
|
|
34
34
|
"trace_id" => CloseYourIt::Scope.current.trace_id,
|
|
35
35
|
"environment" => environment,
|
|
36
36
|
"release" => @configuration.release,
|
|
@@ -39,9 +39,9 @@ module CloseYourIt
|
|
|
39
39
|
"contexts" => { "runtime" => { "name" => "ruby", "version" => RUBY_VERSION } },
|
|
40
40
|
"sdk" => sdk
|
|
41
41
|
)
|
|
42
|
-
#
|
|
42
|
+
# Merges the per-request/job context (user/tags/extra/contexts/request) collected in the Scope.
|
|
43
43
|
merged = deep_merge(base, CloseYourIt::Scope.current.to_event_hash)
|
|
44
|
-
#
|
|
44
|
+
# Extra context passed explicitly (e.g. rails_error from the ErrorReporter).
|
|
45
45
|
@contexts ? deep_merge(merged, { "contexts" => @contexts }) : merged
|
|
46
46
|
end
|
|
47
47
|
|
|
@@ -51,7 +51,7 @@ module CloseYourIt
|
|
|
51
51
|
|
|
52
52
|
private
|
|
53
53
|
|
|
54
|
-
# Cause
|
|
54
|
+
# Cause chain → Sentry array: outermost cause first, main exception LAST.
|
|
55
55
|
def exception_values
|
|
56
56
|
chain = []
|
|
57
57
|
seen = []
|
|
@@ -73,10 +73,10 @@ module CloseYourIt
|
|
|
73
73
|
}
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
# Sentry-style:
|
|
77
|
-
# + snippet
|
|
78
|
-
# `filename`
|
|
79
|
-
# path
|
|
76
|
+
# Sentry-style: most recent frame last; keys filename/function/lineno/in_app/abs_path
|
|
77
|
+
# + source snippet (pre_context/context_line/post_context) when the file is readable.
|
|
78
|
+
# `filename` is relative/basename (culprit comparable across SDKs — CYRB-4); `abs_path` is the
|
|
79
|
+
# absolute path for the detail view and for reading the context lines' source.
|
|
80
80
|
def frames(locations)
|
|
81
81
|
return [] if locations.nil?
|
|
82
82
|
|
|
@@ -94,15 +94,15 @@ module CloseYourIt
|
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
# Path
|
|
98
|
-
# (
|
|
99
|
-
#
|
|
100
|
-
# "filename in function"
|
|
97
|
+
# Path to display (Sentry `filename`): relative to project_root if the file is inside the app
|
|
98
|
+
# (e.g. "app/models/user.rb"), otherwise the basename (gem/stdlib/external paths → "base.rb").
|
|
99
|
+
# Aligns Ruby with the other SDKs (Dart/JS send basename/relative), so the culprit
|
|
100
|
+
# "filename in function" is comparable at a glance on polyglot projects. See CYRB-4.
|
|
101
101
|
def relative_filename(path)
|
|
102
102
|
return path if path.nil?
|
|
103
103
|
|
|
104
104
|
root = @configuration.project_root.to_s
|
|
105
|
-
prefix = root.empty? ? nil : File.join(root, "") # root
|
|
105
|
+
prefix = root.empty? ? nil : File.join(root, "") # root with trailing separator, portable
|
|
106
106
|
return File.basename(path) unless prefix && path.start_with?(prefix)
|
|
107
107
|
|
|
108
108
|
relative = path[prefix.length..]
|
|
@@ -4,15 +4,15 @@ require "securerandom"
|
|
|
4
4
|
require_relative "../event"
|
|
5
5
|
|
|
6
6
|
module CloseYourIt
|
|
7
|
-
#
|
|
8
|
-
# (`subtype=slow_job`)
|
|
9
|
-
#
|
|
7
|
+
# `kind=performance_issue` metric for a background job that exceeded a DURATION threshold
|
|
8
|
+
# (`subtype=slow_job`) or a queue WAIT threshold (`subtype=job_queue_latency`). Sent on the metrics
|
|
9
|
+
# pipeline (`/api/v1/projects/:id/metrics`), like slow_query/slow_method and performance verdicts.
|
|
10
10
|
#
|
|
11
|
-
# `label`
|
|
12
|
-
# label/
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# Rollup.
|
|
11
|
+
# `label` is the job CLASS name — never the arguments (privacy: like slow_method it sends only
|
|
12
|
+
# label/duration). `attempt` makes retries visible; `trace_id` (= ActiveJob job_id / Sidekiq jid)
|
|
13
|
+
# correlates the metric with logs and errors of the same job. `duration_ms` is the subtype's
|
|
14
|
+
# measured value (perform duration for slow_job, queue wait for job_queue_latency) — same
|
|
15
|
+
# convention as the Rollup. Nil fields are omitted.
|
|
16
16
|
class JobMetricEvent < Event
|
|
17
17
|
def initialize(attrs, configuration)
|
|
18
18
|
super(configuration)
|
|
@@ -4,29 +4,29 @@ require "securerandom"
|
|
|
4
4
|
require_relative "../scrubber"
|
|
5
5
|
|
|
6
6
|
module CloseYourIt
|
|
7
|
-
#
|
|
8
|
-
# message/level/attributes/logger). `message`
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
7
|
+
# Structured log entry sent to the /logs ingest (NOT Sentry format: logs are a stream with
|
|
8
|
+
# message/level/attributes/logger). `message` AND `attributes` go through the Scrubber (denylist +
|
|
9
|
+
# patterns) like ErrorEvent — a log can contain as much PII/secrets as an error. `trace_id` is
|
|
10
|
+
# frozen at CONSTRUCTION (request thread) → log↔error correlation for the same request even when
|
|
11
|
+
# the flush happens on a different timer thread.
|
|
12
12
|
class LogEvent < Event
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
13
|
+
# Backend canonical levels (enum) + aliases of ::Logger-style names. Normalized HERE (single
|
|
14
|
+
# source) so every construction — via CloseYourIt.log, .logger or directly — yields a valid level.
|
|
15
|
+
# The index in LEVELS is also the level's numeric severity (debug=0 … fatal=4): the SAME map as
|
|
16
|
+
# dart/js, so threshold gating (`logs_min_level`) filters identically across SDKs (CYRB-6).
|
|
17
17
|
LEVELS = %w[debug info warning error fatal].freeze
|
|
18
18
|
LEVEL_ALIASES = { "warn" => "warning", "err" => "error", "unknown" => "fatal" }.freeze
|
|
19
19
|
|
|
20
|
-
#
|
|
21
|
-
#
|
|
20
|
+
# Normalizes any level (symbol/string, uppercase, alias) to the backend canonical name.
|
|
21
|
+
# Unknown → "info". Class method: single source reused by `CloseYourIt.log` for gating.
|
|
22
22
|
def self.normalize_level(level)
|
|
23
23
|
value = level.to_s.downcase
|
|
24
24
|
value = LEVEL_ALIASES.fetch(value, value)
|
|
25
25
|
LEVELS.include?(value) ? value : "info"
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
28
|
+
# Numeric severity of the level (index in LEVELS), identical to the cross-SDK map. Used for the
|
|
29
|
+
# comparison with `logs_min_level` before building/sending the log.
|
|
30
30
|
def self.severity(level)
|
|
31
31
|
LEVELS.index(normalize_level(level))
|
|
32
32
|
end
|
|
@@ -38,9 +38,9 @@ module CloseYourIt
|
|
|
38
38
|
@attributes = attributes || {}
|
|
39
39
|
@logger = logger
|
|
40
40
|
@scrubber = Scrubber.new(configuration)
|
|
41
|
-
# trace_id
|
|
42
|
-
#
|
|
43
|
-
#
|
|
41
|
+
# trace_id captured HERE, on the request thread: the LogEvent is buffered and flushed on a
|
|
42
|
+
# different timer thread, where the current Scope belongs to ANOTHER request (or is empty) →
|
|
43
|
+
# reading it lazily in to_h would break log↔error correlation. We freeze it at construction.
|
|
44
44
|
@trace_id = CloseYourIt::Scope.current.trace_id
|
|
45
45
|
end
|
|
46
46
|
|
|
@@ -71,7 +71,7 @@ module CloseYourIt
|
|
|
71
71
|
@scrubber.filter_params(deep_stringify_keys(@attributes))
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
#
|
|
74
|
+
# Keys always strings (nested too): consistent with the JSON payload and the Scrubber denylist.
|
|
75
75
|
def deep_stringify_keys(value)
|
|
76
76
|
case value
|
|
77
77
|
when Hash then value.each_with_object({}) { |(key, val), acc| acc[key.to_s] = deep_stringify_keys(val) }
|
|
@@ -4,9 +4,9 @@ require "securerandom"
|
|
|
4
4
|
require_relative "../scrubber"
|
|
5
5
|
|
|
6
6
|
module CloseYourIt
|
|
7
|
-
#
|
|
8
|
-
# (`message.formatted` + level).
|
|
9
|
-
# Scrubber (
|
|
7
|
+
# Explicit diagnostic message (`CloseYourIt.capture_message`) in Sentry event format
|
|
8
|
+
# (`message.formatted` + level). Merges the current Scope like ErrorEvent. The message goes through
|
|
9
|
+
# the Scrubber (patterns) like ErrorEvent's `exception.message` — PII parity across events.
|
|
10
10
|
class MessageEvent < Event
|
|
11
11
|
def initialize(message, level:, configuration:)
|
|
12
12
|
super(configuration)
|
|
@@ -21,7 +21,7 @@ module CloseYourIt
|
|
|
21
21
|
"timestamp" => @occurred_at,
|
|
22
22
|
"platform" => "ruby",
|
|
23
23
|
"level" => @level,
|
|
24
|
-
#
|
|
24
|
+
# Log↔error correlation: same trace_id as the logs of the same request (parity with ErrorEvent).
|
|
25
25
|
"trace_id" => CloseYourIt::Scope.current.trace_id,
|
|
26
26
|
"environment" => environment,
|
|
27
27
|
"release" => @configuration.release,
|
|
@@ -4,10 +4,10 @@ require "securerandom"
|
|
|
4
4
|
require_relative "../event"
|
|
5
5
|
|
|
6
6
|
module CloseYourIt
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# `subtype`
|
|
10
|
-
#
|
|
7
|
+
# `kind=performance_issue` payload for the metrics pipeline (`/api/v1/projects/:id/metrics`).
|
|
8
|
+
# It is an aggregate VERDICT (N+1, slow request, slow external HTTP), not a raw metric:
|
|
9
|
+
# `subtype` qualifies it, `trace_id` correlates it with logs/errors of the same request. The SQL is
|
|
10
|
+
# already obfuscated (it is the profile fingerprint). Nil fields are omitted (slow_request has no sql).
|
|
11
11
|
class PerformanceIssueEvent < Event
|
|
12
12
|
def initialize(attrs, configuration)
|
|
13
13
|
super(configuration)
|
|
@@ -5,10 +5,10 @@ require_relative "../event"
|
|
|
5
5
|
require_relative "../scrubber"
|
|
6
6
|
|
|
7
7
|
module CloseYourIt
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
8
|
+
# `kind=slow_method` payload for the metrics pipeline. By default only label + duration + location.
|
|
9
|
+
# Method arguments are sent ONLY with `capture_method_arguments` (opt-in, default OFF):
|
|
10
|
+
# positional by index, kwargs by name (sensitive key scrubbing), value via `inspect`
|
|
11
|
+
# truncated for JSON safety. See PDR §9.
|
|
12
12
|
class SlowMethodEvent < Event
|
|
13
13
|
def initialize(label, duration_ms, location, configuration, args: nil, kwargs: nil)
|
|
14
14
|
super(configuration)
|
|
@@ -41,8 +41,8 @@ module CloseYourIt
|
|
|
41
41
|
|
|
42
42
|
private
|
|
43
43
|
|
|
44
|
-
#
|
|
45
|
-
#
|
|
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).
|
|
46
46
|
def arguments
|
|
47
47
|
return nil unless @configuration.capture_method_arguments
|
|
48
48
|
|
|
@@ -6,8 +6,8 @@ require_relative "../scrubber"
|
|
|
6
6
|
require_relative "../scope"
|
|
7
7
|
|
|
8
8
|
module CloseYourIt
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# `kind=slow_query` payload for the metrics pipeline (`/api/v1/projects/:id/metrics`).
|
|
10
|
+
# The SQL is obfuscated (binds excluded) — see PDR §9.
|
|
11
11
|
class SlowQueryEvent < Event
|
|
12
12
|
def initialize(payload, duration_ms, configuration)
|
|
13
13
|
super(configuration)
|
|
@@ -47,8 +47,8 @@ module CloseYourIt
|
|
|
47
47
|
connection.adapter_name.to_s.downcase
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
#
|
|
51
|
-
# (denylist password/token/…);
|
|
50
|
+
# Bind values — ONLY with capture_query_bindings (opt-in, default OFF). Scrubbed by column name
|
|
51
|
+
# (denylist password/token/…); the value is rendered as a string for JSON safety.
|
|
52
52
|
def bindings
|
|
53
53
|
return nil unless @configuration.capture_query_bindings
|
|
54
54
|
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
require_relative "events/slow_method_event"
|
|
4
4
|
|
|
5
5
|
module CloseYourIt
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# Times blocks/methods with `CLOCK_MONOTONIC` and sends a `slow_method` when the duration exceeds
|
|
7
|
+
# the threshold. Arguments are sent only with `capture_method_arguments` (opt-in) — see SlowMethodEvent.
|
|
8
8
|
module Instrumenter
|
|
9
9
|
module_function
|
|
10
10
|
|
|
@@ -13,10 +13,10 @@ module CloseYourIt
|
|
|
13
13
|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
14
14
|
yield
|
|
15
15
|
ensure
|
|
16
|
-
# `measure`
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
16
|
+
# `measure` runs INSIDE the app's method (Monitor prepends): a failure here — threshold not
|
|
17
|
+
# configured, unreachable ingest — would raise in a caller that already had its result. It was
|
|
18
|
+
# the only unguarded entry point of the gem: now it is like all the others (CYRB-24). An
|
|
19
|
+
# exception from the measured block, instead, keeps propagating untouched.
|
|
20
20
|
begin
|
|
21
21
|
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000.0
|
|
22
22
|
report(label, duration_ms, location, args: args, kwargs: kwargs)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CloseYourIt
|
|
4
|
-
#
|
|
5
|
-
# (pre_context/context_line/post_context). Bounded:
|
|
6
|
-
#
|
|
4
|
+
# In-process cache of source file lines, for the frames' context lines
|
|
5
|
+
# (pre_context/context_line/post_context). Bounded: beyond MAX_FILES it is emptied entirely —
|
|
6
|
+
# simple and sufficient (the files of an app in error are few and recurring). Thread-safe.
|
|
7
7
|
module LineCache
|
|
8
8
|
MAX_FILES = 200
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@ module CloseYourIt
|
|
|
11
11
|
@mutex = Mutex.new
|
|
12
12
|
|
|
13
13
|
class << self
|
|
14
|
-
#
|
|
14
|
+
# The file's lines (chomped), or nil if unreadable or a synthetic path ("(eval)", "(irb)").
|
|
15
15
|
def lines(path)
|
|
16
16
|
return nil if path.nil? || path.empty? || path.start_with?("(")
|
|
17
17
|
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
require "concurrent"
|
|
4
4
|
|
|
5
5
|
module CloseYourIt
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# Thread-safe in-memory log buffer: collects LogEvents and flushes them in batches to /logs when
|
|
7
|
+
# they reach `logs_batch_size`, when `logs_flush_interval` expires (timer), or at shutdown.
|
|
8
|
+
# Cuts HTTP requests — logs are high-volume, unlike one-to-one errors/metrics.
|
|
9
9
|
class LogBuffer
|
|
10
|
-
attr_reader :timer #
|
|
10
|
+
attr_reader :timer # exposed for tests (checks the configured interval)
|
|
11
11
|
|
|
12
12
|
def initialize(client:, configuration:)
|
|
13
13
|
@client = client
|
|
@@ -33,12 +33,12 @@ module CloseYourIt
|
|
|
33
33
|
|
|
34
34
|
@client.flush_logs(batch)
|
|
35
35
|
rescue StandardError => e
|
|
36
|
-
#
|
|
37
|
-
# before_send
|
|
38
|
-
# (
|
|
36
|
+
# The flush runs both on the request thread (full batch) and on the timer thread: a to_h /
|
|
37
|
+
# before_send raising on ONE event must not propagate into the app nor kill the TimerTask
|
|
38
|
+
# (which would otherwise stop flushing → logs pile up and are lost). Swallowed and logged.
|
|
39
39
|
CloseYourIt.internal_logger.error("CloseYourIt log buffer: #{e.class}: #{e.message}")
|
|
40
|
-
#
|
|
41
|
-
#
|
|
40
|
+
# The batch has already been drained: if flush_logs raises, those logs are lost. Count them as
|
|
41
|
+
# drops so the failure is not silent (CYRB-12). `batch.to_a` is nil-safe.
|
|
42
42
|
batch.to_a.size.times do
|
|
43
43
|
CloseYourIt.stats.increment(:dropped)
|
|
44
44
|
CloseYourIt.notify_diagnostic(:drop, reason: :error)
|
|
@@ -60,7 +60,7 @@ module CloseYourIt
|
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
#
|
|
63
|
+
# Starts the periodic flush timer on the first entry (only once).
|
|
64
64
|
def ensure_timer
|
|
65
65
|
return if @timer
|
|
66
66
|
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CloseYourIt
|
|
4
|
-
#
|
|
5
|
-
# `CloseYourIt.emit_log` (→ ingest /logs).
|
|
6
|
-
# CloseYourIt.logger.warn("
|
|
7
|
-
# `warn`
|
|
4
|
+
# Logger-compatible object exposed as `CloseYourIt.logger`: every call forwards to
|
|
5
|
+
# `CloseYourIt.emit_log` (→ ingest /logs). Usable as the app's explicit logger, also with attributes:
|
|
6
|
+
# CloseYourIt.logger.warn("disk almost full", disk: "sda1")
|
|
7
|
+
# `warn` maps to the `warning` level (backend enum); supports blocks and `::Logger#add` as a drop-in.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# payments = CloseYourIt.logger.named("payments") # child
|
|
9
|
+
# The log source (the payload's `logger` field) is set ONLY via `.named` (child logger, dart/js
|
|
10
|
+
# parity) or the constructor — NEVER from a method keyword: so a `logger` key passed to a method
|
|
11
|
+
# stays a data attribute and is not hijacked as the source (CYRB-8).
|
|
12
|
+
# payments = CloseYourIt.logger.named("payments") # child with source "payments"
|
|
13
13
|
# payments.warn("retry", attempt: 3) # logger=payments, attributes={attempt:3}
|
|
14
|
-
# CloseYourIt.logger.warn("
|
|
14
|
+
# CloseYourIt.logger.warn("disk full", logger: "/dev/sda1") # attributes={logger:"/dev/sda1"}
|
|
15
15
|
class LogDevice
|
|
16
|
-
#
|
|
16
|
+
# ::Logger numeric severities → CloseYourIt levels (UNKNOWN→fatal).
|
|
17
17
|
SEVERITY_LEVELS = { 0 => "debug", 1 => "info", 2 => "warning", 3 => "error", 4 => "fatal", 5 => "fatal" }.freeze
|
|
18
18
|
|
|
19
|
-
# `source` =
|
|
19
|
+
# `source` = name of the log source (the payload's `logger` field). Prefer `.named` in apps.
|
|
20
20
|
def initialize(source = nil)
|
|
21
21
|
@source = normalize_source(source)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# Child logger
|
|
25
|
-
#
|
|
24
|
+
# Child logger with an explicit source (dart/js parity). Returns a NEW LogDevice: the logger it is
|
|
25
|
+
# called on stays unchanged (immutable), like the cross-SDK child loggers.
|
|
26
26
|
# CloseYourIt.logger.named("payments").info("retry", attempt: 3)
|
|
27
27
|
def named(source)
|
|
28
28
|
self.class.new(source)
|
|
@@ -39,7 +39,7 @@ module CloseYourIt
|
|
|
39
39
|
message
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
#
|
|
42
|
+
# Compatible with ::Logger#add(severity, message = nil, progname = nil).
|
|
43
43
|
def add(severity, message = nil, progname = nil, &block)
|
|
44
44
|
write(SEVERITY_LEVELS.fetch(severity.to_i, "info"), message || progname, {}, &block)
|
|
45
45
|
end
|
|
@@ -48,16 +48,16 @@ module CloseYourIt
|
|
|
48
48
|
private
|
|
49
49
|
|
|
50
50
|
def write(level, message, attributes, &block)
|
|
51
|
-
# Gate
|
|
51
|
+
# Gate BEFORE the block: `logger.debug { expensive_dump }` does not evaluate the block when logs are off.
|
|
52
52
|
return unless CloseYourIt.logs_active?
|
|
53
53
|
|
|
54
54
|
message = block.call if block
|
|
55
|
-
#
|
|
56
|
-
#
|
|
55
|
+
# Source and attributes kept apart: the `attributes` (including any `logger` key) stay data;
|
|
56
|
+
# the source is ONLY `@source` (via `.named`/constructor) → no collision (CYRB-8).
|
|
57
57
|
CloseYourIt.emit_log(level, message, source: @source, attributes: attributes)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
#
|
|
60
|
+
# Empty/blank source → no source (nil); otherwise a string (the `logger` field is textual).
|
|
61
61
|
def normalize_source(source)
|
|
62
62
|
return nil if source.nil?
|
|
63
63
|
|
data/lib/closeyourit/monitor.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require_relative "instrumenter"
|
|
4
4
|
|
|
5
5
|
module CloseYourIt
|
|
6
|
-
# Macro
|
|
6
|
+
# Macro to instrument a method automatically:
|
|
7
7
|
#
|
|
8
8
|
# class Report
|
|
9
9
|
# include CloseYourIt::Monitor
|
|
@@ -11,7 +11,7 @@ module CloseYourIt
|
|
|
11
11
|
# monitor :generate
|
|
12
12
|
# end
|
|
13
13
|
#
|
|
14
|
-
#
|
|
14
|
+
# Wraps the method via `Module#prepend` timing it, without changing its signature/result.
|
|
15
15
|
module Monitor
|
|
16
16
|
def self.included(base)
|
|
17
17
|
base.extend(ClassMethods)
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module CloseYourIt
|
|
4
4
|
module Performance
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# Per-request accumulator (lives in the Scope, reset at the end of the request). Counts queries,
|
|
6
|
+
# groups them by [obfuscated SQL fingerprint, call site] (the prosopite pattern for N+1) and keeps
|
|
7
|
+
# the external HTTP calls. Pure in-memory state: Performance::Rollup computes the verdict.
|
|
8
8
|
class RequestProfile
|
|
9
|
-
#
|
|
9
|
+
# Memory guard: cap on the distinct groups/calls tracked (the total count stays exact).
|
|
10
10
|
MAX_GROUPS = 1000
|
|
11
11
|
MAX_EXTERNAL = 500
|
|
12
12
|
|
|
@@ -19,7 +19,7 @@ module CloseYourIt
|
|
|
19
19
|
@external_calls = []
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
#
|
|
22
|
+
# A non-system query. Cached queries are not DB round-trips → they do not count for N+1.
|
|
23
23
|
def add_query(fingerprint:, source:, duration_ms:, cached: false)
|
|
24
24
|
return if cached
|
|
25
25
|
|
|
@@ -4,8 +4,8 @@ require_relative "../events/performance_issue_event"
|
|
|
4
4
|
|
|
5
5
|
module CloseYourIt
|
|
6
6
|
module Performance
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# Turns a RequestProfile (+ request duration/route) into 0..N PerformanceIssueEvent verdicts.
|
|
8
|
+
# Thresholds live in the Configuration. Detection on the client; dedup/alerts on the backend.
|
|
9
9
|
class Rollup
|
|
10
10
|
def self.call(...) = new(...).call
|
|
11
11
|
|
|
@@ -28,7 +28,7 @@ module CloseYourIt
|
|
|
28
28
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
|
-
#
|
|
31
|
+
# One verdict for every [fingerprint, call-site] group that ran more than n_plus_one_threshold times.
|
|
32
32
|
def n_plus_one_events
|
|
33
33
|
@profile.query_groups.values.filter_map do |group|
|
|
34
34
|
next unless group[:count] > @config.n_plus_one_threshold
|