2019年4月23日火曜日

ruby の puts が docker logs でリアルタイムに表示されないときの対応

概要

ruby の puts でデバッグしているとコンテナが停止しないとログが表示されない現象が発生することがあります
今回はその対処方法を紹介します

環境

  • macOS 10.14.4
  • Ruby 2.6.2p47
  • docker 18.09.2

サンプルコード

Ruby は以下のような感じとします

puts "start"
sleep 10
puts "hoge"
sleep 10
puts "fuga"
sleep 10
puts "end"

本来なら 10 秒おきにそれぞれの puts 情報が表示されてほしいのですが docker logs -f などで見るとそうなりません
ちなみに Dockerfile は以下の通りです

FROM ruby:latest

ADD . /home
WORKDIR /home

CMD ["ruby", "app.rb"]

対処方法

docker では有名ですが /proc/1/fd/1 を使います
docker では CMD で指定したコマンドは基本的には 1 番になります (意図的に変更しない限り)
/proc/1/fd/1 はプロセス 1 番のファイルディスクリプタで標準出力へのリダイレクトになっています
このファイルに直接書き込むことで標準出力を強制してさせます

$stdout = IO.new(IO.sysopen("/proc/1/fd/1", "w"), "w")
$stdout.sync = true
STDOUT = $stdout

puts "start"
sleep 10
puts "hoge"
sleep 10
puts "fuga"
sleep 10
puts "end"

これでイメージを作成し直し run してみるとちゃんとリアルタイムに logs で確認できると思います
app.rb:3: warning: already initialized constant STDOUT が表示されてしまいますが不要な場合は -W0ruby 実行時に指定すれば OK です

CMD ["ruby", "-W0", "app.rb"]

logger でも同じように対処できる

logger を使ってロギングしている場合も同様の現象になると思います
その場合は以下のようにしましょう

require 'logger'
logger = Logger.new(STDOUT)

logger.info "start"
sleep 10
logger.info "hoge"
sleep 10
logger.info "fuga"
sleep 10
logger.info "end"

これだと表示されないので以下のようにします

require 'logger'
$stdout = IO.new(IO.sysopen("/proc/1/fd/1", "w"),"w")
$stdout.sync = true
STDOUT = $stdout
logger = Logger.new(STDOUT)

logger.info "start"
sleep 10
logger.info "hoge"
sleep 10
logger.info "fuga"
sleep 10
logger.info "end"

これで表示されるようになると思います

Exception も考慮しなくていいのか

例えば以下のような感じだとエラーメッセージの部分は表示されます

$stdout = IO.new(IO.sysopen("/proc/1/fd/1", "w"),"w")
$stdout.sync = true
STDOUT = $stdout
$stderr = IO.new(IO.sysopen("/proc/1/fd/1", "w"),"w")
$stderr.sync = true
STDERR = $stderr

puts "start"
sleep 1
puts "hoge"
sleep 1
puts "fuga"
begin
  5/0
rescue => e
  puts e.full_message
end
sleep 1
puts "end"

puts e.full_message の部分だけですが以下のようにちゃんと logs でもリアルタイムに確認できます

app.rb:14:in `/': divided by 0 (ZeroDivisionError)
        from app.rb:14:in `<main>'

普通に ruby を実行したときと違うのは「Traceback (most recent call last):」が表示されない部分だけかなと思います
標準エラーも操作してみましたが結果は変わらなかったので STDOUT だけで十分なのかなと思います

$stderr = IO.new(IO.sysopen("/proc/1/fd/2", "w"),"w")
$stderr.sync = true
STDERR = $stderr

最後に

なぜか ruby の puts が docker logs -f でリアルタイムに表示されなかったのでその対処をしてみました
ファイルディスクリプタを直接操作することで解決しましたがちょっと気持ち悪い感じはします
docker のロギングドライバを変えるという方法はありかもしれませんがそもそも docker がログを拾えていないとドライバを変えても意味がないので結局ファイルディスクリプタを操作しなければいけないかもしれません

他の言語だとこういう現象はあまり遭遇しないので Ruby のイメージが原因なのかもしれません
ちなみに ruby の docker イメージバージョンは 2.6.3p62 なのでだいぶ最新を使いました

$ docker run -it --rm ruby ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]

参考サイト

0 件のコメント:

コメントを投稿