Skip to content

Commit a50c61f

Browse files
lewispbdjmb
andcommitted
Add output logging framework with OTel and file backends
Introduce a pluggable output logging system that captures deploy output and ships it to configurable destinations via a new `output:` section in deploy.yml. output: otel: endpoint: http://otel-gateway:4318 file: path: /var/log/kamal/ OTel backend ships OTLP log records via HTTP to /v1/logs. Structured lifecycle events (kamal.start/complete/failed) mark command boundaries with deployment metadata. Stream output lines are tagged per-host (server.address) and by stream type (log.iostream). Format follows OTel semantic conventions: standard attributes where applicable (deployment.*, exception.*, service.*), custom attributes namespaced under kamal.*. File backend writes one timestamped log file per command (e.g. 20260327_143022_production_deploy.log) with a completion/failure summary line at the end. Key design decisions: - `modify` method replaces `with_lock` as the integration point for all infrastructure-modifying commands, scoping output capture and instrumenting deploy lifecycle via ActiveSupport::Notifications - SSHKit output captured via custom Formatter with thread-local context for per-host and stream type tagging - Best-effort: output logger failures never block or fail a deploy Co-Authored-By: Donal McBreen <donal@37signals.com>
1 parent 453d8d7 commit a50c61f

29 files changed

+1356
-92
lines changed

lib/kamal/cli/accessory.rb

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
class Kamal::Cli::Accessory < Kamal::Cli::Base
55
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
66
def boot(name, prepare: true)
7-
with_lock do
7+
modify(lock: true) do
88
if name == "all"
99
KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
1010
else
@@ -42,7 +42,7 @@ def boot(name, prepare: true)
4242

4343
desc "upload [NAME]", "Upload accessory files to host", hide: true
4444
def upload(name)
45-
with_lock do
45+
modify(lock: true) do
4646
with_accessory(name) do |accessory, hosts|
4747
on(hosts) do
4848
accessory.files.each do |(local, config)|
@@ -61,7 +61,7 @@ def upload(name)
6161

6262
desc "directories [NAME]", "Create accessory directories on host", hide: true
6363
def directories(name)
64-
with_lock do
64+
modify(lock: true) do
6565
with_accessory(name) do |accessory, hosts|
6666
on(hosts) do
6767
accessory.directories.each do |(local, config)|
@@ -76,7 +76,7 @@ def directories(name)
7676

7777
desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container; use NAME=all to boot all accessories)"
7878
def reboot(name)
79-
with_lock do
79+
modify(lock: true) do
8080
if name == "all"
8181
KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
8282
else
@@ -91,7 +91,7 @@ def reboot(name)
9191

9292
desc "start [NAME]", "Start existing accessory container on host"
9393
def start(name)
94-
with_lock do
94+
modify(lock: true) do
9595
with_accessory(name) do |accessory, hosts|
9696
on(hosts) do
9797
execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
@@ -107,7 +107,7 @@ def start(name)
107107

108108
desc "stop [NAME]", "Stop existing accessory container on host"
109109
def stop(name)
110-
with_lock do
110+
modify(lock: true) do
111111
with_accessory(name) do |accessory, hosts|
112112
on(hosts) do
113113
execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
@@ -124,7 +124,7 @@ def stop(name)
124124

125125
desc "restart [NAME]", "Restart existing accessory container on host"
126126
def restart(name)
127-
with_lock do
127+
modify(lock: true) do
128128
stop(name)
129129
start(name)
130130
end
@@ -213,7 +213,7 @@ def logs(name)
213213

214214
desc "pull_image [NAME]", "Pull accessory image on host", hide: true
215215
def pull_image(name)
216-
with_lock do
216+
modify(lock: true) do
217217
with_accessory(name) do |accessory, hosts|
218218
on(hosts) do
219219
execute *KAMAL.auditor.record("Pull #{name} accessory image"), verbosity: :debug
@@ -227,7 +227,7 @@ def pull_image(name)
227227
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
228228
def remove(name)
229229
confirming "This will remove all containers, images and data directories for #{name}. Are you sure?" do
230-
with_lock do
230+
modify(lock: true) do
231231
if name == "all"
232232
KAMAL.accessory_names.each { |accessory_name| remove_accessory(accessory_name) }
233233
else
@@ -239,7 +239,7 @@ def remove(name)
239239

240240
desc "remove_container [NAME]", "Remove accessory container from host", hide: true
241241
def remove_container(name)
242-
with_lock do
242+
modify(lock: true) do
243243
with_accessory(name) do |accessory, hosts|
244244
on(hosts) do
245245
execute *KAMAL.auditor.record("Remove #{name} accessory container"), verbosity: :debug
@@ -251,7 +251,7 @@ def remove_container(name)
251251

252252
desc "remove_image [NAME]", "Remove accessory image from host", hide: true
253253
def remove_image(name)
254-
with_lock do
254+
modify(lock: true) do
255255
with_accessory(name) do |accessory, hosts|
256256
on(hosts) do
257257
execute *KAMAL.auditor.record("Removed #{name} accessory image"), verbosity: :debug
@@ -263,7 +263,7 @@ def remove_image(name)
263263

264264
desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
265265
def remove_service_directory(name)
266-
with_lock do
266+
modify(lock: true) do
267267
with_accessory(name) do |accessory, hosts|
268268
on(hosts) do
269269
execute *accessory.remove_service_directory
@@ -277,7 +277,7 @@ def remove_service_directory(name)
277277
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
278278
def upgrade(name)
279279
confirming "This will restart all accessories" do
280-
with_lock do
280+
modify(lock: true) do
281281
host_groups = options[:rolling] ? KAMAL.accessory_hosts : [ KAMAL.accessory_hosts ]
282282
host_groups.each do |hosts|
283283
host_list = Array(hosts).join(",")

lib/kamal/cli/app.rb

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
class Kamal::Cli::App < Kamal::Cli::Base
22
desc "boot", "Boot app on servers (or reboot app if already running)"
33
def boot
4-
with_lock do
4+
modify(lock: true) do
55
say "Get most recent version available as an image...", :magenta unless options[:version]
66
using_version(version_or_latest) do |version|
77
say "Start container with version #{version} (or reboot if already running)...", :magenta
@@ -42,7 +42,7 @@ def boot
4242

4343
desc "start", "Start existing app container on servers"
4444
def start
45-
with_lock do
45+
modify(lock: true) do
4646
on_roles(KAMAL.roles, hosts: KAMAL.app_hosts, parallel: KAMAL.config.boot.parallel_roles) do |host, role|
4747
app = KAMAL.app(role: role, host: host)
4848
execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
@@ -61,7 +61,7 @@ def start
6161

6262
desc "stop", "Stop app container on servers"
6363
def stop
64-
with_lock do
64+
modify(lock: true) do
6565
on_roles(KAMAL.roles, hosts: KAMAL.app_hosts, parallel: KAMAL.config.boot.parallel_roles) do |host, role|
6666
app = KAMAL.app(role: role, host: host)
6767
execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
@@ -233,7 +233,7 @@ def logs
233233

234234
desc "remove", "Remove app containers and images from servers"
235235
def remove
236-
with_lock do
236+
modify(lock: true) do
237237
stop
238238
remove_containers
239239
remove_images
@@ -243,7 +243,7 @@ def remove
243243

244244
desc "live", "Set the app to live mode"
245245
def live
246-
with_lock do
246+
modify(lock: true) do
247247
on_roles(KAMAL.roles, hosts: KAMAL.proxy_hosts) do |host, role|
248248
execute *KAMAL.app(role: role, host: host).live if role.running_proxy?
249249
end
@@ -256,7 +256,7 @@ def live
256256
def maintenance
257257
maintenance_options = { drain_timeout: options[:drain_timeout] || KAMAL.config.drain_timeout, message: options[:message] }
258258

259-
with_lock do
259+
modify(lock: true) do
260260
on_roles(KAMAL.roles, hosts: KAMAL.proxy_hosts) do |host, role|
261261
execute *KAMAL.app(role: role, host: host).maintenance(**maintenance_options) if role.running_proxy?
262262
end
@@ -265,7 +265,7 @@ def maintenance
265265

266266
desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
267267
def remove_container(version)
268-
with_lock do
268+
modify(lock: true) do
269269
on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
270270
execute *KAMAL.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
271271
execute *KAMAL.app(role: role, host: host).remove_container(version: version)
@@ -275,7 +275,7 @@ def remove_container(version)
275275

276276
desc "remove_containers", "Remove all app containers from servers", hide: true
277277
def remove_containers
278-
with_lock do
278+
modify(lock: true) do
279279
on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
280280
execute *KAMAL.auditor.record("Removed all app containers", role: role), verbosity: :debug
281281
execute *KAMAL.app(role: role, host: host).remove_containers
@@ -285,7 +285,7 @@ def remove_containers
285285

286286
desc "remove_images", "Remove all app images from servers", hide: true
287287
def remove_images
288-
with_lock do
288+
modify(lock: true) do
289289
on(hosts_removing_all_roles) do
290290
execute *KAMAL.auditor.record("Removed all app images"), verbosity: :debug
291291
execute *KAMAL.app.remove_images
@@ -295,7 +295,7 @@ def remove_images
295295

296296
desc "remove_app_directories", "Remove the app directories from servers", hide: true
297297
def remove_app_directories
298-
with_lock do
298+
modify(lock: true) do
299299
on(hosts_removing_all_roles) do |host|
300300
execute *KAMAL.server.remove_app_directory, raise_on_non_zero_exit: false
301301
execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory}"), verbosity: :debug
@@ -347,7 +347,7 @@ def version_or_latest
347347

348348
def with_lock_if_stopping
349349
if options[:stop]
350-
with_lock { yield }
350+
modify(lock: true) { yield }
351351
else
352352
yield
353353
end

lib/kamal/cli/base.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ def print_runtime
7272
puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
7373
end
7474

75+
def modify(lock: false)
76+
KAMAL.modify(command: command, subcommand: subcommand) do
77+
lock ? with_lock { yield } : yield
78+
end
79+
end
80+
81+
def say(message = "", *)
82+
super
83+
KAMAL.log(message.to_s)
84+
end
85+
7586
def with_lock
7687
if KAMAL.holding_lock?
7788
yield

0 commit comments

Comments
 (0)