deployangel 0.1.14 → 0.1.15
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/CHANGELOG.md +8 -0
- data/README.md +4 -0
- data/lib/deployangel/agent.rb +4 -2
- data/lib/deployangel/core/aggregator.rb +21 -5
- data/lib/deployangel/core/execution_context.rb +37 -0
- data/lib/deployangel/core/protocol.rb +5 -1
- data/lib/deployangel/rack/http.rb +5 -0
- data/lib/deployangel/rails/active_job.rb +29 -0
- data/lib/deployangel/sidekiq.rb +3 -0
- data/lib/deployangel/version.rb +1 -1
- data/lib/deployangel.rb +1 -0
- 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: fae02d0fd8337fc6eb82ef0492d1bab86d03071dd9c9774dc332e95a6f52ffc9
|
|
4
|
+
data.tar.gz: 733fce2015cfa6e8fd6f5c618005cdda6c5aabf6aae8d7be128a156c46be65a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db2bd88f8bb149df827631c8a44453c8a328c790b6174a370d39469c1e025055bfb5923c70f52984b8a1a6f2e702e79a17c600f176375dc053f31c390f7ebae8
|
|
7
|
+
data.tar.gz: 2e33938b4d09aa6bd342300e2097ad7a10fe8a3a722296acd5e06a6e5fc9f39a9c6f16364907abb159dcfe02c95f659e0df5096e3c51f9334240b1cce50f0faa
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.15 (2026-10-06)
|
|
4
|
+
|
|
5
|
+
- Checkpoints report whether they were recorded in an HTTP request or a job
|
|
6
|
+
(`http` and `job` counts on each checkpoint), so DeployAngel compares a
|
|
7
|
+
checkpoint recorded in jobs against job traffic rather than requests. A job
|
|
8
|
+
performed inline during a request counts as a job. Needs a DeployAngel
|
|
9
|
+
server that reads the new fields; older servers ignore them.
|
|
10
|
+
|
|
3
11
|
## 0.1.14 (2026-10-06)
|
|
4
12
|
|
|
5
13
|
- `deployangel plan` separates what clearance waits on ("Needed to clear")
|
data/README.md
CHANGED
|
@@ -207,6 +207,10 @@ and job still succeeds. Only drops are flagged, and a checkpoint without enough
|
|
|
207
207
|
traffic never blocks a release from being cleared. Checkpoints can also be part
|
|
208
208
|
of a critical flow (`checkpoint:order.created`).
|
|
209
209
|
|
|
210
|
+
The agent also records whether each checkpoint was counted during an HTTP
|
|
211
|
+
request or a background job, so DeployAngel compares it against the right
|
|
212
|
+
traffic: a checkpoint counted in jobs is judged against jobs, not requests.
|
|
213
|
+
|
|
210
214
|
It's safe to call anywhere: it never raises, never touches the network, and is
|
|
211
215
|
ignored outside reporting environments. Names use letters, numbers, and
|
|
212
216
|
`. _ : -` (up to 100 characters); keep them to a fixed set rather than
|
data/lib/deployangel/agent.rb
CHANGED
|
@@ -71,12 +71,14 @@ module DeployAngel
|
|
|
71
71
|
warn_once(:record_discard, "DeployAngel failed to record a discarded job: #{e.class}: #{e.message}")
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
# context defaults to the unit of work this fiber is running: an HTTP
|
|
75
|
+
# request, a job, or neither.
|
|
76
|
+
def record_checkpoint(name, count: 1, context: ExecutionContext.current)
|
|
75
77
|
return unless @active
|
|
76
78
|
|
|
77
79
|
after_fork! if Process.pid != @pid
|
|
78
80
|
start_reporter
|
|
79
|
-
@aggregator.record_checkpoint(name: name, count: count)
|
|
81
|
+
@aggregator.record_checkpoint(name: name, count: count, context: context)
|
|
80
82
|
rescue StandardError => e
|
|
81
83
|
warn_once(:record_checkpoint, "DeployAngel failed to record a checkpoint: #{e.class}: #{e.message}")
|
|
82
84
|
end
|
|
@@ -37,6 +37,20 @@ module DeployAngel
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# count: every recording; http and job: those recorded while handling an
|
|
41
|
+
# HTTP request or running a job (see ExecutionContext). The rest were
|
|
42
|
+
# recorded outside either.
|
|
43
|
+
CheckpointStats = Struct.new(:count, :http, :job) do
|
|
44
|
+
def record(count, context)
|
|
45
|
+
self.count += count
|
|
46
|
+
if context == ExecutionContext::HTTP
|
|
47
|
+
self.http += count
|
|
48
|
+
elsif context == ExecutionContext::JOB
|
|
49
|
+
self.job += count
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
40
54
|
class Period
|
|
41
55
|
attr_reader :started_at, :requests, :status_counts, :unhandled_exceptions, :histogram, :routes,
|
|
42
56
|
:jobs, :job_classes, :exceptions, :exceptions_truncated, :checkpoints
|
|
@@ -52,13 +66,13 @@ module DeployAngel
|
|
|
52
66
|
@job_classes = {}
|
|
53
67
|
@exceptions = {}
|
|
54
68
|
@exceptions_truncated = 0
|
|
55
|
-
@checkpoints =
|
|
69
|
+
@checkpoints = {}
|
|
56
70
|
end
|
|
57
71
|
|
|
58
72
|
# Up to 100 names per period; the rest are counted together.
|
|
59
|
-
def record_checkpoint(name, count)
|
|
73
|
+
def record_checkpoint(name, count, context)
|
|
60
74
|
key = @checkpoints.key?(name) || @checkpoints.size < MAX_CHECKPOINTS - 1 ? name : OTHER_ROUTE
|
|
61
|
-
@checkpoints[key]
|
|
75
|
+
(@checkpoints[key] ||= CheckpointStats.new(0, 0, 0)).record(count, context)
|
|
62
76
|
end
|
|
63
77
|
|
|
64
78
|
# Up to 20 fingerprints per period; the rest are only counted.
|
|
@@ -141,9 +155,11 @@ module DeployAngel
|
|
|
141
155
|
@mutex.synchronize { (@periods[started_at] ||= Period.new(started_at)).record_discard(job_class.to_s, @max_routes) }
|
|
142
156
|
end
|
|
143
157
|
|
|
144
|
-
|
|
158
|
+
# context: where it was recorded, ExecutionContext::HTTP, ExecutionContext::JOB,
|
|
159
|
+
# or nil for neither.
|
|
160
|
+
def record_checkpoint(name:, count: 1, context: nil)
|
|
145
161
|
started_at = period_start(@clock.call)
|
|
146
|
-
@mutex.synchronize { (@periods[started_at] ||= Period.new(started_at)).record_checkpoint(name, count) }
|
|
162
|
+
@mutex.synchronize { (@periods[started_at] ||= Period.new(started_at)).record_checkpoint(name, count, context) }
|
|
147
163
|
end
|
|
148
164
|
|
|
149
165
|
# A representative backtrace is kept only the first time this process
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DeployAngel
|
|
4
|
+
# The unit of work the current fiber is running: HTTP while the request
|
|
5
|
+
# middleware handles a request, JOB while job instrumentation runs a job,
|
|
6
|
+
# and nil otherwise (console, rake tasks, boot, a thread of the app's own).
|
|
7
|
+
# Checkpoints read it to say where they were recorded.
|
|
8
|
+
#
|
|
9
|
+
# Thread.current[] is fiber-local, like Rails' isolated execution state, so
|
|
10
|
+
# requests served concurrently in threads (Puma) or fibers (Falcon) each
|
|
11
|
+
# see their own. Every integration restores the previous value when its
|
|
12
|
+
# unit ends, even when it raises, so the innermost unit wins when they nest
|
|
13
|
+
# (a job performed inline during a request), and nothing leaks into the
|
|
14
|
+
# next request a reused thread serves.
|
|
15
|
+
module ExecutionContext
|
|
16
|
+
KEY = :__deployangel_execution_context
|
|
17
|
+
HTTP = :http
|
|
18
|
+
JOB = :job
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def current
|
|
23
|
+
Thread.current[KEY]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the previous context, to hand back to restore.
|
|
27
|
+
def enter(kind)
|
|
28
|
+
previous = Thread.current[KEY]
|
|
29
|
+
Thread.current[KEY] = kind
|
|
30
|
+
previous
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def restore(previous)
|
|
34
|
+
Thread.current[KEY] = previous
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -34,7 +34,11 @@ module DeployAngel
|
|
|
34
34
|
"exceptions_truncated" => period.exceptions_truncated,
|
|
35
35
|
"jobs" => job_stats(period.jobs),
|
|
36
36
|
"job_classes" => period.job_classes.map { |key, stats| { "key" => key }.merge(job_stats(stats)) },
|
|
37
|
-
|
|
37
|
+
# http and job are always sent, 0 when none, so the cloud can tell
|
|
38
|
+
# this agent from one that doesn't say where checkpoints happened.
|
|
39
|
+
"checkpoints" => period.checkpoints.map do |key, stats|
|
|
40
|
+
{ "key" => key, "count" => stats.count, "http" => stats.http, "job" => stats.job }
|
|
41
|
+
end
|
|
38
42
|
}
|
|
39
43
|
end
|
|
40
44
|
|
|
@@ -31,11 +31,16 @@ module DeployAngel
|
|
|
31
31
|
|
|
32
32
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
33
33
|
Redaction.request_host = env["HTTP_HOST"]
|
|
34
|
+
# Checkpoints recorded from here until the app returns count as
|
|
35
|
+
# recorded in a request.
|
|
36
|
+
previous_context = ExecutionContext.enter(ExecutionContext::HTTP)
|
|
34
37
|
begin
|
|
35
38
|
status, headers, body = @app.call(env)
|
|
36
39
|
rescue Exception => e # rubocop:disable Lint/RescueException -- recorded, then re-raised untouched
|
|
37
40
|
record(env, 500, started, unhandled: true, exception: e)
|
|
38
41
|
raise
|
|
42
|
+
ensure
|
|
43
|
+
ExecutionContext.restore(previous_context)
|
|
39
44
|
end
|
|
40
45
|
# A framework that renders an exception itself swallows it before it
|
|
41
46
|
# reaches this middleware, and renders some as 4xx; only those that
|
|
@@ -11,6 +11,34 @@ module DeployAngel
|
|
|
11
11
|
# perform finishes.
|
|
12
12
|
module ActiveJob
|
|
13
13
|
STATE = :@__deployangel_job_state
|
|
14
|
+
PREVIOUS_CONTEXT = :@__deployangel_previous_context
|
|
15
|
+
|
|
16
|
+
# Marks the fiber as running a job for as long as perform.active_job
|
|
17
|
+
# lasts, so checkpoints recorded by the job say so. ActiveSupport calls
|
|
18
|
+
# finish from an ensure, so the previous context (an HTTP request, for
|
|
19
|
+
# a job performed inline) comes back even when the job raises. It is
|
|
20
|
+
# kept on the job, so only a perform that started here restores.
|
|
21
|
+
module ContextListener
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def start(_name, _id, payload)
|
|
25
|
+
job = payload[:job]
|
|
26
|
+
return unless job && DeployAngel.recording?
|
|
27
|
+
|
|
28
|
+
job.instance_variable_set(PREVIOUS_CONTEXT, ExecutionContext.enter(ExecutionContext::JOB))
|
|
29
|
+
rescue StandardError
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def finish(_name, _id, payload)
|
|
34
|
+
job = payload[:job]
|
|
35
|
+
return unless job&.instance_variable_defined?(PREVIOUS_CONTEXT)
|
|
36
|
+
|
|
37
|
+
ExecutionContext.restore(job.remove_instance_variable(PREVIOUS_CONTEXT))
|
|
38
|
+
rescue StandardError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
end
|
|
14
42
|
|
|
15
43
|
module_function
|
|
16
44
|
|
|
@@ -23,6 +51,7 @@ module DeployAngel
|
|
|
23
51
|
subscribe("enqueue_retry.active_job") { |payload| state(payload).merge!(failed: true, error: payload[:error]) }
|
|
24
52
|
subscribe("discard.active_job") { |payload| state(payload).merge!(failed: true, discarded: true, error: payload[:error]) }
|
|
25
53
|
subscribe("retry_stopped.active_job") { |payload| state(payload).merge!(failed: true, discarded: true, error: payload[:error]) }
|
|
54
|
+
::ActiveSupport::Notifications.subscribe("perform.active_job", ContextListener)
|
|
26
55
|
::ActiveSupport::Notifications.subscribe("perform.active_job") { |event| record(event) }
|
|
27
56
|
end
|
|
28
57
|
|
data/lib/deployangel/sidekiq.rb
CHANGED
|
@@ -16,12 +16,15 @@ module DeployAngel
|
|
|
16
16
|
|
|
17
17
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
18
18
|
latency = DeployAngel::Sidekiq.queue_latency_ms(job)
|
|
19
|
+
previous_context = ExecutionContext.enter(ExecutionContext::JOB)
|
|
19
20
|
begin
|
|
20
21
|
yield
|
|
21
22
|
rescue Exception => e # rubocop:disable Lint/RescueException -- recorded, then re-raised untouched
|
|
22
23
|
DeployAngel.record_exception(e, source: "job_class:#{job["class"]}")
|
|
23
24
|
record(job, started, latency, failed: true)
|
|
24
25
|
raise
|
|
26
|
+
ensure
|
|
27
|
+
ExecutionContext.restore(previous_context)
|
|
25
28
|
end
|
|
26
29
|
record(job, started, latency, failed: false)
|
|
27
30
|
end
|
data/lib/deployangel/version.rb
CHANGED
data/lib/deployangel.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative "deployangel/core/release"
|
|
|
9
9
|
require_relative "deployangel/core/instance"
|
|
10
10
|
require_relative "deployangel/core/aggregator"
|
|
11
11
|
require_relative "deployangel/core/protocol"
|
|
12
|
+
require_relative "deployangel/core/execution_context"
|
|
12
13
|
require_relative "deployangel/core/buffer"
|
|
13
14
|
require_relative "deployangel/core/transport"
|
|
14
15
|
require_relative "deployangel/agent"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: deployangel
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jordan Owens
|
|
@@ -38,6 +38,7 @@ files:
|
|
|
38
38
|
- lib/deployangel/configuration.rb
|
|
39
39
|
- lib/deployangel/core/aggregator.rb
|
|
40
40
|
- lib/deployangel/core/buffer.rb
|
|
41
|
+
- lib/deployangel/core/execution_context.rb
|
|
41
42
|
- lib/deployangel/core/fingerprint.rb
|
|
42
43
|
- lib/deployangel/core/histogram.rb
|
|
43
44
|
- lib/deployangel/core/instance.rb
|