M5StickC Plus とマイクモジュールでインターホンの呼び出し音が鳴ったら Discord に通知を送るようにした

宅急便等でインターホンが鳴っても別の部屋で作業をしていると気づかないことがあったので Discord に通知を送るようにしました。

以下解説

参考

qiita.com

大体ここから拝借しました

機器

ソースコード

ここでしか C++ 書いたことないからやっつけレベル

github.com

解説

これは何

マイクモジュールが一定以上の音を検出すると Discord に post する仕組みです。

インターホンの呼び出し 1 回で何度も Discord に通知がいくと邪魔なので、一度 Discord に通知してから 120 秒間は通知しないようにしています。

冒頭の M5Stick のディスプレイに写っているもの

  • 左上
  • 右上
    • Discord 通知禁止期間の残り秒数
  • 真ん中の緑の線
    • 音の波形。マイクが音を検出するとギザギザする。サンプルコードを動かした時のやつをそのままにしてる。
  • 左下
    • ローカル IP アドレス。これも参考にしたソースコードそのままの状態にしている。

まとめ

インターホンの音を検知して Discord へ post することで宅急便に気付きやすくなるようにしました。

本記事執筆時点でまだ運用 2 日目なのでまだ改善の余地がありそうですが今の所想定通りに動いています。

M5StickC Plus と MH-Z19C センサで CO2 濃度を可視化してみた

この記事は Rakuten Rakuma Advent Calendar 2022 の 17 日目の記事です。

マイコン初心者が CO2 センサの MH-Z19C で計測した値を M5StickC Plus で表示するまでの内容となります。

この手の記事は検索すると割と出てくるものですが、前提や諸条件が自分の手元の環境と微妙に違ったりしてそれなりに苦労したので誰かの助けになればと思い記事にしました。

きっかけ

かねてからラズパイに興味はあったものの今まで手をつけてこなかったのですが、ちょうどプラモが完成したタイミングでアドベントカレンダーをやりますという話を聞いたのでいい機会だしラズパイで遊んで記事書くかと軽い気持ちで手を上げました。

ラズパイ手に入らん

いざラズパイ買おうとするとどこも品切れ。。。

ポチれるサイトを見つけてポチって見たら後々メールで入荷予定は 1 年後とのこと。

M5Stick なるものを見つけた

元々家の二酸化炭素濃度計測したいなーと思っていてそれをラズパイでやってみたいと思ってたのでそれっぽいワードで色々検索していたら M5StickC Plus と MH-Z19C で CO2 濃度を測ってみたという記事に流れ着いたのでこれ幸いと飛びついたわけです。

kitto-yakudatsu.com

環境

CO2 濃度を表示するまでに苦労して色々なサイトに書いてあることを試行錯誤したので最終的な環境を書き残します

開発環境

必要なブツ

M5StickC Plus

Amazon | M5Stack M5StickC Plus ESP32-PICOミニIoT開発ボードm5stack iotキット フィンガーコンピューターカラーLCD | 基板 | 産業・研究開発用品 通販

MH-Z19C

これ系のブツはフェイク品が結構あるらしいですがここのサイトで買えば良いらしいです

akizukidenshi.com

ピン配列交換ケーブル

M5StickC Plus と MH-Z19C をつなぐやつ

www.amazon.co.jp

ソースコード

ソースはほぼ こちらのサイト をまるっと使わせていただきましたが一応 github に上げておきました。

github.com

色々あってできた

CO2 センサから値を取ってきて表示するまでできたのでとりあえずのゴールは出来ました。

追記

ここまで書いた後 Twitter 経由で CO2 HAT の作成者様からアドバイスいただきました。

CO2 HAT とは

当初 MH-Z19C と M5StickC Plus をつなぐものとして CO2 HAT なるものを使っていました。

kitto-yakudatsu.booth.pm

ただ合体させるだけで良いのでめっちゃ楽だしケーブルで繋ぐよりスッキリします。

しかし、使い方が書かれたブログ の通りに進めていくと VSCode 上で M5STACK DEVICE のファイルリストが見えるらしいのですが何も見えず途中で挫折し PlatformIO 拡張機能を使う方法にシフトしました。

その後アドバイスいただけたおかげで CO2 HAT でも CO2 濃度を表示させることができました。こちらオススメです。

先ほど紹介したソースコードからピン番号が変わります。

このピン番号というか回路図あたりが全然理解できてない初心者丸出し具合が出てます。。。

#define RX_PIN 36 // Rx pin which the MHZ19 Tx pin is attached to
#define TX_PIN 0 // Tx pin which the MHZ19 Rx pin is attached to

devise ソースコードリーディング

何年か前にも devise 関連の gem の中身見て、再び中身を見る機会があったけどすっかり挙動を忘れてたので大まかな流れをメモっておく

完全に自分用のメモ

古の盟約により devise-token_authenticatable を使用している

- application_controller
  - authenticate_user!
    - devise helper_method authenticate_user!
      - warden authenticate!
        - warden proxy _perform_authentication
          - warden proxy _retrieve_scope_and_opts
          - warden proxy user
            - warden session_serializer.fetch return nil
            - warden hooks after_failed_fetch
            - warden proxy set_user
          - warden proxy _run_stragegies_for
            - warden proxy _fetch_stragegy strategies.each
            - warden proxy stragety.performed?
            - devise-token_authenticatable stragety valid? (stragety.valid?)
              - devise strategies authenticatable valid?
                - devise authenticatable valid_for_params_auth?
                  - devise authenticatable stragety params_authenticatable?
                    - devise authenticatable model params_authenticatable?
                  - devise authenticatable stragety valid_params_request?
                  - devise authenticatable stragety valid_params?
                  - devise authenticatable stragety with_authentication_hash
            - warden base _run!(strategy._run!)
              - devise-token_authenticatable stragety authenticate!
                - devise-token_authencatable model fird_for_token_authentication
                  - devise-token_authencatable model find_for_authentication
                    - devise-token_authencatable model find_first_by_auth_conditions
                - devise strategies authenticatable validate
                  - devise model authenticatable valid_for_authentication?
                - devise-token_authencatable model after_token_authentication (only definition)
                - warden base success!
          - warden proxy set_user

基本的な流れは

strategies.each do |strategy|
  strategy._run!
end

をやってる

stragegy は mail & pass 認証だったり token 認証だったりしてるやつ

gem の中で params とかが使われているけど、application_controller とかで使う params ではなく warden の params メソッドを使ってる

devise-token_authenticatable-1.0.2/lib/devise/token_authenticatable/strategy.rb

def params_auth_hash
  if params[scope].kind_of?(Hash) && params[scope].has_key?(authentication_keys.first)
    params[scope]
  else
    params
  end
end

warden-1.2.7/lib/warden/mixins/common.rb

def params
  request.params                          
end

ドリコムを支えるデータ分析基盤がTD+AWSに移行した話

はじめに

これは ドリコムAdventCalendar の7日目です

6日目は、keiichironaganoさんによる iTunes 使用許諾更新のとき一旦キャンセルしてほしい話 です

【その2】ドリコム Advent Calendar 2015 もあります

自己紹介

@ka_nipan

去年の ドリコムを支えるデータ分析基盤 に引き続き、今年もドリコムのデータ分析基盤を担当しています。

分析基盤をTreasure Dataに移行

オンプレ環境の Hadoop からTreasure Data に移行しました。

また、ジョブ管理ツールやBIツールといったサーバーもAmazon EC2 に移行しており、 徐々にオンプレ環境を離れつつあります。

背景

オンプレ環境で Hadoop を運用して3年も経つと考えなければならないのが HW の寿命です。

さてどうしようかとなった時に、ほぼ迷いなく外部サービスに移行するという選択を取りました。

Treasure Data、BigQuery、Amazon Redshift のようなサービスがある中で HW の交換作業をしてまでこのままオンプレで運用し続けるメリットはあまり感じられなかったためです。

サービス選定

候補として上で挙げた Treasure Data、BigQuery、Amazon Redshift の3サービスを実際に触ってみて最終的に Treasure Data を使うことに決めました。

Redshift はちょっと触っただけなのですが、実際に触ってみた所感の比較です。 ※ 2015.1 時点

     Treasuredata presto BigQuery Amazon Redshift
リソースのスケール フルマネージ フルマネージ ユーザがノード変更
同時実行 4〜 20 5〜
課金 固定額。お高め データサイズ+クエリ数+クエリスキャンサイズ+etc 。全体的にお安い。データ抽出サイズに気をつけないとウン百万の請求も 固定額+etc
パフォーマンス 速い (件数依存、対象が数十億ならHiveにする) 件数に関係なく速い 速い (件数に依存)
テーブル(分散) 設計 不要 不要 必要
スキーマレス 固定 (json構造ならレス) 固定 (json構造ならレス)
データ投入 実質入れ放題 制約有り (回避方法あり) S3経由 (大量データのインポートが遅い)
サポート
総評 バランスが良い。専任で人をつけなくても安定した価格、性能で運用できそう パフォーマンスを保つため、制約が多いので注意が必要 (制約条件が突然変わったら爆死する可能性)。節約したい時、件数が多くてどうしようもなくなった時に使えるかも 安価で始められるDWH。大規模になるほどチューニングが必要になってくる。 スモールスタート。データマート向き。変更に弱い。

他にも比較する部分が色々あるのですが、あんまり細かくなってもしょうがないので、ざっくりとした所感で書きました。

また、弊社の分析基盤としては職人をできるだけ作らないような体制を目指しているので一番運用が楽で安定しそうな Tresure Data を使うことになりました。

新データ分析基盤全体図

f:id:ka_nipan:20151126175144p:plain

大きなところは Hadoop が Treasure Data に変わり、AWS のサーバーを徐々に使い始めているというところで、それ以外のところは去年の ドリコムを支えるデータ分析基盤 とほぼ変わっていません。

DBのスナップショットはS3へ

Hadoop の時は、アプリのログもDBから取ってきたスナップショット(tsv) もHDFS に置いていたのですが、Treasure Data にはデータ件数の制限があるので、無闇に全部 TD に置くということはせずに TD 上で集計が必要なもの以外は S3 に置くことにしています。

また、ログもTD上には直近x日まで保存して、古いものはS3に逃がすということもログ件数調整の一環としてやっています。

闇のスクレイピング技術は今も健在

去年のエントリの闇スクレピングは今も元気に動いています。。。地味に便利なのが憎らしい。。。

まとめ   

・安定した運用、職人芸を少しでも減らすために Treasure Data を分析基盤に

・特定の条件下であれば、BigQuery、Redshift はあり

・金を出した分だけ楽ができる

8日目は @sazae657 さんです

ドリコムを支えるデータ分析基盤

はじめに

これは ドリコムAdventCalendar の4日目です

3日目は、@arihh さんによる 3年くらいお菓子神社運営してきた です

自己紹介

@ka_nipan

ドリコムに新卒で入社し、Android開発、BtoBtoC のwebサービス開発を経て、現在は弊社アプリのログ収集から集計、可視化、その他周辺ツールといった分析基盤の面倒を見ています

本日はそのデータ基盤の話を書きます

データ分析基盤全体図

f:id:ka_nipan:20141128203507p:plain

弊社では Hadoop をオンプレで運用していて、そこにログや分析用のデータを置いています

メリット

運用コストが安い

Treasure Data、Big Query、Amazon Redshift 等の外部サービスを使うよりは安く済みます

自由度が高い

各サービスには容量をはじめ色々と制限があったり、こちらの要求仕様にマッチしない部分が少なからずありますが、自前の場合その辺は融通がききやすいです

分析する人が生データにアクセスしやすい

後述しますが、分析チームの人が R言語 で処理しやすいように、Hadoop にデータを集積しています

なので、基本的に HDFS には gzip 圧縮した tsv ファイル等を置いていて、SequenceFile は置いていません

分析用のサーバーに Fuse で mount しているので通常のファイルシステムのように扱えます

デメリット

メンテナンスが辛い

HDDに障害があったり、運用年数が経ってくると新しいものに入れ替えたり、リカバリ作業をする必要があります

また、HadoopやOSのバージョンアップをしようと思ったら、NameNode や DataNode 全台、その他すべて含めると結構な台数になってくるので精神がガリガリ削られていきます

HDFS全体の容量とかブロックの数とか気にしてあげないといけない点も結構あります

データ取得・保存

ログ送信

ログの送信は fluentd を使用しています

鉄板ですね

図では端折ってますが、web → log collector → Hadoop のように一旦ログを集約してから Hadoop に送信するようにしています

テスト環境のログ排除

テスト環境のログが Hadoop に送られてしまうと集計値がくるってしまうので、ホスト名に test, staging のような文字列が入っている場合 Hadoop に送信しないようにしています

ログ送信成否の確認

webサーバー追加したけど設定ミスでログ送れてませんでしたー!等を突き止めやすくするために、web → collector に送る時にホスト名を送るようにしています

事前処理

送られてきたログに対して、集計に必要な付加情報を加える処理をしています

ここでフォーマットのチェックも行っていて、エラーとなったログは集計対象から除外されます

集計は hive を使用しており、事前処理が終わったらロードされます

スナップショット取得

アプリのDBにSQLを投げて、その結果をファイルとしてHDFSに保存しています

DBのフェイルオーバー対策

DBにクエリを投げる際、SLAVE に投げるわけですが、DBがフェイルオーバーするとSLAVEのIPアドレスも変わってしまいます

そこで、SLAVE のIP情報を管理用のサーバーのDBに入れておき、アプリのDBがフェイルオーバーすると管理用DBにあるIPも書き換わるという仕組みが用意してあります

この仕組みにより常にSLAVEへとクエリを投げることが可能になっています

データの持ち方としてはこんな感じ

CREATE TABLE IF NOT EXISTS `slavedb` (
  `app_key`               varchar(64)    NOT NULL ,
  `host_name`             varchar(64)    NOT NULL ,
  `slave_ipaddress`       varchar(64)    NOT NULL ,
  `db_name`               varchar(64)    NOT NULL ,
  `updated_at`            timestamp      NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`app_key`,`host_name`,`db_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

帯域への配慮

結構な数のクエリを投げたり、結果がでかいものがあったりで何も考えずに一気に流すとネットワークの帯域を食いつぶす可能性があった (実際やばかった) ので、

一度に流すクエリの量を抑える仕組みを作って安全に配慮してます

クエリの管理

アプリのDBへ投げるクエリは ActiveAdmin で管理しています

また、ここで管理されているクエリは管理画面から Explain を実行してその結果を見ることができるように手を加えています

クエリのシンタックスエラーのチェックもできて地味に便利

f:id:ka_nipan:20141203155910p:plain

バックアップ

ログ、スナップショットファイルは Hadoop に取り込まれると同時にバックアップサーバにコピーされます

集計

前述のとおり集計はみんな大好き hive を使っています

presto はテスト的に使ってみたりしてます

集計したデータは可視化のため MySQL

集計単位

  • hourly
  • dayily
  • monthly

ジョブ数

集計以外の全ジョブ含め約600個

可視化

有料BIツールを使っていますが、パフォーマンス、アカウント制限等々の理由で結局BIツール自作しました

f:id:ka_nipan:20141128203959p:plain

黒背景にするとなんかカッコよく見えますね!

分析チーム

弊社にはアプリの分析を行う分析専門のチームがいて、

ハイスペックな分析用サーバ数台に HadoopFuse マウントして自由に Hadoop 上のファイルを使えるような環境を用意しています

基本的に R で分析を行い、彼らの定期ジョブは分析サーバーで Jenkis で実行しています

少し前まで cron を使っていましたが、さすがに管理が辛くなってきました

スクレイピング

ログの集計値の正当性を担保するために、プラットフォームの管理画面で見ることができる数字(売上等)を取ってこなければいけません

(ちなみにあまりにも誤差があると、ログと管理画面からDLできる課金明細をつき合せていくことになります...)

しかし、プラットフォームによってサイトの作りがまちまちです

単純にスクレイピング用ライブラリではすべてを解決できませんでした (そのへんのAPIは提供されていない)

そこで色々もがいた結果、以下のような環境が出来上がりました

使用しているもの

f:id:ka_nipan:20141128205157p:plain

Grease monkey とは firefoxプラグインのひとつで、特定のURLに対して任意のコード (javascript) を実行できます

仕組み

  • Linux に作った GUI 環境から指定のURLに firefox でアクセスする
  • グリモンで目的のページまでたどり着いたら、そのページをテキストファイルとしてダウンロードします
  • そのテキストファイルから正規表現で必要な部分を抜いてDBに入れます

もはやスクレイピングでもなんでもないですね

最高に力技ですが、この方法でURLの末尾にランダムなハッシュがつくサイトも非同期処理をするサイトでもなんでも抜いてこれるようになりました!

サンプルコード (雰囲気だけ感じてもらえれば...)

// ==UserScript==
// @name           kani_scraper
// @namespace      imperial_cross
// @include        https://developer.hogee.jp/
// @include        https://developer.hogee.jp/*
// ==/UserScript==


(function()
{


if(location.href == "https://developer.hogee.jp/"){
    document.getElementsByName("login_id")[0].value = login_id;
    document.getElementsByName("password")[0].value = password;
    document.getElementsByName("form")[0].submit();
}else if(location.href == "https://developer.hogee.jp/home"){

// http://d.hatena.ne.jp/a_bicky/20110718/1311027391 参照
writeHtmlFile =  function () {
    function writeToLocal (filename, content){
    var ua = navigator.userAgent.toLowerCase();

    try {
        if (ua.indexOf('firefox') != -1) {  // Firefox
                //filename = (ua.indexOf('windows') != -1 ? 'C:\\tmp\\' : '/tmp/') + filename;
        if (ua.indexOf('windows') != -1){
            filename = "C:\\tmp\\" + filename;
        }
        else {
                    filename = path + filename;
        }

        unsafeWindow.netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
        // ファイルコンポーネントの取得+ローカルファイル操作用のインターフェイスの取得;
        var file = Components.classes['@mozilla.org/file/local;1'].createInstance(Components.interfaces.nsILocalFile);
        file.initWithPath(filename);

        var fileStream = Components
            .classes['@mozilla.org/network/file-output-stream;1']
            .createInstance(Components.interfaces.nsIFileOutputStream);
        // ファイルが存在しない場合は664の権限で新規作成して書き込み権限で開く
        // cf. https://developer.mozilla.org/en/NsIFileOutputStream
        //     http://www.oxymoronical.com/experiments/apidocs/interface/nsIFileOutputStream;
        fileStream.init(file,
                0x02 | 0x08,  // 0x01: 読み取り専用, 0x02: 書き込み, 0x03: 読み書き, 0x08: 新規作成, 0x10: 追記
                0664,         // mode
                0             // 第4引数は現在サポートしていないとか
                   );

        // cf. http://www.oxymoronical.com/experiments/apidocs/interface/nsIConverterOutputStream
        var converterStream = Components
            .classes['@mozilla.org/intl/converter-output-stream;1']
            .createInstance(Components.interfaces.nsIConverterOutputStream);
        converterStream.init(fileStream, 'UTF-8', content.length,
                     Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
        converterStream.writeString(content);

        converterStream.close();
        fileStream.close();
        //alert('書き込みが完了しました!');
        } else if (ua.indexOf('chrome') != -1) {  // Google Chrome
               // 起動オプションに --unlimited-quota-for-files --allow-file-access-from-files をつける必要あり
        function errorCallback(e) {
            alert('Error: ' + e.code);
        }

        function fsCallback(fs) {
            fs.root.getFile(filename, {create: true}, function(fileEntry) {
            fileEntry.createWriter(function(fileWriter) {
                fileWriter.onwriteend = function(e) {
                alert('書き込みが完了しました!');
                };

                fileWriter.onerror = function(e) {
                alert('Failed: ' + e);
                };

                var bb = new WebKitBlobBuilder();
                bb.append(content);
                var output = bb.getBlob('text/plain');
                fileWriter.write(output);
            }, errorCallback);
            }, errorCallback);
        }
        // 現時点ではたぶん第1引数はPERSISTENTもTEMPORARYディレクトリ名が異なるだけだし、
        // 第2引数は極端な話0でもOK
        webkitRequestFileSystem(PERSISTENT, 1024, fsCallback, errorCallback);
        } else if (ua.indexOf('msie')) {  // MS IE
        filename = 'C:\\tmp\\' + filename;
        // インターネットオプションで「スクリプトを実行しても安全だとマークされていない
        // ActiveX コントロールの初期化とスクリプトの実行(セキュリティで保護されていない)」
        // を有効にする必要あり
        var fso = new ActiveXObject('Scripting.FileSystemObject');

        // ファイルを新規作成して書き込みモードで開く (文字コードはUTF-16)
        // cf. http://msdn.microsoft.com/ja-jp/library/cc428044.aspx
        //     http://msdn.microsoft.com/ja-jp/library/cc428042.aspx
        var file = fso.OpenTextFile(filename,
                        2,     // 1: 読み取り専用, 2: 書き込み, 8: 追記
                        true,  // ファイルが存在しなければ新規作成するかどうか
                        -1     // -2: OSのデフォルト文字コード, -1: UTF-16, 0: ASCII
                       );
        file.Write(content);

        file.Close();
        alert('書き込みが完了しました!');

        /*
         * ADODB.Stream を使う場合(レジストリをいじっても何故か書き込めない・・・)
         */
        // var adodbStream = new ActiveXObject('ADODB.Stream');
        // adodbStream.type = 2;  // テキストファイル(バイナリは1)
        // adodbStream.charset = 'UTF-8';
        // adodbStream.open(filename);
        // adodbStream.writeText(content);
        // adodbStream.saveToFile(filename, 2);  // 上書き保存(1だと新規作成のみが対象)
        // adodbStream.close();
        } else {
        alert('エラー: ローカルファイルへの書き込み方がわかりません・・・');
        }
    } catch (e) {
        alert('Error: ' + e);
    }
   }    


    writeToLocal('test1.txt', "hogehoge");


}
}
setTimeout( writeHtmlFile, 20000);//20秒後にページを閉じる
})();

闇っぽい雰囲気だけ感じてください

Hadoop まわりの運用

Hadoop、hive での運用で気を使っている点や以前困っていた点を紹介します

細かいファイルは集約する

HDFS のデフォルトブロックサイズは 64MB と大きく、小さいファイルをたくさん置くのはあまり効率が良くありません

サイズの小さいものは1日単位などに集約すると結構ブロック数を減らせます

簡単に再集計できる仕組み

再集計をかけるタイミングが必ず存在します。しかも意外に高い頻度で

ジョブスケジューラで簡単にできればそれがベストですが、

なんらかの事情やそんな機能がなかった場合は、再集計するコードを書いておくと幸せになります

hive の json カラムには気をつけろ

hive には json 型があり、スキーマレスっぽい感覚で使えます

しかし使えるからと言って何でもつっこんで、1行なのに画面いっぱいの json で埋まるみたいなことをされるとさすがに hive さんも落ちます気を付けてください

まとめ

  • fluentd 最高
  • Hadoop の自前運用には割と手間暇かかる
  • 分析チームの人がデータをRで好きにいじれる環境を作った
  • プラットフォーム管理画面のスクレイピングは業が深い

次は id:GUSSAN さんです。

-- 追記 --

HDFS 上のファイル集約のくだりですが、ご指摘を頂いたのでほそくすると、

容量を節約したいからではなく、NameNode のメモリが節約できるということです

あと、mapタスクの数を減り mapの起動コストも減ることになります (こっちは失念してました!)

カニでもできるRailsでアクセスログ実装

こんにちはこんにちは

Rails 4 でさくっとアクセスログ出そうと思ったけど、思ったよりさくっといかなかったので実装方法を残しておく

ログの項目

これくらいの簡単な内容

  • 時間
  • ユーザー名
  • リクエストURI
  • USER AGENT

独自ログの設定

まず、Railsのログじゃなくて独自のログを出したかったので、

config/environment.rb

    # Load the Rails application.
    require File.expand_path('../application', __FILE__)
    
    class AccessLogger < ::Logger
      class NoHeaderLogDevice < ::Logger::LogDevice
        def add_log_header(file)
        end
      end
    
      class AccessFormatter < ::Logger::Formatter
        def call(severity, timestamp, progname, msg)
          "#{msg}\n"
        end
      end
    
      def initialize(logdev, shift_age = 0, shift_size = 1048576)
        super(nil, shift_age, shift_size)
    
        @formatter = AccessFormatter.new
    
        if logdev
          @logdev = NoHeaderLogDevice.new(
            logdev,
            :shift_age => shift_age,
            :shift_size => shift_size
          )
        end
      end
    
    end
    
    # Initialize the Rails application.
    Kaniapp::Application.initialize!

解説

ソースの中身を解説すると、

ログファイル作成時に出る邪魔なヘッダー(# Logfile created on ...)を抹殺

  class NoHeaderLogDevice < ::Logger::LogDevice
    def add_log_header(file)
    end
  end

ログフォーマットを指定

  class AccessFormatter < ::Logger::Formatter
    def call(severity, timestamp, progname, msg)
      "#{msg}\n"
    end
  end

config/environments/production.rb に

config.access_logger = AccessLogger.new('log/skv_access.log')

を追加

呼び出し

ApplicationController から

before_filter { access_log if Rails.env.production? }

private
def access_log
  return if session['warden.user.user.key'].nil?
  @user = User.find_by_id(session['warden.user.user.key'][0][0])
  return if @user.nil?
  now = Time.now.to_s.gsub(/ \+0900/,'')
  request_uri = URI.unescape(request.env['REQUEST_URI'])
  remote_ip = request.env['HTTP_X_FORWARDED_FOR'] || request.remote_ip
  user_agent = request.env['HTTP_USER_AGENT']
  row = "#{now}\t#{@user.username}\t#{request_uri}\t#{remote_ip}\t#{user_agent}"
  Kaniapp::Application.config.access_logger.info(row)
end

このやり方で君だけの最強のアクセスログを作ろう!!