おどるダメ人間

Web系の技術のこととか、そうじゃないこととか、思いついたことをメモする。

【Rails】バッチの実行ログを専用のログファイルと標準出力の両方に出す

目的

以下のようなことがやりたかったので、その実現方法のメモです。

  • cronで実行されるrakeタスクの実行ログを、そのバッチ専用のログファイルに出力
  • 手動でバッチ実行したときのために、標準出力にも同じ内容を出力
  • 予期せぬ例外が発生したときのエラー内容もログファイルに記録

1. バッチ専用のログファイルに出力

単純にRails.loggerを使うと、Controllerなどが出力するアプリケーションログもバッチのログもすべて同じファイルに出力されてしまいます(log/production.logなど)。 それらを区別してバッチの実行ログを確認しやすいようにするため、個別のログファイルに出力します。

ファイル名の指定

以下でlog/batch.logに出力されます。

logger = ActiveSupport::Logger.new(Rails.root.join("log/batch.log"))
logger.info "バッチのログだよーん" # => log/batch.logに書き込まれる

ローテートの設定

さらに、Logger.newの第2,3引数を指定すると、ログローテートの設定ができます。 ファイルサイズが膨大になる前に、ファイルを自動的に分割してくれます。

サイズ指定で分割

以下は、10MBを超えたらファイルを分割し、5ファイルを超えたら古いファイルを削除する、という設定。 batch.log.0、batch.log.1、というファイル名になります。

ActiveSupport::Logger.new("log/batch.log", 5, 10.megabytes)
日数で分割

'daily', 'weekly', 'monthly'を指定すると、1日 or 1週間 or 1ヶ月の間隔でファイルを更新します。 batch.log.20141101、とか、batch.log.201411、などのファイル名になります。

# 1日で分割
ActiveSupport::Logger.new("log/batch.log", "daily")
# 1週間で分割
ActiveSupport::Logger.new("log/batch.log", "weekly")
# 1ヶ月で分割
ActiveSupport::Logger.new("log/batch.log", "monthly")

フォーマットの指定

Logger.newしたログはメッセージのみを出力する味気ないものになってます。 以下でフォーマットをつけましょう。

logger.formatter = ::Logger::Formatter.new
logger.info "バッチのログだよーん"

Formatterのデフォルトの形式に則って、以下のようなログが出ます。

I, [2014-11-15T18:48:18.763841 #21583]  INFO -- : バッチのログだよーん

2. 標準出力にも同じ内容を出力する

開発中などは、バッチ実行したときにログファイルだけに出力されるだとけっこう不便です。 なので、同時に標準出力にも出力されるようにします。

標準出力にログを出す場合は、Logger.newにファイル名ではなくSTDOUTを指定します。 標準エラー出力の場合はSTDERRですね。

2つのLoggerに同時に出力させるには、ActiveSupport::Logger.broadcastメソッドを利用します。

logger = ActiveSupport::Logger.new("log/batch.log")
console = ActiveSupport::Logger.new(STDOUT)

logger.extend ActiveSupport::Logger.broadcast(console)

logger.info "バッチのログだよーん" # => batch.log と 標準出力の両方に出力

broadcastの実装は以下を参照。 2つのLoggerに同じメソッド呼び出しをするModuleを生成していますね。

rails/logger.rb at master · rails/rails

これを、バッチの先頭でRails.loggerに上書いてしまえば、バッチ実行中に呼び出されるRails.loggerはすべて指定したログファイルと標準出力に出るので、便利です。

# ログレベルはアプリケーションのものを継承するとよいかも
logger.level = Rails.logger.level

Rails.logger = logger

3. エラー内容をログファイルに出力

本筋とは少しずれておまけみたいになります。 バッチ実行中に予期せぬ例外が発生した場合は、そのエラー内容がログファイルに書きだされていると便利です。 これに関してはうまいやり方がわからなかったので、とりあえずcron実行のときには標準エラー出力の内容をログファイルに追記するようにします。

crontabに2>>log/cron.logをつければいいのですが、cronの登録にはwheneverを使うので、wheneverでのこれの付け方。

schedule.rbで以下のように記述します。

every 1.hour do
  rake "batch", output: { error: "log/cron.log" }
end

ちなみに、output: { standard: "log/cron.log" }1>>log/cron.logがつきます。

参考: Output redirection aka logging your cron jobs · javan/whenever Wiki

ちょっと便利なActiveSupportの標準クラス拡張(Array & Hash編)

RailsActiveSupportにはcore_extというのがあって、標準クラスを拡張して便利なメソッドを追加してます。 Railsを使ってると実は結構いろんなところでお世話になっていて、たまに素のrubyを書くと「あれ、これ使えないのか」と戸惑ったりしますね。

一度俯瞰で把握しとくのもいいかなと思ってまとめてみました。 といっても多すぎて途中で力尽きたので、とりあえずよく使われそうなArrayとHashについて、適当に便利そうなのを見繕ってます。

環境は、Rails 4.1.1、ruby 2.0.0

Array

参考: rails/activesupport/lib/active_support/core_ext/array at master · rails/rails

from と to

%w( a b c d ).from(0)  # => ["a", "b", "c", "d"]
%w( a b c d ).from(2)  # => ["c", "d"]

%w( a b c d ).to(0)  # => ["a"]
%w( a b c d ).to(2)  # => ["a", "b", "c"]

second 〜 fifth(+おまけ?)

firstは標準Arrayにもあるんですが、ActiveSupportでは2〜5個目までとれるみたいです。

%w( a b c d e ).second # => "b"
%w( a b c d e ).third  # => "c"
%w( a b c d e ).fourth # => "d"
%w( a b c d e ).fifth  # => "e"

ついでに、なぜか42個目もとれるという謎のforty_twoメソッドも…。

(1..42).to_a.forty_two # => 42

なぜ42なのかの答えは、これ? → 人生、宇宙、すべての答え - Google 検索

to_sentence

おもしろいけど使い道ありそうでなさそうな…。 配列を文章っぽくつなげてくれます。オプションやlocaleの設定でつなげる文字を返ることもできる。

%w(one two).to_sentence          # => "one and two"
%w(one two three).to_sentence # => "one, two, and three"
%w(one two three four).to_sentence(words_connector: ' & ', last_word_connector: ' &... ')
 # => "one & two & three &... four"

append と prepend

<<unshift をちょっと人に優しくしたエイリアス

[1, 2, 3].append(4) # => [1, 2, 3, 4]
[1, 2, 3].prepend(4) # => [4, 1, 2, 3]

グルーピング系

配列をいくつかの小さい配列に分割したり、複数の要素を同時に繰り返し処理するメソッドたち。 用途としては、例えばviewで3個ずつ横並びにするときなどに使えそう。

in_groups_of

指定した数が要素数になるような配列に分割。 あまったときに埋める値を第2引数で指定できる。デフォルトはnil

%w(1 2 3 4 5).in_groups_of(3) {|group| p group}
["1", "2", "3"]
["4", "5", nil]
in_groups

素数が均等な配列が、指定した数だけできるように分割。 最大長になるように埋める値を第2引数で指定できる。デフォルトはnil

%w(1 2 3 4 5).in_groups(3) {|group| p group}
["1", "2"]
["3", "4"]
["5", nil]
split

指定された要素を境にして複数の配列に分割。 文字列のsplitと同じイメージ。

(1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]

Hash

参考: rails/activesupport/lib/active_support/core_ext/hash at master · rails/rails

compact と compact!

ArrayのcompactをHashにも。valueがnilのものを除きます。

deep_merge と deep_merge!

多次元ハッシュにも使えるmerge

h1 = { x: { y: [4, 5, 6] }, z: [7, 8, 9] }
h2 = { x: { y: [7, 8, 9] }, z: 'xyz' }

h1.deep_merge(h2) # => {x: {y: [7, 8, 9]}, z: "xyz"}
h2.deep_merge(h1) # => {x: {y: [4, 5, 6]}, z: [7, 8, 9]}

except と except!

keyを指定してそれを除外したHashを返す。

{ x: 'x', y: 'y', z: 'z' }.except(:y) # => {:x=>"x", :z=>"z"}
{ x: 'x', y: 'y', z: 'z' }.except(:y, :z) # => {:x=>"x"}

with_indifferent_access

なにげにありがたいメソッドActiveSupport::HashWithIndifferentAccessを生成して返します。 これは、keyに文字列・シンボルのどちらでも同じようにアクセスできるHashです。 Rails使ってるとControllerのparamとかでこいつの存在を感じます。 自分で作ったHashオブジェクトにRails風味の統一感を出したいときに使いましょう。

h1 = { a: 'aa' }
h1[:a] # => "aa"
h1['a'] # => nil

h2 = h1.with_indifferent_access
h2[:a] # => "aa"
h2['a'] # => "aa"

key変換系

transform_keys

keyを一括変換する。

hash = { name: 'Rob', age: '28' }
hash.transform_keys{ |key| key.to_s.upcase } # => {"NAME"=>"Rob", "AGE"=>"28"}
stringify_keys と symbolize_keys

keyを一括で文字列化 or シンボル化。

hash = { name: 'Rob', age: '28' }
hash.stringify_keys! # => {"name"=>"Rob", "age"=>"28"}
hash.symbolize_keys! # => {:name=>"Rob", :age=>"28"}

transform_keysstringify_keyssymbolize_keysにはそれぞれdeep_付きで多次元Hashにも対応できます。

assert_valid_keys

すべてのkeyが有効なものかチェックする。 有効でないkeyが含まれていたらArgumentErrorの例外。

{ name: 'Rob', years: '28' }.assert_valid_keys(:name, :age)
 # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
{ name: 'Rob', age: '28' }.assert_valid_keys(:name, :age)
 # => passes, raises nothing

slice と slice!

指定したkey以外のkeyを除外したHashを生成する。 これも何かと便利なメソッドですが、sliceslice! で返り値が真逆になるので少しだけ注意が必要。

hash = { a: 1, b: 2, c: 3 }
hash.slice(:a) # => {:a=>1}  返り値は指定したkeyのみを含むHash
hash # => {:a=>1, :b=>2, :c=>3} 非破壊なので元の変数は元のまま。


hash.slice!(:a) # => {:b=>2, :c=>3}  返り値は指定したkey以外のみを含むHash(除外される方)
hash # => {:a=>1} 破壊的なので元の変数が指定したkeyのみを含むHashに。

ちなみに、extract!slice!と逆のことができますが、extractはありません。なんでだろ…?

感想

日頃意識していなかったRails様の優しい心遣いに触れて胸が熱くなっています。

けどまとめるの意外と面倒だったのでもう他はやらないだろうなあ。 dateとかstringとかちょっと整理しときたい気もするなあ。 死ぬほど暇だったらやってもいいかなあ。

参考リンクの保存に便利なMarkdown形式のリンクを表示するブックマークレット

何か調べ物したときに参考リンクを保存するのに、ブラウザのブックマークって使いにくいなあといつも思います。テーマ毎にディレクトリ分けてると手間がかかるしディレクトリが大量にできてしまって探しづらい。

結局、調べた内容に関するメモと一緒に参考にしたサイトのURLを貼り付けるのが一番いいかなと。

普段、調べたことや作業記録などをとにかくメモするのにKobitoを使ってます。

Kobito - プログラミングのメモやスニペットの記録に最適なMacアプリケーション

で、参考リンクを貼るときにMarkdown形式で、プレビュー画面が見やすくなります。

上のKobitoのリンクは、Markdownだとこう↓

[Kobito - プログラミングのメモやスニペットの記録に最適なMacアプリケーション](http://kobito.qiita.com/ja)

これをいちいち作るのが面倒臭かったので、ブックマークレットにして使ってます。

以下をブックマークに登録して、保存したいwebページ開いて実行すれば[ページタイトル](URL)の形式を表示します。

javascript:(function(){if(document.getElementById("mdlinkview")==null){var%20text="["+document.title+"]("+document.URL+")";var%20elem=document.createElement("textarea");elem.id="mdlinkview";elem.innerText=text;elem.textContent=text;var%20style="position:fixed%20!important;position:absolute;top:0;left:0;width:100%;height:20px;z-index:999999;font-size:20px;line-height:20px;";elem.setAttribute("style",style);var%20body=document.getElementsByTagName("body").item(0);body.appendChild(elem);var%20el=document.getElementById("mdlinkview");el.addEventListener('click',function(){el.focus();el.select();},false);el.addEventListener('blur',function(){el.remove();},false);el.click();}})()

かなーり適当なのでブラウザによって動かないこともあるかも。

恥ずかしながらブログを始める

文章を書かなすぎてアウトプット不全障害になりそうなので、とにかく何か書き出していきます。