rake db:migrateをRidgepoleで置き換える

ちょっとRidgepole試してみようかなと思って使いはじめた。rails書いていると他の人がmigrationふつうにしようとしてしまったりするだろうから、rake db:migrateのデフォルトの動作を無効にして、ridgepoleを動作させたい。

lib/tasks/ridgepole.rakeとかで、

# overwrite db:migrate or db:dump
task 'db:migrate' => :environment do
  ENV['RAILS_ENV'] ||= "development"
  sh "bin/ridgepole -E#{ENV['RAILS_ENV']} -c config/database.yml --apply"
  sh "bin/ridgepole -E#{ENV['RAILS_ENV']} -c config/database.yml --export -o Schemafile"
end

task 'db:dump' => :environment do
  ENV['RAILS_ENV'] ||= "development"
  sh "bin/ridgepole -E#{ENV['RAILS_ENV']} -c config/database.yml --export -o Schemafile"
end

applyした直後でexportするべきかはどうかなーと思ったりしつつ、db:dumpいちいち打つのがダルいことが多いのかな。ということでやることにした。

Gemfileで、activerecordのedgeを指定する

先に結論をまとめると、こうすればよいらしい。

gem 'arel', git: 'https://github.com/rails/arel.git'
git 'https://github.com/rails/rails.git' do
  gem 'activerecord'
end


ライブラリで、特定のバージョン以上にbundlerが指定されていて、それを一時的にtravisなどで、HEADでテストを走らせたい、というときにどうするか、ということですが、railsの場合には、

rails/
-------rails.gemspec
-------activerecord/
-------------------activerecord.gemspec
-------actionpack/
-------------------actionpack.gemspec

のようなディレクトリ構成になっていて、directoryのtopにgemspecがある場合には、gitオプションで指定すればよいのですが、それ以下のディレクトリはどう指定したらいいんだろうか、と悩んでいたのでした。

test with active_record edge. · 3233386 · walf443/activerecord-mysql-unsigned · GitHub

config/routes.rbを分割する

とあるサービスのconfig/routes.rbが肥大化してきたので、分割したくなっていたので、調べていた。

ググってみると色々方法があるらしいが、バージョンごとにやり方が違ったりしてげんなりする。
よく出てくるconfig.paths["config/routes"]をいじる方法は、どうもRails4からは削除されていて動かないようだ

ということで黒魔術を使った。めでたしめでたし。

# config/routes/some_module.rb
module SomeModule
    def self.apply(context)
        context.instance_exec do
            # 実行させたい処理を書く
            #   resources :products do
            #     member do
            #       get 'short'
            #       post 'toggle'
            #     end
             #  end
        end
    end
end
# config.rb
Dir[Rails.root.join('config/routes/*.rb')].each do |file|
  load file
end

Your::Application.routes.draw do
    SomeModule.apply(self)
end

解説

Your::Application.routes.drawの実態は、ActionDispatch::Routing::RouteSet#drawというやつのようで、その実装は、次のようになっている。

# actionpack/lib/action_dispatch/routing/route_set.rb
      def draw(&block)
        clear! unless @disable_clear_and_finalize
        eval_block(block)
        finalize! unless @disable_clear_and_finalize
        nil
      end

#eval_blockの実装は、

      def eval_block(block)
        if block.arity == 1
          raise "You are using the old router DSL which has been removed in Rails 3.1. " <<
            "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/"
        end
        mapper = Mapper.new(self)
        if default_scope
          mapper.with_default_scope(default_scope, &block)
        else
          mapper.instance_exec(&block)
        end
      end
      private :eval_block

なので、drawしたときに作成する&blockの実態は、instance_execのコンテキストで実行されているようだ。

with_default_scopeするケースがあるが、こちらの実装は、

# actionpack/lib/action_dispatch/routing/mapper.rb
        def with_default_scope(scope, &block)
          scope(scope) do
            instance_exec(&block)
          end
        end

となっていて、こちらもinstance_execになっている。

なので、コンテキストを受けとって、コンテキストに対してinstance_execしてやれば同じコンテキストで実行できるので、instance_exec以降は通常のroutesと同じことが記述できる。

開発環境で再起動しなくても読みなおせるようにloadで読みこんでいるが、config/routes.rbのtimestampを更新しないと再読みこみしないのがどうしたもんかなーという感じではある。

ActiveRecordのSQLの実行箇所をSQLのコメントに入れる

arproxyを使うと、SQLにフックして色々書きかえることができて非常に便利ですね。

module Arproxy
  class QueryLocationCommentAppender < Arproxy::Base

    WHITE_LIST_WORD_RE = %r{^[a-zA-Z0-9\-_/:\?]+$}

    def execute(sql, name=nil)
      if ENV["ARPROXY_QUERY_LOCATION"]
        # SQL Injection対策
        # コメントの閉じ文字がパラメータに含まれていなければ大丈夫なはず
        if ENV["ARPROXY_QUERY_LOCATION"].index("*/").nil?
          # 制御文字や、マルチバイトの文字を防ぐため、文字種を制限する
          if ENV["ARPROXY_QUERY_LOCATION"] =~ WHITE_LIST_WORD_RE
            substr = ENV["ARPROXY_QUERY_LOCATION"]
            substr = substr.size >= 100 ? substr.slice(0, 100) : substr
            sql += " /* called from \"#{substr}\" */ "
          end
        end
      end
      super(sql, name)
    end
  end
end

Arproxy.configure do |config|
  config.adapter = "mysql2"
  config.use Arproxy::QueryLocationCommentAppender
end

Arproxy.enable!

みたいなのをconfig/initializers/arproxy.rbに書いておいて、ApplicationControllerのbefore_filterあたりにでも、

  def set_query_comment_url
    ENV["ARPROXY_QUERY_LOCATION"] = request.path_info
  end

こんなのをしこんでおけば、実行したページのURL情報がわかるので、mysqlのslow queryのログなどを見て、どのURLのクエリが重たいのか判別できる。
(なおSQL Injection対策はmysql限定(それ以外は調べてないので、注意))

情報の受けわたしに環境変数をつかってしまったが、グローバル変数でよい気もする。

また、開発時には、URLではなくて、どのmodelや、controller、viewから発行されているか知りたくなるので、

    def execute(sql, name=nil)
      stack_trace = caller.select {|i| i.inspect =~ %r{app/(models|controllers|view)} }.join("\t")
      if stack_trace
      # SQL Injection対策
      # コメントの閉じ文字がパラメータに含まれていなければ大丈夫なはず
        if stack_trace.index("*/").nil?
          substr = stack_trace
          sql += " /* called from \"#{substr}\" */ "
        end
      end
      super(sql, name)
    end

こんな感じにしてdevelopment.logを見つつ、発行元を確認するとよい。(だいぶ見づらいけど。

arproxy++

rrails v1.0.0 released.

以前公開した、rails/rakeコマンドを超早くするrrailsですが、
http://d.hatena.ne.jp/walf443/20120427/1335482420

あれから、色々な方に使っていただいているようで、パッチをいくつかもらったりしてupdateしたりしています。

最近quark-zjuさんという方からたくさんpatchをいただいて、今までできなかったことができるようになった & 今までと非互換なところがでてきた、ということで紹介します。

rails console/serverができるように

今まで子プロセスへPTYの引きつぎをしていなかったため、rails console/serverなどが実行しても、こちらの入力をrrailsへ教えることができなかったのですが、できるようになりました。

個人的にはrails consoleあんまり使わないので対応しなくてもいいかなーと思って放置していたところですが、使えるとまぁ便利かなとは思います。

rrails-serverコマンドは非推奨になり、rrails startで起動するように

guard-rrailsつかっているとあまり関係ないですが、rrails-serverコマンドはそのうちなくなると思われるので、rrails startを使うようにしてください。

TCPSocketではなく、UNIXDomainSocketをデフォルトで使うように

rrailsとの通信は今までTCPでの通信でしたが、デフォルトではUnixDomainSocketを使うようになりました。

これにより、複数のプロジェクトでのrrailsの立ちあげが楽になると思われます。
一方で、rrailsを実行する際に、対象のプロジェクトのディレクトリへcdしないと適切に実行できなくなっています。

まとめ

使い方が色々かわっている(なるべくは互換性残すようにはがんばったつもりだけど...)ので、今まで使っていた方はあらためてREADMEを読みなおすことを推奨します。
http://walf443.github.com/rrails/

https://github.com/walf443/rrails/blob/master/Changes.md

rackのアクセスログをfluentdへ投げるミドルウェア

railsのアクセスログは、とても機械的に解析しづらく、運用するにはあまりよろしくない。そこで、Rack::CommonLoggerを使ってアクセスログを別途とったりしようとしていたのですが、(一般的にはNew Rericとかをつかうのが普通なんでしょうか?)
ログのローテーションとか考え出すと、あまり良い方法が思いつかない、という関係で、

  • syslogへ投げる
  • fluentdへ投げる

というアイディアを思いついたのですが、syslogだと機械解析しづらいフォーマットになってしまったりあまり詳しい人が意外といなかったりする関係で、流行っているfluentdへ投げちゃえと思いRack::CommonLogger::Fluentというrackのミドルウェアを書いてみた次第。

https://github.com/walf443/rack-common_logger-fluent

まだ作り始めで、仕様をどうしようと悩んでいるのだけど、わりと需要はありそうな気がしていて、できるだけ使われるものにしたいのと、あとから後方互換性を崩すということはなるべくしたくないので、ご意見を募集中

ログのフォーマット

Rack:CommonLogger::Fluentでは、RackのリクエストからHashを構築し、それをFluent::Loggerを使い、fluentdへ投げる、という感じになっています。

  • "content_length": レスポンスのCONTENT_LENGTHヘッダの値。Integerにするか、Stringにするか悩んでいる。Intergerがよいのかな
  • "http_status": レスポンスのHTTP STATUS CODE。これもIntegerにするかStringにするか悩んでいる。Integerがよいのかな
  • "accessed_at": 日付はどのフォーマットの文字列にするべきか?
  • ヘッダが空だった場合の扱い。nil or ""で悩んでいる。どちらかというとnilかなー?

ActiveResourceを少し触ってみた

TwitterAPIRailsのActive Resouceを試してみる
http://blog.tkmr.org/tatsuya/show/318-twitter-api-rails-active-resouce

の影響を受けてActiveResourceを使ってみた。

上の記事と合わせて思ったことをつらつらと書きます。

多くの場合書くクラスの設定は共通しているのでサイトごとにベースのクラスを用意してやって小クラスはそれを継承してやってから使う方が色々と楽でよいと思う。つまり上の記事の例なら、

require 'highline'
require 'logger'

class Twitter::API < ActiveResource::Base
 USER = HighLine.new.ask('user:')
 PASSWD = HighLine.new.ask('passwd:') {|q| q.echo = '*' }
 self.site = "http://#{USER}:#{PASSWD}@twitter.com"
 self.logger = Logger.new($stderr)
end

class Twitter::API::Status < Twitter::API
end

Twitter::API::Status.find(:all, :from => :user_timeline)
   #=> ...
   # Log
   GET http://twitter.com:80/statuses/user_timeline.xml--> 200 OK (12220b 0.80s)

みたいな感じに仕える。また、クラスアクセサにloggerを渡してやるとアクセスするたびにログにはかれるのでrequestメソッドをHackする必要はない。

ちなみにActiveResource::StructのRDocを見る限りでは将来的にはログイン情報などはcredentialsとして渡せるようになりそう。つまり上記のは次のように書ける。

class Twitter::API < ActiveResource::Base
 self.site = 'http://twitter.com'
 self.credentials({
   :username => HighLine.new.ask('user:'),
   :password => HighLine.new.ask('passwd:') {|q| q.echo = '*' }
 })
end

早速HighLineの出番がきた。HighLine++