closeyourit-ruby 0.10.2 → 0.11.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 +16 -1
- data/lib/closeyourit/configuration.rb +2 -1
- data/lib/closeyourit/events/job_metric_event.rb +3 -0
- data/lib/closeyourit/job_context.rb +61 -0
- data/lib/closeyourit/rails/active_job_extension.rb +3 -0
- data/lib/closeyourit/rails/railtie.rb +2 -18
- data/lib/closeyourit/scrubber.rb +3 -2
- data/lib/closeyourit/sidekiq/job_metrics_middleware.rb +31 -8
- data/lib/closeyourit/subscribers/job_performance.rb +73 -6
- data/lib/closeyourit/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d77ef65ad83c3890320e2c3c3a002a6708ef52d223204da4b553ac75ec58c4b9
|
|
4
|
+
data.tar.gz: 4bc0274665e54f19fe33d6e379270f31802a71317075ceae65fecdd4b8a6d66c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc89f3b0f20d8781b5a963b9fda04935b735500612db5883b29f86abc92bc560a452abc133db66859b919cdfcd8994ff6c4891fdcafe39a6c7878a960c74b627
|
|
7
|
+
data.tar.gz: 5b55acc5e39e78e9fc875e6cbfd32dfce56bbb9e27d600ec30f86a6d27d12660c1c61195a28c32f32ac22b418c78ac9d41e83af8bbdbd26f96781d740bd6f1cd
|
data/README.md
CHANGED
|
@@ -109,6 +109,7 @@ end
|
|
|
109
109
|
| `slow_request_threshold_ms` | `1000` | Durata totale della richiesta (ms) oltre cui = `slow_request` |
|
|
110
110
|
| `slow_external_threshold_ms` | `1000` | Durata di una singola HTTP esterna (ms) oltre cui = `slow_external_http` |
|
|
111
111
|
| `capture_external_http` | `true` | Strumenta `Net::HTTP` per rilevare le HTTP esterne (effettivo solo con `detect_performance_issues`) |
|
|
112
|
+
| `job_lifecycle_logs` | `false` | Log informativo per esito di tentativo osservato, opt-in. |
|
|
112
113
|
| `monitor_jobs` | `true` | Misura durata e attesa in coda dei background job (ActiveJob + Sidekiq) → metriche `slow_job`/`job_queue_latency` (vedi [Metriche dei background job](#metriche-dei-background-job)) |
|
|
113
114
|
| `slow_job_threshold_ms` | `5000` | Durata del job (ms) oltre cui = `slow_job` |
|
|
114
115
|
| `job_queue_latency_threshold_ms` | `60000` | Attesa in coda (enqueue→esecuzione, ms) oltre cui = `job_queue_latency` |
|
|
@@ -359,7 +360,7 @@ sulla pipeline metriche (`/api/v1/projects/:id/metrics`). Funziona con **ActiveJ
|
|
|
359
360
|
Due `subtype`:
|
|
360
361
|
|
|
361
362
|
- **`slow_job`** — la durata del `perform` supera `slow_job_threshold_ms` (default 5000 ms).
|
|
362
|
-
- **`job_queue_latency`** — l'attesa
|
|
363
|
+
- **`job_queue_latency`** — l'attesa dopo la scadenza effettiva (`max(enqueued_at, scheduled_at)`) supera
|
|
363
364
|
`job_queue_latency_threshold_ms` (default 60000 ms): coda intasata o worker insufficienti.
|
|
364
365
|
|
|
365
366
|
Ogni metrica porta la **label** (nome della classe del job), la **queue**, l'**adapter**
|
|
@@ -380,6 +381,20 @@ CloseYourIt.init do |c|
|
|
|
380
381
|
end
|
|
381
382
|
```
|
|
382
383
|
|
|
384
|
+
Il contesto additivo `contexts.job` usa il contratto `schema_version: 1`: separa durata monotona,
|
|
385
|
+
attesa in coda e ritardo programmato. Un orologio mancante resta `null`; un'attesa negativa viene
|
|
386
|
+
azzerata con `clock_skew: true`. Sidekiq 8 usa timestamp in millisecondi, le versioni precedenti
|
|
387
|
+
secondi: la conversione segue la versione del framework, senza euristiche sulla grandezza.
|
|
388
|
+
|
|
389
|
+
Per osservare anche gli esiti dei tentativi veloci, abilita `c.job_lifecycle_logs = true` (default
|
|
390
|
+
`false`). Emette un log `info` con messaggio fisso `Background job attempt finished` e
|
|
391
|
+
`attributes.contexts.job`, senza argomenti, risultati o header. `retry` non è terminale;
|
|
392
|
+
`discarded` è esplicito; un errore con retry dell'adapter non noto ha `terminal: null`.
|
|
393
|
+
Un `rescue_from` applicativo non identificabile resta `unknown`, non diventa successo.
|
|
394
|
+
ActiveJob dentro Sidekiq è osservato una sola volta quando il subscriber ActiveJob è installato.
|
|
395
|
+
Sampling, disabilitazione o perdita del transport impediscono di considerare questi log un censimento.
|
|
396
|
+
Il raggruppamento backend v1 separa framework, nome, coda e misura; i gruppi legacy restano intatti.
|
|
397
|
+
|
|
383
398
|
## Privacy & PII
|
|
384
399
|
|
|
385
400
|
Privacy-by-default (`send_pii = false`). In sintesi:
|
|
@@ -41,7 +41,7 @@ module CloseYourIt
|
|
|
41
41
|
:query_time_threshold_ms, :slow_request_threshold_ms, :slow_external_threshold_ms,
|
|
42
42
|
:capture_external_http, :trap_signals,
|
|
43
43
|
:monitor_jobs, :slow_job_threshold_ms, :job_queue_latency_threshold_ms,
|
|
44
|
-
:jobs_sample_rate, :propagate_trace_context,
|
|
44
|
+
:jobs_sample_rate, :job_lifecycle_logs, :propagate_trace_context,
|
|
45
45
|
:usage_enabled, :usage_flush_interval, :usage_max_symbols
|
|
46
46
|
attr_writer :release, :project_root
|
|
47
47
|
attr_reader :excluded_exceptions, :excluded_log_patterns, :excluded_query_patterns,
|
|
@@ -157,6 +157,7 @@ module CloseYourIt
|
|
|
157
157
|
# (normal jobs under threshold = no metric) and by sampling. The label is the job class name;
|
|
158
158
|
# arguments are NEVER sent.
|
|
159
159
|
@monitor_jobs = true
|
|
160
|
+
@job_lifecycle_logs = false
|
|
160
161
|
@slow_job_threshold_ms = 5000 # perform duration beyond which = slow_job
|
|
161
162
|
@job_queue_latency_threshold_ms = 60_000 # enqueue→execution wait beyond which = job_queue_latency
|
|
162
163
|
@jobs_sample_rate = 1.0 # fraction of over-threshold candidates actually sent
|
|
@@ -33,6 +33,9 @@ module CloseYourIt
|
|
|
33
33
|
"queue" => @attrs[:queue],
|
|
34
34
|
"adapter" => @attrs[:adapter],
|
|
35
35
|
"attempt" => @attrs[:attempt],
|
|
36
|
+
"contexts" => (@attrs[:job_context] && { "job" => @attrs[:job_context].merge(
|
|
37
|
+
"measurement" => (@attrs[:subtype] == "slow_job" ? "duration" : "queue_wait")
|
|
38
|
+
) }),
|
|
36
39
|
"sdk" => sdk
|
|
37
40
|
)
|
|
38
41
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
module CloseYourIt
|
|
7
|
+
# Versioned attempt metadata. Missing clocks stay unknown, never an invented zero.
|
|
8
|
+
module JobContext
|
|
9
|
+
def self.build(framework:, name:, queue: nil, job_id: nil, attempt: nil, retry_count: :derive,
|
|
10
|
+
enqueued_at: nil, scheduled_at: nil, started_at: nil)
|
|
11
|
+
enqueued, scheduled, started = [ enqueued_at, scheduled_at, started_at ].map { |value| instant(value) }
|
|
12
|
+
due = enqueued && scheduled ? [ enqueued, scheduled ].max : enqueued
|
|
13
|
+
wait = (started - due) * 1000.0 if started && due
|
|
14
|
+
delay = (scheduled - enqueued) * 1000.0 if enqueued && scheduled
|
|
15
|
+
attempt = nil unless attempt.is_a?(Integer) && attempt.positive?
|
|
16
|
+
retry_count = attempt && attempt - 1 if retry_count == :derive
|
|
17
|
+
retry_count = nil unless retry_count.is_a?(Integer) && retry_count >= 0
|
|
18
|
+
{
|
|
19
|
+
"schema_version" => 1, "framework" => framework, "name" => bounded_text(name, 200) || "unknown",
|
|
20
|
+
"queue" => bounded_text(queue, 128), "job_id" => bounded_text(job_id, 256), "attempt" => attempt,
|
|
21
|
+
"retry_count" => retry_count, "attempt_outcome" => "unknown", "terminal" => nil,
|
|
22
|
+
"enqueued_at" => enqueued&.iso8601(6), "scheduled_at" => scheduled&.iso8601(6),
|
|
23
|
+
"started_at" => started&.iso8601(6), "duration_ms" => nil,
|
|
24
|
+
"queue_wait_ms" => wait && [ wait, 0.0 ].max,
|
|
25
|
+
"scheduled_delay_ms" => delay && [ delay, 0.0 ].max, "clock_skew" => !!(wait && wait.negative?)
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.bounded_text(value, limit)
|
|
30
|
+
value if value.is_a?(String) && value.length.between?(1, limit) && !value.match?(/[\x00-\x1f\x7f]/)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.sidekiq_time(value, version:)
|
|
34
|
+
return value unless value.is_a?(Numeric)
|
|
35
|
+
|
|
36
|
+
version.to_s.split(".").first.to_i >= 8 ? value / 1000.0 : value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.instant(value)
|
|
40
|
+
case value
|
|
41
|
+
when Time then value.getutc
|
|
42
|
+
when Numeric then Time.at(value).utc if value.finite?
|
|
43
|
+
when String
|
|
44
|
+
if value.match?(/(?:Z|[+-]\d{2}:\d{2})\z/)
|
|
45
|
+
DateTime.rfc3339(value)
|
|
46
|
+
Time.iso8601(value).utc
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
rescue ArgumentError, RangeError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.log(context, configuration)
|
|
54
|
+
return unless configuration.monitor_jobs && configuration.job_lifecycle_logs
|
|
55
|
+
|
|
56
|
+
CloseYourIt.log(:info, "Background job attempt finished", logger: "closeyourit.jobs", contexts: { job: context })
|
|
57
|
+
rescue StandardError
|
|
58
|
+
CloseYourIt.internal_logger.warn("CloseYourIt job lifecycle emission failed")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -31,6 +31,9 @@ module CloseYourIt
|
|
|
31
31
|
def self.included(base)
|
|
32
32
|
base.around_perform do |job, block|
|
|
33
33
|
CloseYourIt::Rails::ActiveJobExtension.monitor(job) { block.call }
|
|
34
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
35
|
+
job.instance_variable_set(:@__closeyourit_job_outcome, :raised)
|
|
36
|
+
raise
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
return unless base.respond_to?(:after_discard)
|
|
@@ -72,25 +72,9 @@ module CloseYourIt
|
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
#
|
|
76
|
-
# `perform_start` gives the wait (now - enqueued_at) as soon as the job starts, `perform` gives
|
|
77
|
-
# the execution duration at the end. Beyond threshold → slow_job / job_queue_latency metrics.
|
|
78
|
-
# No-op when monitor_jobs is OFF.
|
|
75
|
+
# Observe the completed attempt after ActiveJob has decided retry/discard.
|
|
79
76
|
initializer "closeyourit.subscribe_active_job_performance" do
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
ActiveSupport::Notifications.subscribe("perform_start.active_job") do |*args|
|
|
83
|
-
job = ActiveSupport::Notifications::Event.new(*args).payload[:job]
|
|
84
|
-
jobs.active_job_started(job) if job
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
ActiveSupport::Notifications.subscribe("perform.active_job") do |*args|
|
|
88
|
-
event = ActiveSupport::Notifications::Event.new(*args)
|
|
89
|
-
job = event.payload[:job]
|
|
90
|
-
# CYSK-29 — jobs also declare they ran: kind `job`, symbol = the class.
|
|
91
|
-
CloseYourIt.usage_registry.record("job", job.class.name) if job && CloseYourIt.enabled?
|
|
92
|
-
jobs.active_job_performed(job, event.duration) if job
|
|
93
|
-
end
|
|
77
|
+
CloseYourIt::Subscribers::JobPerformance.new.install
|
|
94
78
|
end
|
|
95
79
|
|
|
96
80
|
# Captures HANDLED errors reported via Rails.error.report (Rails 7+).
|
data/lib/closeyourit/scrubber.rb
CHANGED
|
@@ -69,8 +69,9 @@ module CloseYourIt
|
|
|
69
69
|
# credential: here the prose guard is needed, the word "bearer" shows up in messages.
|
|
70
70
|
AUTH_CREDENTIAL = /(\b#{AUTH_SCHEME}[ \t]+)#{NOT_PROSE}#{CREDENTIAL_TOKEN}/
|
|
71
71
|
|
|
72
|
+
DOLLAR_LITERAL = /\$([A-Za-z_][A-Za-z0-9_]*|)\$.*?(?:\$\1\$|\z)/m
|
|
72
73
|
STRING_LITERAL = /'(?:[^']|'')*'/
|
|
73
|
-
NUMERIC_LITERAL =
|
|
74
|
+
NUMERIC_LITERAL = /(?<![\w$])\d+(?:\.\d+)?\b/
|
|
74
75
|
|
|
75
76
|
def initialize(configuration)
|
|
76
77
|
@configuration = configuration
|
|
@@ -94,7 +95,7 @@ module CloseYourIt
|
|
|
94
95
|
def obfuscate_sql(sql)
|
|
95
96
|
return sql if sql.nil? || !@configuration.obfuscate_sql
|
|
96
97
|
|
|
97
|
-
sql.to_s.gsub(STRING_LITERAL, "?").gsub(NUMERIC_LITERAL, "?")
|
|
98
|
+
sql.to_s.gsub(DOLLAR_LITERAL, "?").gsub(STRING_LITERAL, "?").gsub(NUMERIC_LITERAL, "?")
|
|
98
99
|
end
|
|
99
100
|
|
|
100
101
|
# Built-in rule (authentication schemes) BEFORE the user patterns: the two protections add up,
|
|
@@ -16,27 +16,50 @@ module CloseYourIt
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def call(_worker, job, queue)
|
|
19
|
+
return yield if active_job_owned?(job)
|
|
20
|
+
|
|
19
21
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
context = observe(job, queue)
|
|
23
|
+
value = yield
|
|
24
|
+
succeeded = true
|
|
25
|
+
value
|
|
24
26
|
ensure
|
|
25
|
-
emit(job, queue, started,
|
|
27
|
+
emit(job, queue, started, context, succeeded) if started
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
private
|
|
29
31
|
|
|
30
|
-
def
|
|
32
|
+
def active_job_owned?(job)
|
|
33
|
+
%w[ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper Sidekiq::ActiveJob::Wrapper].include?(job["class"]) &&
|
|
34
|
+
Subscribers::JobPerformance.active_job_installed?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def observe(job, queue)
|
|
38
|
+
# Sidekiq 8 changed the timestamp wire unit; do not infer it from magnitude.
|
|
39
|
+
version = ::Sidekiq::VERSION if defined?(::Sidekiq::VERSION)
|
|
40
|
+
enqueued = JobContext.sidekiq_time(job["enqueued_at"], version: version)
|
|
41
|
+
JobContext.build(framework: "sidekiq", name: job["wrapped"] || job["class"],
|
|
42
|
+
queue: queue || job["queue"], job_id: job["jid"], attempt: attempt(job), retry_count: job["retry_count"],
|
|
43
|
+
enqueued_at: enqueued, started_at: Time.now.utc)
|
|
44
|
+
rescue StandardError
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def emit(job, queue, started, context, succeeded)
|
|
31
49
|
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000.0
|
|
50
|
+
if context
|
|
51
|
+
context = context.merge("duration_ms" => duration_ms, "attempt_outcome" => succeeded ? "success" : "error",
|
|
52
|
+
"terminal" => succeeded ? true : nil)
|
|
53
|
+
JobContext.log(context, CloseYourIt.configuration)
|
|
54
|
+
end
|
|
32
55
|
subscriber.record(
|
|
33
56
|
job_class: job["wrapped"] || job["class"],
|
|
34
57
|
queue: queue || job["queue"],
|
|
35
58
|
adapter: "sidekiq",
|
|
36
59
|
duration_ms: duration_ms,
|
|
37
|
-
queue_latency_ms:
|
|
60
|
+
queue_latency_ms: context && context["queue_wait_ms"],
|
|
38
61
|
attempt: attempt(job),
|
|
39
|
-
trace_id: job["jid"]
|
|
62
|
+
trace_id: job["jid"], job_context: context
|
|
40
63
|
)
|
|
41
64
|
rescue StandardError => e
|
|
42
65
|
CloseYourIt.internal_logger.error("CloseYourIt job metrics: #{e.class}: #{e.message}")
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "time"
|
|
4
4
|
require_relative "../events/job_metric_event"
|
|
5
|
+
require_relative "../job_context"
|
|
5
6
|
|
|
6
7
|
module CloseYourIt
|
|
7
8
|
module Subscribers
|
|
@@ -12,10 +13,43 @@ module CloseYourIt
|
|
|
12
13
|
# wiring to ActiveSupport::Notifications and the Sidekiq middleware lives elsewhere (Railtie /
|
|
13
14
|
# JobMetricsMiddleware). Honors the `monitor_jobs` master switch, the thresholds and `jobs_sample_rate`.
|
|
14
15
|
class JobPerformance
|
|
16
|
+
REGISTRY_LOCK = Mutex.new
|
|
17
|
+
ACTIVE_JOB_SUBSCRIBERS = []
|
|
18
|
+
|
|
19
|
+
def self.active_job_installed?
|
|
20
|
+
REGISTRY_LOCK.synchronize { !ACTIVE_JOB_SUBSCRIBERS.empty? }
|
|
21
|
+
end
|
|
15
22
|
def initialize(configuration = nil)
|
|
16
23
|
@configuration = configuration
|
|
17
24
|
end
|
|
18
25
|
|
|
26
|
+
def install
|
|
27
|
+
REGISTRY_LOCK.synchronize do
|
|
28
|
+
return self if @subscriptions || !ACTIVE_JOB_SUBSCRIBERS.empty?
|
|
29
|
+
|
|
30
|
+
@subscriptions = []
|
|
31
|
+
{ "enqueue_retry" => :retry, "retry_stopped" => :exhausted, "discard" => :discarded }.each do |name, outcome|
|
|
32
|
+
subscribe(name) { |event| event.payload[:job]&.instance_variable_set(:@__closeyourit_job_outcome, outcome) }
|
|
33
|
+
end
|
|
34
|
+
subscribe("perform_start") { |event| active_job_started(event.payload[:job]) if event.payload[:job] }
|
|
35
|
+
subscribe("perform") do |event|
|
|
36
|
+
job = event.payload[:job]
|
|
37
|
+
if job
|
|
38
|
+
CloseYourIt.usage_registry.record("job", job.class.name) if CloseYourIt.enabled?
|
|
39
|
+
active_job_performed(job, event.duration, payload: event.payload)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
ACTIVE_JOB_SUBSCRIBERS << self
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def uninstall
|
|
48
|
+
Array(@subscriptions).each { |subscription| ActiveSupport::Notifications.unsubscribe(subscription) }
|
|
49
|
+
@subscriptions = nil
|
|
50
|
+
REGISTRY_LOCK.synchronize { ACTIVE_JOB_SUBSCRIBERS.delete(self) }
|
|
51
|
+
end
|
|
52
|
+
|
|
19
53
|
# Single decision point: from the measured values (duration and/or wait) it builds 0..2 metrics,
|
|
20
54
|
# applies the thresholds (strict `>`, so X makes no noise and X+1 does) and sampling, then sends
|
|
21
55
|
# them fire-and-forget. `duration_ms` and `queue_latency_ms` are optional: ActiveJob provides them
|
|
@@ -23,11 +57,11 @@ module CloseYourIt
|
|
|
23
57
|
# Sampling is applied ONLY to candidates already beyond the threshold (normal jobs neither
|
|
24
58
|
# consume nor generate anything).
|
|
25
59
|
def record(job_class:, queue: nil, adapter: nil, duration_ms: nil, queue_latency_ms: nil,
|
|
26
|
-
attempt: nil, trace_id: nil)
|
|
60
|
+
attempt: nil, trace_id: nil, job_context: nil)
|
|
27
61
|
config = configuration
|
|
28
62
|
return unless config.monitor_jobs
|
|
29
63
|
|
|
30
|
-
common = { job_class: job_class, queue: queue, adapter: adapter, attempt: attempt, trace_id: trace_id }
|
|
64
|
+
common = { job_class: job_class, queue: queue, adapter: adapter, attempt: attempt, trace_id: trace_id, job_context: job_context }
|
|
31
65
|
events = []
|
|
32
66
|
events << build(config, "slow_job", duration_ms, common) if slow?(config, duration_ms)
|
|
33
67
|
events << build(config, "job_queue_latency", queue_latency_ms, common) if waited?(config, queue_latency_ms)
|
|
@@ -41,26 +75,51 @@ module CloseYourIt
|
|
|
41
75
|
# `perform_start.active_job` hook: the queue wait is known as soon as the job starts (now - the
|
|
42
76
|
# moment it was due to run).
|
|
43
77
|
def active_job_started(job, now: Time.now.utc)
|
|
78
|
+
context = JobContext.build(framework: "active_job", name: job.class.name,
|
|
79
|
+
queue: (job.queue_name if job.respond_to?(:queue_name)), job_id: (job.job_id if job.respond_to?(:job_id)),
|
|
80
|
+
attempt: (job.executions if job.respond_to?(:executions)), enqueued_at: enqueued_at(job),
|
|
81
|
+
scheduled_at: (job.scheduled_at if job.respond_to?(:scheduled_at)), started_at: now)
|
|
82
|
+
job.instance_variable_set(:@__closeyourit_job_context, context)
|
|
44
83
|
record(
|
|
45
84
|
job_class: job.class.name,
|
|
46
85
|
queue: (job.queue_name if job.respond_to?(:queue_name)),
|
|
47
86
|
adapter: "active_job",
|
|
48
|
-
queue_latency_ms:
|
|
87
|
+
queue_latency_ms: context["queue_wait_ms"],
|
|
49
88
|
attempt: (job.executions if job.respond_to?(:executions)),
|
|
50
|
-
trace_id: (job.job_id if job.respond_to?(:job_id))
|
|
89
|
+
trace_id: (job.job_id if job.respond_to?(:job_id)), job_context: context
|
|
51
90
|
)
|
|
52
91
|
end
|
|
53
92
|
|
|
54
93
|
# `perform.active_job` hook: at the end of execution the duration is `event.duration` (ms).
|
|
55
|
-
def active_job_performed(job, duration_ms)
|
|
94
|
+
def active_job_performed(job, duration_ms, payload: {})
|
|
95
|
+
context = job.instance_variable_get(:@__closeyourit_job_context)
|
|
96
|
+
if context
|
|
97
|
+
outcome, terminal = active_job_outcome(job, payload)
|
|
98
|
+
context = context.merge("duration_ms" => duration_ms, "attempt_outcome" => outcome, "terminal" => terminal)
|
|
99
|
+
JobContext.log(context, configuration)
|
|
100
|
+
end
|
|
56
101
|
record(
|
|
57
102
|
job_class: job.class.name,
|
|
58
103
|
queue: (job.queue_name if job.respond_to?(:queue_name)),
|
|
59
104
|
adapter: "active_job",
|
|
60
105
|
duration_ms: duration_ms,
|
|
61
106
|
attempt: (job.executions if job.respond_to?(:executions)),
|
|
62
|
-
trace_id: (job.job_id if job.respond_to?(:job_id))
|
|
107
|
+
trace_id: (job.job_id if job.respond_to?(:job_id)), job_context: context
|
|
63
108
|
)
|
|
109
|
+
ensure
|
|
110
|
+
job.remove_instance_variable(:@__closeyourit_job_context) if job.instance_variable_defined?(:@__closeyourit_job_context)
|
|
111
|
+
job.remove_instance_variable(:@__closeyourit_job_outcome) if job.instance_variable_defined?(:@__closeyourit_job_outcome)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def active_job_outcome(job, payload)
|
|
115
|
+
outcome = job.instance_variable_get(:@__closeyourit_job_outcome)
|
|
116
|
+
return [ "error", nil ] if payload[:exception_object] || payload[:exception]
|
|
117
|
+
return [ "retry", false ] if outcome == :retry
|
|
118
|
+
return [ "discarded", true ] if outcome == :discarded
|
|
119
|
+
return [ "error", nil ] if outcome == :exhausted
|
|
120
|
+
return [ "unknown", nil ] if outcome == :raised || payload[:aborted]
|
|
121
|
+
|
|
122
|
+
[ "success", true ]
|
|
64
123
|
end
|
|
65
124
|
|
|
66
125
|
# Normalizes `enqueued_at` (Time, Numeric epoch in seconds like Sidekiq, or ISO8601 String) into
|
|
@@ -91,6 +150,14 @@ module CloseYourIt
|
|
|
91
150
|
|
|
92
151
|
private
|
|
93
152
|
|
|
153
|
+
def subscribe(name, &callback)
|
|
154
|
+
@subscriptions << ActiveSupport::Notifications.monotonic_subscribe("#{name}.active_job") do |*args|
|
|
155
|
+
callback.call(ActiveSupport::Notifications::Event.new(*args))
|
|
156
|
+
rescue StandardError
|
|
157
|
+
CloseYourIt.internal_logger.warn("CloseYourIt job observation failed")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
94
161
|
def enqueued_at(job)
|
|
95
162
|
job.enqueued_at if job.respond_to?(:enqueued_at)
|
|
96
163
|
end
|
data/lib/closeyourit/version.rb
CHANGED
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.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alessio Bussolari
|
|
@@ -62,6 +62,7 @@ files:
|
|
|
62
62
|
- lib/closeyourit/events/slow_method_event.rb
|
|
63
63
|
- lib/closeyourit/events/slow_query_event.rb
|
|
64
64
|
- lib/closeyourit/instrumenter.rb
|
|
65
|
+
- lib/closeyourit/job_context.rb
|
|
65
66
|
- lib/closeyourit/line_cache.rb
|
|
66
67
|
- lib/closeyourit/log_buffer.rb
|
|
67
68
|
- lib/closeyourit/log_device.rb
|