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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 866fd0b6c34af299e0e538ddb181b1f6b3fd961da1e638226a81ad8b8ea15c0b
4
- data.tar.gz: eaf0d4530e9015f7353790aeffe104f04a2f80d46c4c7622ad7dad3302bd75fe
3
+ metadata.gz: d77ef65ad83c3890320e2c3c3a002a6708ef52d223204da4b553ac75ec58c4b9
4
+ data.tar.gz: 4bc0274665e54f19fe33d6e379270f31802a71317075ceae65fecdd4b8a6d66c
5
5
  SHA512:
6
- metadata.gz: b4193d36f1123b89bdf11998ebcd5ea445b0b84c325ccae654d434d2d94158d6dd9c920b0e879167f0fba5cbaa7615f42a3d2cd11f3f61d8ade9f3eaee18917f
7
- data.tar.gz: 7a4d366fbf67b5720d878e2e1febbcda5500c6063d8f654e450c10b68789964a7c8b15390e23d4516d9220ea6cb2ffbde619900ec2871fe7b8b20fd23c967b8b
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 in coda (dall'enqueue all'inizio dell'esecuzione) supera
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
- # Measures duration and queue wait of ActiveJob jobs via ActiveSupport notifications:
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
- jobs = CloseYourIt::Subscribers::JobPerformance.new
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+).
@@ -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 = /\b\d+(?:\.\d+)?\b/
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
- # The queue wait is known at the START (now - enqueued_at); Sidekiq stores enqueued_at as epoch
21
- # seconds. Computed before the yield so it does not include the job duration.
22
- latency = Subscribers::JobPerformance.latency_ms(job["enqueued_at"], now: Time.now.utc)
23
- yield
22
+ context = observe(job, queue)
23
+ value = yield
24
+ succeeded = true
25
+ value
24
26
  ensure
25
- emit(job, queue, started, latency)
27
+ emit(job, queue, started, context, succeeded) if started
26
28
  end
27
29
 
28
30
  private
29
31
 
30
- def emit(job, queue, started, latency)
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: latency,
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: self.class.latency_ms(due_at(job), now: now),
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloseYourIt
4
- VERSION = "0.10.2"
4
+ VERSION = "0.11.0"
5
5
  end
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.10.2
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