モンモンブログ

技術的な話など

ActionMailerのメール本文がログに出力されるのを抑制

メール本文がログに出力されるとログ容量を圧迫するので(あとウザいので)出力を抑制しました。

環境

$ ruby -v
ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin13]
$ rails -v
Rails 4.2.5

事象

メーラーでメールを送信すると、デフォルトでメール本文がログに記録されます。

...
  Rendered notifier/order_received.jp.html.erb within layouts/mailer (3.9ms)
  Rendered notifier/order_received.jp.text.erb within layouts/mailer (31.2ms)
Notifier#order_received: processed outbound mail in 69.6ms
Sent mail to hoehoe@hoe.com (148.1ms)
Date: Thu, 28 Jan 2016 04:25:52 +0000
From: noreply@skimatalk.com
To: hoehoe@hoe.com
Message-ID: <56a99850628a8_e3fe3d690d13056190@812fa40a-c82a-4968-83bf-a9b4df09e732.mail>
Subject: hoehoe
Mime-Version: 1.0
Content-Type: multipart/alternative;
 boundary="--==_mimepart_56a998505d1bb_e3fe3d690d130560ea";
 charset=UTF-8
Content-Transfer-Encoding: 7bit
----==_mimepart_56a998505d1bb_e3fe3d690d130560ea
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: base64
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
...

例では XXXX... で表現してますが、base64 エンコードされたメール本文がこのように長々と出力されます。こんなのログに残っても対して役に立たない上にうっとうしいです。

解決策1

メーラーのロガーを nil に設定するだけでもOK。

environments/production.rb

    config.action_mailer.logger = nil

ただしこれだと、メーラーのログが一切出なくなり、それもちょっとどうかなあという感じ。(メールのテンプレートレンダリングのログは残ります。先ほどの例でいうと Rendered notifier/order_received.jp.html.erb within layouts/mailer (3.9ms) などの行。ERB のレンダリングメーラーの仕事じゃないからででしょうか。)

解決策2

メーラーのロガーを設定して、ロガーのログレベルを INFO レベルにしてやります。

environments/production.rb

    config.action_mailer.logger = Logger.new(config.paths["log"].first)
    config.action_mailer.logger.level = Logger::INFO

ログはこんな感じになりました。

...
  Rendered admin_mailer/test.jp.text.erb (0.9ms)
  Rendered admin_mailer/test.jp.html.erb (1.7ms)
I, [2016-01-28T07:14:51.945455 #17]  INFO -- :
Completed 302 Found in 670ms (ActiveRecord: 0.0ms)
Sent mail to hoehoe@hoe.com (367.3ms) ←
...

Sent mail to hoehoe@hoe.com (367.3ms) というメーラーのログは残しつつ、うっとうしいメール本文は消すことが出来ました。

ActiveRecordのscope,validatorでの意図せぬキャッシュに要注意

クリティカルなバグの原因になりえます(なりました)。

環境

少々古いです。activerecord の最近のバージョンでは未確認です。

ruby 1.9.3p547 (2014-05-14 revision 45962) [x86_64-darwin13.3.0]
rails-3.2.11
activerecord-3.2.11
MacOSX Mavericks

現象

ActieRecord の named scope の条件文や、 validator のエラーメッセージなどは自動でキャッシュされちゃいます。そのため、

  scope :upcoming, where('start_at > ?', Time.now)

のように scope の where 句で時刻比較したり、

  validates :start_at, presence: { message: I18n.t(:start_at_is_required) }

のように validation のエラーメッセージを I18n で国際化したりすると、1回目に実行した時点での時刻やロケール文字列がキャッシュされてしまい、

  • 時間が経過しても古い時刻での検索結果が返されたり
  • 日本語ロケールを指定してるのに英語でメッセージが出力されたり

しちゃいます。怖いですね。

対策

Proc で囲むことで回避できます。lambda でも可。

named scope ならこう。

  # クエリ全体を Proc.new で囲む
  scope :upcoming, Proc.new {
    where('start_at > ?', Time.now)
  }

validator ならこう。

  # I18n 呼んでる部分を Proc.new で囲む
  validates :start_at, presence: { message: Proc.new{ I18n.t(:start_at_is_required) } }
end

named scope から別の named scope を使ってる場合は更に注意が必要です。

時刻比較等の動的な操作を行う named scope (scope A) を、別の named scope (scope B) から使う場合、scope A のみならず scope B も Proc.new で囲む必要があるんです。

  # (scope A) 時刻比較する "upcoming" scope。Proc.new で囲んで対策済み
  scope :upcoming, Proc.new {
    where('start_at > ?', Time.now)
  }

  # (scope B) "upcoming" scope を利用。Proc.new で囲んでない
  scope :available, upcoming.where(status: 1)

これはアウト。"available" scope の評価結果がキャッシュされ、時刻が変化しません。

  # (scope A) こちらを Proc.new で囲む必要があるのはもちろん、
  scope :upcoming, Proc.new {
    where('start_at > ?', Time.now)
  }

  # (scope B) "upcoming" scope を利用するこちらも Proc.new で囲まないといけない
  scope :available, Proc.new {
    upcoming.where(state: :open)
  }

両方 Proc.new で囲みました。これでセーフ。

気をつけましょう。

Action Mailerのマルチパートメールでファイル形式の優先順位が変わっちゃう件

Action Mailer でマルチパートメールを送るようにしていて、かつ mail メソッドにブロックを渡す場合は注意が必要です。html 版、text 版の優先順位が意図せず変わってしまう場合があります。

環境

MacOSX Mavericks
ruby 1.9.3p547 (2014-05-14 revision 45962) [x86_64-darwin13.3.0]
rails-3.2.11
actionmailer-3.2.11

そもそもマルチパートメールって?

1通のメール中に複数の形式のメール本文を含められる機能です。

例えば html 形式と text 形式、両方のメール本文を含めれば、html 対応のメール表示ソフトではリッチな html 形式で表示させ、そうでなければ text 形式で表示させる、てことが可能です。

マルチパートメール on Rails

rails でマルチパートメールを送るのは超簡単。mailer のテンプレートとして複数の形式のファイルを用意しておくだけです。

app/views/user_mailer/password_reset.html.erb    # html版
app/views/user_mailer/password_reset.text.erb   # text版

あとは普通にメールを送信するだけ。

UserMailer.password_reset(user).deliver

送信されるメールの内容はこんな感じ。1本のメールに複数の形式が含まれてるのが分かると思います。

Sent mail to xxxx@skimatalk.com (1786ms)         # メールヘッダ
Date: Mon, 02 Feb 2015 11:09:00 +0900
From: noreply@skimatalk.com
To: xxxx@skimatalk.com
Message-ID: <xxxx>
Subject: xxxx
Mime-Version: 1.0
Content-Type: multipart/alternative;            # Content Type でマルチパート指定
 boundary="--==_mimepart_54cedc31b283f";        # 各パートの境界となる文字列を定義
 charset=UTF-8
Content-Transfer-Encoding: 7bit



----==_mimepart_54cedc31b283f                   # パート境界1 (text版ここから)
Date: Mon, 02 Feb 2015 11:09:00 +0900           # text版メールヘッダ
Mime-Version: 1.0
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: base64
Content-ID: <xxxx>

44G744GS44G744GS44G744GS44G744GS44G744GS44G7    # text版メール本文 (base64 encoded)
44GS44G744GS44G744GS44G744GS44G744GS44G744GS
44G744GS44G744GS44G744GS44G744GS44G744GS44G7
44GS44G744GS44G744GS44G744GS44G744GS44G744GS

----==_mimepart_54cedc31b283f                   # パート境界2 (html版ここから)
Date: Mon, 02 Feb 2015 11:09:00 +0900           # html版メールヘッダ
Mime-Version: 1.0
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: base64
Content-ID: <xxxx>

44G744GS44G744GS44G744GS44G744GS44G744GS44G7    # html版メール本文 (base64 encoded)
44GS44G744GS44G744GS44G744GS44G744GS44G744GS
44G744GS44G744GS44G744GS44G744GS44G744GS44G7
44GS44G744GS44G744GS44G744GS44G744GS44G744GS

----==_mimepart_54cedc31b283f--                 # パート境界3 (メール末尾)

各形式の優先順位は ActionMailer::Basedefault メソッドparts_order の値をセットすることで指定出来ます。この値はデフォルトで ["text/plain", "text/enriched", "text/html"] となってます(参考)。各形式の内容はこの順番で出力されます(先ほどの例も然り)。

class UserMailer < ActionMailer::Base
  default parts_order: ["text/plain", "text/enriched", "text/html"]
  
  def password_reset(user)
    ...
  end
end

この順番だと、一番上に出力される text/plain 形式の内容が最優先で表示されそうですが、違います。 RFCに、表示の際の優先順位は逆順とするように定められており、この場合、一番下の text/html が最優先となります。

この理由もRFCにありました。もっともシンプルな形式を一番上に出力するようにしておけば、MIME非対応のメール表示ソフトであっても読みやすいよね、ということのようです。なるほどちゃん。

placing the plainest alternative first is the friendliest possible option when mutlipart/alternative entities are viewed using a non-MIME- compliant mail reader.

mail メソッドにブロックを渡すと parts_order 指定が無視されちゃう

mail メソッドにはブロックを渡すことが出来ます。下記のように、ファイル形式ごとにレイアウトを指定出来たりします。

class UserMailer < ActionMailer::Base
  default parts_order: ["text/plain", "text/enriched", "text/html"]
  
  def password_reset(user)
    mail to: 'xx@skimatalk.com', subject: 'hello!' do |format|
      format.text { render layout: 'mailer' }
      format.html { render layout: 'mailer' }
    end
  end
end

これで、

app/views/layouts/mailer.text.erb
app/views/layouts/mailer.html.erb

がメールのレイアウトとして使われるって寸法。

が、ここに落とし穴があります。

      format.text { render layout: 'mailer' }
      format.html { render layout: 'mailer' } # html形式が後

この順番を逆にして

      format.html { render layout: 'mailer' }
      format.text { render layout: 'mailer' } # text形式が後

ってしちゃうと、parts_order での指定を無視して、html 形式が先、text 形式が後に出力されます。 すると RFC のルール通り、text 版の方が優先して表示されちゃいます。html 版は決して表示されなくなります。なんと。

ActionMailer::Base のドキュメントを読んでもそんなこと書いてないですねえ。(該当箇所は "If you want to explicitly render only certain templates..." らへんから)

参考

ActionMailer overrides header[:parts_order] when a block is passed to #mail · Issue #7978 · rails/rails

Rails + Grape 構成で Grape API ファイルを自動再読み込みさせるには

RESTful な API が楽ちんに書ける graperails との組み合わせで使う場合も README に従うだけで簡単に出来ますが、なぜか自動再読み込み (auto reloading) されないって問題にぶち当たります。

development 環境だと model や controller への修正はブラウザをリロードするだけで即反映されますよね。でも grape の api ファイルはなぜか再読み込みされない。いちいち rails サーバを再起動しないといけなくて、非常に困っちゃんです。

(よく理解してませんが rails 上で別の rack フレームワーク(この場合 grape)を動かそうとするとrails の自動再読み込みの仕組みがうまく動かないみたいです)

で、解決方法ですが、grape api ファイルの更新を検知して、無理やり再読み込みさせてやります。

環境

古くてごめんなさい

MacOSX Mavericks
ruby 1.9.3p547 (2014-05-14 revision 45962) [x86_64-darwin13.3.0]
rails-3.2.11
grape-0.9.0

セットアップ

今回は既存の rails アプリに grape を追加しました。手順をざくっと書いておきます。

Gemfile に追記して、

gem 'grape'

インストール

bundle install

grape api ファイルを置くディレクトリを作って

mkdir app/api

Hello World なやつを書いた。(app/api/skimatalk_api.rb)

require 'grape' # この行は grape 単体で動かす場合に必要。rails との組み合わせでは不要
class SkimatalkApi < Grape::API
  format :json

  resource :ehehe do
    desc 'this is a test'
    get :hoe do
      {a: 1, b: 2}
    end
  end
end

rails と組み合わせて実験する前に grape 単体で動かしてみたかったので、同じディレクトリにテスト用の config.ru を作成 (app/api/config.ru)

# coding: utf-8
require './skimatalk_api.rb'
run SkimatalkApi

起動

$ cd app/api
$ rackup
Thin web server (v1.6.2 codename Doc Brown)
Maximum connections set to 1024
Listening on 0.0.0.0:9292, CTRL+C to stop
127.0.0.1 - - [22/Nov/2014 19:54:05] "GET /ehehe/hoe HTTP/1.1" 200 13 0.0971

http://localhost:9292/ehehe/hoe にアクセスすると JSON が表示された。わーい。

f:id:ymdsmn:20141122201157p:plain

(さっき作った config.ru はもう不要なので削除してよいです)

今度はこれを rails 上で動かします。 config/routes.rb に以下を追加して /api/* へのリクエストを grape へ渡すようにします

  mount SkimatalkApi => '/api'

サーバ再起動してから、

rails s

http://localhost:3000/api/ehehe/hoe にアクセス。JSON が表示された。やったね。

f:id:ymdsmn:20141122201201p:plain

でも(さっきも言ったとおり)app/api/skimatalk_api.rb を修正してリロードしても変更が反映されません。困ったね。

解決方法

ここからが本題。 config/initializers/reload_api.rb を作って以下のように書きます

if Rails.env.development?
  api_reloader = ActiveSupport::FileUpdateChecker.new(Dir['app/api/*']) do # app/api/* は環境にあわせて変更すること
    Rails.application.reload_routes! # or do something better here
  end

  ActionDispatch::Callbacks.to_prepare do
    api_reloader.execute_if_updated
  end
end

これだけで、 app/api/* への修正を検知して自動再読み込みしてくれるようになりました。

仕組みは読めばなんとなく分かるよね。分かんなくてもいいよね。

参考

正直このエントリは stackoverflow のこれ↓を解説しただけです。ググって見つけるのに苦労したので書きました…
Ruby on Rails 3 - Reload lib directory for each request - Stack Overflow

jQuery プラグインを bower のレジストリに登録してみた

表題の通り、jQuery プラグイン jquery.narrowsbowerレジストリに登録してみた時の記録です。

bower とは?

こちらが非常に分かりやすかったです。

基礎編の冒頭から(勝手に)引用させて頂きますと、bower とは

Twitter社が作ったフロントエンド用のパッケージマネージャです。 Java で言う MavenRuby で言う gem、 Perl で言う cpan のようなものです。 Node.jsには npm と呼ばれるパッケージマネージャがありますが、それに強く影響を受けています。

ということです。

bower のパッケージは bower レジストリってところで管理されています。 登録済みパッケージの一覧は Bower components ってページで見られます。検索もできます。

この bower レジストリにおいらの jQuery プラグインをいっちょ登録したるぜ!と思い立ちました。

bower レジストリに登録すると何が嬉しいの?

ここに登録しておけば、

bower search で検索したり("sinon" は javascript のテスト用ライブラリです)

$ bower search sinon
Search results:

    sinon git://github.com/cjohansen/Sinon.JS.git
    sinon-chai git://github.com/domenic/sinon-chai.git
    sinonjs git://github.com/blittle/sinon.js.git
(以下略)

bower info でインストール可能なバージョンを調べたり

$ bower info sinon
bower sinon#*               not-cached git://github.com/cjohansen/Sinon.JS.git#*
bower sinon#*                  resolve git://github.com/cjohansen/Sinon.JS.git#*
bower sinon#*                 download https://github.com/cjohansen/Sinon.JS/archive/v1.7.3.tar.gz
bower sinon#*                  extract archive.tar.gz
bower sinon#*                 resolved git://github.com/cjohansen/Sinon.JS.git#1.7.3

{
  name: 'sinon',
  homepage: 'https://github.com/cjohansen/Sinon.JS',
  version: '1.7.3'
}

Available versions:
  - 1.7.3
  - 1.7.1
  - 1.7.0
(中略)
  - 0.6.0
  - 0.5.0
  - 0.2.3

You can request info for a specific version with 'bower info sinon#<version>'

bower install で指定したバージョンをダウンロードしたり出来ます。

$ bower install sinon#1.7.3
bower sinon#1.7.3               cached git://github.com/cjohansen/Sinon.JS.git#1.7.3
bower sinon#1.7.3             validate 1.7.3 against git://github.com/cjohansen/Sinon.JS.git#1.7.3
bower sinon#1.7.3              install sinon#1.7.3

sinon#1.7.3 bower_components/sinon

デフォルトで bower_components ディレクトリ以下にダウンロードされます。

$ ls bower_components/sinon
AUTHORS        LICENSE        build*         lib/           release.sh*
Changelog.txt  README.md      jsl.conf       package.json   test/

JavaScript ライブラリがコマンドラインだけでダウンロード出来ちゃうのがステキです。 いちいちググって、ダウンロードやら git clone やらして、って手間が不要です。

(あと npm の package.jsondependency や devDependency と同様の機能も備えますがここでは触れません)

bower レジストリ登録への道のり

  1. bower.json を作ってリポジトリにコミットして、
  2. リリースバージョン番号をタグ付けして、
  3. bower register コマンドを叩く

という感じ。簡単。順に説明します。

0. bower をインストール

npm でインストール。

$ npm install -g bower
$ bower -v
1.2.8

1. bower.json を作成

bower init コマンドを叩いて質問に答えていくと bower.json の雛形が生成できます。

$ bower init
[?] name: jquery.narrows
[?] version: 0.3.1
[?] description: jQuery Hierselect Plugin
[?] main file: jquery.narrows.js
[?] keywords: jQuery,select,hierselect
[?] authors: simon <xxxxx@gmail.com>
[?] license: MIT
[?] homepage: https://github.com/monmonmon/jquery.narrows
[?] set currently installed components as dependencies? Yes
[?] add commonly ignored files to ignore list? Yes
[?] would you like to mark this package as private which prevents it from being accidentally published to the registry?
[?] would you like to mark this package as private which prevents it from being accidentally published to the registry?
No

{
  name: 'jquery.narrows',
  main: 'jquery.narrows.js',
  version: '0.3.1',
  homepage: 'https://github.com/monmonmon/jquery.narrows',
  authors: [
    'simon <xxxxx@gmail.com>'
  ],
  description: 'jQuery Hierselect Plugin',
  keywords: [
    'jQuery',
    'select',
    'hierselect'
  ],
  license: 'MIT',
  ignore: [
    '**/.*',
    'node_modules',
    'bower_components',
    'test',
    'tests'
  ]
}

[?] Looks good? Yes

それを手直しして、コミット。

$ vim bower.json
$ git add bower.json
$ git commit
$ git push

ちなみに手直しした結果はこんな感じ。

{
  "name": "jquery.narrows.js",
  "version": "0.3.1", // <-gitでのバージョン番号を記述
  "description": "jQuery Hierselect Plugin",
  "license": "MIT",
  "main": "jquery.narrows.js", // <-パッケージのメインとなるファイル
  "ignore": [ // <-bower install した時にダウンロードさせたくないファイルのリスト
    "**/.*",
    "**/*.html",
    "Gruntfile.coffee",
    "node_modules",
    "README.md",
    "package.json",
    "lib",
    "spec"
  ]
}

さっきのBower入門(応用編)にも書かれていますが、 "main" と "ignore" をキチンと書かないと bower install した時に余計なものまでダウンロードしちゃって鬱陶しいので、気をつけて記述します。 (例えば Gruntfile.coffee はライブラリ利用者には不要なので "ignore" に含めてます。)

あと "version" にも要注意。ここに書いたバージョン番号を、次で git tag でタグ付けします。

2. リリースバージョン番号をタグ付け

bower.json に書いたバージョン番号をタグ付けしてやります。 bower はこのバージョン番号に基いてパッケージを管理します。

$ git tag 0.3.1
$ git push --tags
Total 0 (delta 0), reused 0 (delta 0)
To git@github.com:monmonmon/jquery.narrows.git
 * [new tag]         0.3.1 -> 0.3.1

バージョン 0.3.1 がリリースされました。

3. bower register

さていよいよ、ライブラリをレジストリへ登録します。コマンドの書式は

bower register <my-package-name> <git-endpoint>

です。

$ bower register jquery.narrows git@github.com:monmonmon/jquery.narrows.git
bower                          convert Converted git@github.com:monmonmon/jquery.narrows.git to git://github.com/monmonmon/jquery.narrows.git
bower jquery.narrows#*         resolve git://github.com/monmonmon/jquery.narrows.git#*
bower jquery.narrows#*        download https://github.com/monmonmon/jquery.narrows/archive/0.3.1.tar.gz
bower jquery.narrows#*         extract archive.tar.gz
bower jquery.narrows#*        resolved git://github.com/monmonmon/jquery.narrows.git#0.3.1
[?] Registering a package will make it installable via the registry (https://bower.herokuapp.com), continue? (Y/n)
bower jquery.narrows          register git://github.com/monmonmon/jquery.narrows.git

Package jquery.narrows registered successfully!
All valid semver tags on git://github.com/monmonmon/jquery.narrows.git will be available as versions.
To publish a new version, just release a valid semver tag.

Run bower info jquery.narrows to list the available versions.

ほんとに登録されたのか確認してみましょう。

$ bower info jquery.narrows
bower jquery.narrows#*          cached git://github.com/monmonmon/jquery.narrows.git#0.3.1
bower jquery.narrows#*        validate 0.3.1 against git://github.com/monmonmon/jquery.narrows.git#*

{
  name: 'jquery.narrows.js',
  version: '0.3.1',
  main: 'jquery.narrows.js',
  description: 'jQuery Hierselect Plugin',
  license: 'MIT',
  ignore: [
    '**/.*',
    '**/*.html',
    'Gruntfile.coffee',
    'node_modules',
    'README.md',
    'package.json',
    'lib',
    'spec'
  ],
  homepage: 'https://github.com/monmonmon/jquery.narrows'
}

Available versions:
  - 0.3.1

You can request info for a specific version with 'bower info jquery.narrows#<version>'

出た出た!

Bower components でも、検索フィールドに "jquery.narrows" って入力するとちゃんと表示されました。

試しに bower install してみる

今登録したライブラリがちゃんと bower install 出来るかも実験してみましょう。

$ bower install jquery.narrows#0.3.1
bower jquery.narrows#0.3.1      cached git://github.com/monmonmon/jquery.narrows.git#0.3.1
bower jquery.narrows#0.3.1    validate 0.3.1 against git://github.com/monmonmon/jquery.narrows.git#0.3.1
bower jquery.narrows#0.3.1     install jquery.narrows#0.3.1

jquery.narrows#0.3.1 bower_components/jquery.narrows

そしたらホラホラ!

$ ls -l bower_components/jquery.narrows
bower.json             jquery.narrows.js      jquery.narrows.min.js

ひゃっほい。

bowser.json の "ignore" をちゃんと書いたおかげで、余計なファイルを(ほぼ)含まずに js ファイルだけダウンロードすることが出来ました。(bower.json 自身だけ "ignore" に入れるのを忘れてたけど…笑)

というわけで

快適な bower ライフをお楽しみ下さい(とってつけた)