MP3ファイルのID3タグを一度に変更できるツールを作ってみた
昔にリッピングしたMP3ファイルには下のように iTunesで曲名が 01 AudioTrack 01.mp3 などと表示されています。これはiTunesの「情報を見る」ダイアログで変更できますが、いちいちGUIから変更していくのは大変です。そこで、ファイル名、曲名を書いたファイルから一括で変更できるツールを作ってみました。
iTunes, MP3 などに付いて・・・
iTunesは曲名等の情報は iTunes Library.itl (それを書き出した可読性のある iTunes Music Library.xml)というファイルで管理しています、このファイルを変更するとiTunesに表示される曲名を変更できます(この話しは次回にでも書きます)。
しかし、そこで曲を再生すると、なんと元の 01 AudioTrack 01.mp3 に戻ってしまいます!
実は wikipediaのID3タグ項目 にあるようにMP3,4(.mp3, .m4a)ファイルには アーティスト・作成年・曲名等の情報を書かられていて、iTunesは再生する時に、この情報から曲名を戻して(変更して)しまいます。
ID3タグを編集するツールは有料、オープンソースを含めたくさんあります。中には Amazonを検索し曲名等を一括で設定すてくれそうなツールがあったのですが、なぜか上手く行きませんでした。また、ID3タグのバージョンやエンコード(文字化け)の問題がありMacで動作するオープンソースのツールは良い物が見つけられませんでした。
TagLib, taglib-ruby
ID3タグを扱うライブラリーとしては、C++で書かれた TagLibが良いようで、Rubyから使える taglib-ruby があったので Rubyでツールを書く事にしました。
ちなみに、id3lib というライブラリーもあります。こちらはHome brewでコマンドラインツールもインストール出来るのですが、最近はメンテされてないようで曲名に日本語を入れる事が出来ないので断念しました。
ツール
下のコードのように taglib-rubyを使う事で、MP3ファイルを読み込み、タイトル (曲名)を設定し保存するツールが簡単にできました。MP3ファイルのパス名、曲名をタブ区切りで書いたファイルを用意する事で複数のMP3ファイルを一度に変更できます。
#!/usr/bin/env ruby require 'taglib' MP_FILE_CLASSES = {'.mp3' => TagLib::MPEG::File, '.m4a' => TagLib::MP4::File} def set_mp_audio_file_to_title(path, title) MP_FILE_CLASSES[File.extname(path)].open(path) {|mp| mp.tag.title = title; mp.save} puts " set #{path} title=#{title}" rescue puts " error: #{$!}" exit(-1) end if ARGV.count < 2 puts "Usage: #{$0} MPEG_AUDIO_FILE TITLE or" puts " #{$0} -f FILE_AND_TITLE_LIST" elsif ARGV[0] == '-f' IO.readlines(ARGV[1]).each do |line| path_title = line.split("\t") set_mp_audio_file_to_title(*path_title) if path_title.length == 2 end else set_mp_audio_file_to_title(ARGV[0], ARGV[1]) end exit(0)
Ruby開発者を増やすための教育について (8年間のRuby教育で得た知見)
わけあって昔作ったKeynoteを眺めていたら、かなり良い資料だと思い再掲載してみました (はてなダイアリー|ブログには初めてです)。 RubyWorld Conference 2013 の発表資料です。
Ruby、Ruby on Rails開発者を増やしたいと考えている開発マネージャーやリーダーの方は是非読んでみて下さい。社内で教育いくして行く際に役立つヒントがたくさん書かれていると思います (自画自賛 ^^;)。
Dockerを使いRuby on Railsアプリ、PostgreSQL、Nginxなどのコンテナーをクラウドサービスで動かしてみた
環境構築
普通に Boot2Docker をMacにインストールしました。Boot2Dockerは ここのブログの中ほどの画像のようにVitualBox上でDockerサーバーを動かし、Macの dockerコマンドがDockerサーバーと通信して動作します。
Dockerサーバーの作成・起動などは boot2docker コマンドで行います。
作成・起動は、
% boot2docker init % boot2docker up
これで、dockerコマンドが使えるようになりますが、通信用のDOCKER_HOST環境変数を設定する必要があります。
% export DOCKER_HOST=tcp://192.168.59.103:2375 % docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE nginx latest c43e4a994b02 4 hours ago 231.7 MB app latest 4b465cc33c7c 4 hours ago 602.7 MB pg latest 8f1dd8d424ad 4 hours ago 380.3 MB ....
dockerコマンドの詳細はドキュメント等を参考にして下さい。Dockerでば docker build コマンドで Docefile に従いイメージ(ファイルシステムのアーカイブ?)を作成します。そして、 docker run コマンドでコンテナー(仮想マシン?)を作成しイメージを実行します。
コンテナーの割り振り
Ruby on Railsのアプリケーションは通常、アプリケーションを動かすサーバー(Unicron等)、データベース(PostgreSQL, MySQL等)、画像CSS等の配信を行うWebサーバー(Nginx,Apache等)のサーバーソフトが必要になります。これらを1つのコンテナーの中で動かす事も出来ますが、今回はメンテナンス性等を考え、3つのコンテナーで動かす事にします
- pgコンテナー : RDBサーバー、PostgreSQL
- nginxコンテナー : Webサーバー、Nginxと画像/CSS等のコンテンツ
- appコンテナー : アプリケーション、Ruby, Ruby on Rails, Unicorn
ちなみに、今回参考にした Deploy Rails Applications Using Docker では RDBサーバー、アプリ+Nginxの2つのコンテナーで動いていいます。
Ruby on Rails アプリケーションの作成
シンプルな TODOアプリです。コードは GitHub あります。
作成手順
% rails new rails_app_with_docker % cd rails_app_with_docker % rails g scaffold todo due:date task:string % rake db:migrate % ruby s
その後 unicorn, pgをインストルし、production環境ではRDBがPostgreSQLになるように、またサーバーはUnicornに変更しました。
pgコンテナー
pgコンテナーは DockerのドキュメントにあるDockerizing a PostgreSQL serviceを、ほぼそのまま使っています。
行っているのは、ubuntu14.04にPostgreSQLをインストールし docker というデータベースを作成し、run でPostgreSQLサーバーが起動するようにしています。また、VOLUME 命令でバックアップ等で使うディレクトリーを他のコンテナーからマウント出来るようにしています。
- config/docker/pg/Dockerfile
FROM ubuntu:14.04 RUN apt-get update RUN apt-get install -y -q postgresql-9.3 libpq-dev postgresql-client-9.3 postgresql-contrib-9.3 USER postgres RUN /etc/init.d/postgresql start &&\ psql --command "CREATE USER docker WITH SUPERUSER PASSWORD 'docker';" &&\ psql --command "CREATE DATABASE docker WITH OWNER docker TEMPLATE template0 ENCODING 'UTF8';" RUN echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/9.3/main/pg_hba.conf RUN echo "listen_addresses='*'" >> /etc/postgresql/9.3/main/postgresql.conf EXPOSE 5432 VOLUME ["/etc/postgresql", "/var/log/postgresql", "/var/lib/postgresql"] CMD ["/usr/lib/postgresql/9.3/bin/postgres", "-D", "/var/lib/postgresql/9.3/main", "-c", "config_file=/etc/postgresql/9.3/main/postgresql.conf"]
appコンテナー
appコンテナーは rbdock Gem が生成するDockerfile を参考に作りました。内容は
- ubuntu14.04 + ruby2.1.2のインストール(後ほど説明します)
- Gemfileをコピーしてbundleコマンドで必要な Gem をインストール
- アプリケーションのコピー
- 不要なファイルの削除
やはり、VOLUME 命令でアプリ全体を他のコンテナーからマウント出来るようにしています、この一部は nginxコンテナーで使います。
- Dockerfile
FROM yuumi3/ruby:2.1.2 RUN mkdir /home/app WORKDIR /home/app ADD Gemfile /home/app/Gemfile RUN bundle install ADD . /home/app RUN rake tmp:clear RUN rake log:clear VOLUME ["/home/app"] ENTRYPOINT bin/start_server.sh
run で実行されるスクリプトでは、Rails用の環境変数の設定、データベースのマイグレーション、Unicornサーバーの起動です。
データベースのマイグレーションは、Dockerfileで指定したかったのですが、buildの際にはpgコンテナーと接続出来ないのでここで行っています。
- bin/start_server.sh
#!/bin/bash -x export RAILS_ENV=production export SECRET_KEY_BASE=`rake secret` rake db:migrate unicorn_rails -c config/unicorn.rb
ubuntu14.04 + ruby2.1.2のインストールは既に Docker Hubに登録してあるイメージを使っています。 このDockerfileもrbdock Gem が生成するDockerfile ほぼそのままです。ありがとうございます! 内容は
- Rubyをコンパイルするためのツール・ライブラリーのインストール
- Rubyのコンパイル・インストール
gemのアップデート、bundler gemのインストール
yuumi3/ruby:2.1.2作成時のDockerfile
FROM ubuntu:14.04 # Install basic dev tools RUN apt-get update && apt-get install -y \ build-essential \ wget \ curl \ git # Install package for ruby RUN apt-get install -y \ zlib1g-dev \ libssl-dev \ libreadline-dev \ libyaml-dev \ libxml2-dev \ libxslt-dev # Install package for sqlite3 RUN apt-get install -y \ sqlite3 \ libsqlite3-dev # Install package for postgresql RUN apt-get install -y libpq-dev # Install ruby-build RUN git clone https://github.com/sstephenson/ruby-build.git .ruby-build RUN .ruby-build/install.sh RUN rm -fr .ruby-build # Install ruby-2.1.2 RUN ruby-build 2.1.2 /usr/local # Install bundler RUN gem update --system RUN gem install bundler --no-rdoc --no-ri
nginxコンテナー
nginxコンテナーは Deploy Rails Applications Using Docker に書かれているものを参考にしています。
ただし、sedを使いnginxの設定ファイルを書き換える力業は Server Fault を参考にしました。通常のnginxの設定ファイルに環境変数の値を使う事は出来ないのですね(luaを組み込んだnginxなら出来るようです)。
- config/docker/nginx/Dockerfile
FROM ubuntu:14.04 RUN apt-get update RUN apt-get install -y nginx RUN echo "daemon off;" >> /etc/nginx/nginx.conf ADD default /etc/nginx/sites-available/default EXPOSE 80 VOLUME ["/var/log/nginx"] ENTRYPOINT /bin/sed -i "s/[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+/${APP_PORT_3000_TCP_ADDR}/" \ /etc/nginx/sites-available/default &&\ /usr/sbin/nginx
nginxの設定ファイルも、ほぼDeploy Rails Applications Using Docker に書かれているものです。
root に指定されている /home/app/public は appコンテナーでexportしている /home/app をマウント(共有)しています。これで画像やcssなどがnginxで配信できます。
- config/docker/nginx/default
cat config/docker/nginx/default server { server_name _; root /home/app/public; add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_redirect off; proxy_set_header Host $http_host; if (!-f $request_filename) { proxy_pass http://192.168.111.111:3000; break; } } }
Dockerイメージの作成と実行
各コンテナーは以下のRakeコマンドで作成 docker build 、実行 docker run しています。オプションの指定がたくさんあるので rakeコマンドにしてあります。
例えば、 nginxコンテナーの作成では -t nginx でイメージの名前を付け、次にDockefileのあるディレクトリーのパスを指定しています。 実行は * -d でバックグラウンド実行 * -p 80:80 はコンテナーのポート80を外部から80でアクセス出来るように設定 * --link app:app はappコンテナーの情報をnginxコンテナー内で app(例 APP_PORT_3000_TCP_ADDR 環境変数はappコンテナーのIPアドレス)で参照出来るようにする * --volumes-from app appコンテナーの VOLUME で指定されたディレクトリーをこのコンテナーでマウント(共有) * --name nginx 起動されたコンテナーに nginx という名前を付ける
namespace :docker do desc "Build Postgresql container" task :build_pg do sh "docker build -t pg config/docker/pg" end desc "Run Postgresql container" task :run_pg do sh "docker run -d -p 5432:5432 --name pg pg" sh "docker ps" end desc "Build Rails application container" task :build_app do Rake::Task['assets:precompile'].invoke sh "docker build -t app ." end desc "Run Rails application container" task :run_app do sh "docker run -d -p 3000:3000 --link pg:db --name app app" sh "docker ps" end desc "Build Nginx container" task :build_nginx do sh "docker build -t nginx config/docker/nginx" end desc "Run Nginx container" task :run_nginx do sh "docker run -d -p 80:80 --link app:app --volumes-from app --name nginx nginx" sh "docker ps" end end
DigitalOceanへdeploy
激安なクラウドサービス DigitalOcean に出来た Docker Image をデプロイしてみましょう。
1. DigitalOcean に SignUpし、サーバー(droplet)を作成
SIGN UPで登録し $ 0.015 / Hour のサーバーを選んでみました、OSはUbuntu 14.04 x64 。
メールでサーバー(droplet)のIPやアカウント情報が送られてきます。
2. Dockerのインストールと確認
Ubuntu - Docker Documentation の手順で最新版のDockerのインストールし、確認。
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9 $ sudo sh -c "echo deb https://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list" $ sudo apt-get update $ sudo apt-get install -y lxc-docker $ sudo docker run -i -t ubuntu /bin/bash /# .... /# exit $
3. 作ったDocker ImageをDocker Hubから取り込み実行
DockerコマンドでDocker HubからImageを取得し実行
$ sudo docker run -d -p 5432:5432 --name pg yuumi3/postgres:docker $ sudo docker run -d -p 3000:3000 --link pg:db --name app yuumi3/rails_app:todo $ sudo docker run -d -p 80:80 --link app:app --volumes-from app --name nginx yuumi3/nginx
ブラウザーでアクセスしTODOアプリの動作確認。
動いた ^^)/
DigitalOceanではサーバー(droplet)を停止しても、存在すれば課金されるのようなので 終わったらDestroyしてしまいましょう :-)
Docker感想
既にChefは使っていますが、今回Dockerを使って思った事は
- Chefはサーバー構築手順をコード化するものですが、Dockerはイメージを構築し利用するものなので、Chefのような考え方は切り換えないと行けない。例えば実行時に決まるIPアドレスなどはイメージ/Dockerfileには書けない。
- Dockerfileはシンプルな記述しか出来ないので、shell script等に頼る事になり、Rubyプログラマーの私には色々な事ができるChefの方が良いな
- イメージが出来てしまえば確かにサーバーへのデプロイは早いが、大きなイメージファイルをやり取りするので思ったほど早くはない。イメージをネットワーク的にデプロイ先に近いところ置けば良いのだろうか?
- 一つのサーバー上で複数のコンテナーが作れるので、今回のようにRDB,Web,アプリを分けておけばメンテナンス等でのダウンタイムが減らせるかも。
- 現在はクラウド+ Chefで満足しているので直ぐにDockerに移ろうとは私は思わないですが、Dockerをベースにしたサービスや新たな技術が生まれてきているので将来が楽しみな技術ですね。
はてなブログに引っ越しました。
今さらながら はてなブログに引っ越しました。
URLは http://yuumi3.hatenablog.com/ ですが、従来の http://d.hatena.ne.jp/yuum3/ をアクセスしても新URLにリダイレクトします。RSSも従来のままでもだいじょぶです。
Herokuへのデプロイのデモで失敗しないための手順
先日の セミナーの中で Heroku を使えば Ruby on Rails アプリの公開(デプロイ)は超簡単! という話しをしデモを行ったのですが、見事に失敗してしまいました ^^;
2度と失敗しないように記事を書きました。
失敗する手順
1. セミナー等でデモを行う場合は事前に練習!!
と言うことで以下の手順で無事にデプロイ出来る事を確認
$ heroku create $ git push heroku master $ heroku run rake db:migrate $ heroku open
3. セミナー本番
1. と同じ手順でデプロイを実行すると・・・・
$ heroku create $ git push heroku master ! No such app as xxxx-yyyy-9999.
なんと、エラー! 何度かリトライする内に原因が判ったのですが、焦っていたので失敗し、時間切れでデモは中止 (v_v)
失敗しない手順 (1)
1. セミナー等でデモを行う場合は事前に練習!!
と言うことで以下の手順で無事にデプロイ出来る事を確認
$ heroku create $ git push heroku master $ heroku run rake db:migrate $ heroku open
2. heroku コマンド でアプリを削除
$ heroku list === My Apps xxxx-zzzzz-2222 ← アプリのID. destroyコマンドで指定 $ heroku destroy xxxx-zzzzz-2222 ! WARNING: Potentially Destructive Action ... > xxxx-zzzzz-2222 ← アプリのID を再入力
3. セミナー本番
1. と同じ手順でデプロイを実行すれば 成功!
「クラウドxスマフォ時代のRuby on Rails入門」セミナーで使ったコードをGitHubに置きました
昨日、行われた クラウドxスマフォ時代のRuby on Rails入門 セミナーで使ったコードをGitHubに置きました。
簡単な Ruby on Rails で作ったサーバーと連携できる iOSアプリです。
コード、セミナー資料
JavaScriptの勉強に、Node.js, Angula.js を使って簡単ツールを作った
片手間でJavaScript書いてる Yuumi3 です。
時間が出来たので、今さらながら Node.js や 流行の Angula.js を学んでました。学ぶためには何かアプリを作るのが一番ということで、簡単なWebアプリの状態監視ツールを Node.js, Socket.IO, Koa, Angula.js, Mocha を使い作ってみました。
作るアプリについて
サーバー1台で運用しているサービスで、ソフトの更新などでアプリケーションサーバーをリスタートするタイミングは、今までは tail -f production.log でアクセス状況を見ながら行ってきました。しかし、面倒なので、ログをチェックしながらアプリのステータスを教えてくれるアプリを作ってみました。
ステータスは3つあって
構成
- サーバー側は Node.js
- クライアントは Angula.js
- サーバーから情報を Socket.IO を使いクライアントに push
- クライアントから最初のアクセスは http
- サーバー側のフレームワークは Koa
- サーバー側のテンプレートは Jade
- サーバー側ロジックのテストを Mocha で記述
- 判定に使うログは Ruby on Railsのログに UserAgentを追加したもの UserAgenのログ追加に付いては、こちら
クライアント側のAngula.js や Koa などこの程度のアプリではオーバースペックですが勉強のために使ってみました。
設定ファイル
"log_path" : "/home/rails_apps/sample/current/log/production.log", "effective_time" : 120, "crucial_condition" : {"path" : "^/orders"}, "pointless_condition" : {"user_agent" : "Googlebot|Yahoo!|Y!J|msnbot|bingbot|Baidu|Wget|Pingdom"} }
サーバー側
全てのコードはGitHub https://github.com/yuumi3/app_status に置きました。
メインのコード(Koa)
行っている事は
- Koaの設定
- / へのアクセスがあるとログファイルの最後8Kbyteを読み込み、サーバーにindex.htmlを配信
- クライアントとの間にSocketが繋がると、現在の状態、最新のログをpush
- ログファイルを file-tail を使い監視、変更があったら読み込み、現在の状態、最新のログをpush
- アクセスの有効時間以上なにもなかったら、現在の状態、最新のログをpush
- CSS, JS等の配信 koa-static
use strict'; var app = require('koa')(); var router = require('koa-router'); var serve = require('koa-static'); var views = require('koa-views'); var fileTailer = require('file-tail'); var railsLog = require('./rails-log'); app.use(serve('./public')); app.use(views('views', {default: 'jade'})); app.use(router(app)); app.get('/', function *(next) { yield log.readTail(8 * 1024); yield this.render('index', {}); }); var server = require('http').Server(app.callback()); var io = require('socket.io')(server); var refreshTimeout; if (process.argv.length < 3) { console.log("Usage: " + process.argv[0] + " --harmony " + process.argv[1] + " CONFIG_PATH"); process.exit(1); } var config = require(process.argv[2]); var log = railsLog(config); var tail = fileTailer.startTailing(config.log_path); function sendStatus(log) { var status = log.isCrucial() ? 'red' : (log.isIdle() ? 'green' : 'yellow'); if (log.logs.length == 0) { console.log(" - status: " + status); } else { var lastLog = log.logs[log.logs.length - 1]; console.log(" - status: " + status + " log: " + lastLog.path + " " + lastLog.time.toISOString()); } io.sockets.emit('update', {status: status, logs: log.logs}); setAutoRefresh(); } function setAutoRefresh() { if (refreshTimeout) clearTimeout(refreshTimeout); refreshTimeout = setTimeout(sendStatus, config.effective_time * 1000, log); } io.on('connection', function(socket){ sendStatus(log); tail.on('line', function(line) { if (log.addOneLine(line)) { sendStatus(log); } }); }); server.listen(3000); console.log("Start server.");
Railsログのライブラリー
node.js のモジュールとして RailsLogクラス(rails-log.js)を作ってみました。ログファイルの読み込み readLogFile は ES6の Generator (コルーチン、継続?) を使っています。Generator を手軽に使える Co ライブラリーや Co を fs (ファイルを扱うライブラリー)に組み込んだ co-fs などを使っています
'use strict'; var co = require('co'); var fs = require('co-fs'); module.exports = railsLog; function railsLog(config) { return new RailsLog(config); } function RailsLog(config) { this.path = config.log_path; this.crucial_condition = prepareCondition(config.crucial_condition); this.pointless_condition = prepareCondition(config.pointless_condition); this.effective_time = config.effective_time || 120; this.max_log_size = config.max_log_size || 100; // not implemented yet this.logs = []; this.lastAddedLine = ''; } RailsLog.prototype.readTail = function*(len) { this.logs = [] var content = yield readLogFile(this.path, len); content.split("\n").forEach(function(log) {this.addOneLine(log)}, this); } RailsLog.prototype.addOneLine = function(logString) { if (logString == this.lastAddedLine) return false; this.lastAddedLine = logString; var m = logString.match(/Started\s+(\w+)\s+\"(.*?)\"\s+for\s+([\d\.]+)\s+at\s+([\d: +-]+)\s+by\s+(.*)$/); if (!m) return false; var log = {method: m[1], path: m[2], ip: m[3], time: new Date(m[4]), user_agent: m[5]}; if (isMatchCondition(log, this.pointless_condition)) return false; this.logs.push(log) return true; } RailsLog.prototype.isCrucial = function() { var cond = this.crucial_condition; addEffectiveTimeCondition(cond, this.effective_time); return this.logs.some(function(e) { return isMatchCondition(e, cond); }) } RailsLog.prototype.isIdle = function() { var cond = {} addEffectiveTimeCondition(cond, this.effective_time); return !this.logs.some(function(e) { return isMatchCondition(e, cond); }) } RailsLog.prototype.last = function(lines) { return this.logs.slice(-lines); } exports.railsLog = function(config) {new RailsLog(config);} function *readLogFile(path, len) { var fd = yield fs.open(path, 'r'); var stat = yield fs.fstat(fd); var buff = new Buffer(len); var pos = stat.size - len; if (pos < 0) { pos = 0; } var len_buff = yield fs.read(fd, buff, 0, len, pos); return len_buff[1].toString(); } function isMatchCondition(e, conds) { for (var key in conds) { if (key == 'time') { if (e[key].getTime() < conds[key]) return false; } else { if (!e[key].match(conds[key])) return false; } } return true; } function prepareCondition(hash) { return Object.keys(hash || {}).reduce(function(conds, key){ conds[key] = new RegExp(hash[key]); return conds }, {}); } function addEffectiveTimeCondition(conds, effective_time) { var d = new Date(); conds['time'] = d.setTime(d.getTime() - effective_time * 1000); }
クライアント側
index.jade
実はスタティックなページですが、テンプレートエンジンを使ってみたかったので使用しました。Angula.js はごく基本的な部分しか使っていません。
doctype html html(ng-app='statusApp') head title application status script(src='/socket.io/socket.io.js') script(src='assets/javascripts/angular.min.js') script(src='assets/javascripts/ng-socket-io.min.js') script(src='assets/javascripts/status.js') link(rel='stylesheet' href='assets/stylesheets/bootstrap.min.css') link(rel='stylesheet' href='assets/stylesheets/bootstrap-theme.min.css') link(rel='stylesheet' href='assets/stylesheets/status.css') body div(ng-controller="StatusController") div(class="panel {{status_class}}") div.panel-heading {{status_text}} div.panel-body div.logs table(class='table table-striped') tbody tr(ng-repeat="log in logs") td {{log.time | date : 'yyyy-MM-dd HH:mm:ss'}} td {{log.method}} td {{log.path}} td {{log.ip}} td {{log.user_agent}}
status.js
クライアント側のJS, サーバーからのステータス、ログを受け取り Angulaへ渡すだけの簡単なお仕事。Socket.IOは ngSocketIO を使ってます。
angular.module('statusApp', ['socket-io']) .controller('StatusController', function($scope, socket) { socket.on('update', function(data) { $scope.logs = data.logs; switch(data.status) { case 'red': $scope.status_class = 'panel-danger'; $scope.status_text = 'Busy'; break; case 'yellow': $scope.status_class = 'panel-warning'; $scope.status_text = 'Warning'; break; case 'green': $scope.status_class = 'panel-success'; $scope.status_text = 'Idle'; break; default: $scope.status_class = 'panel-info'; $scope.status_text = '???'; break; } }); });
テスト
サーバー側の rails-log.js のテストを Mocha + expect.js でテストを書いてみました。
- Co を使ったコードもあるので co-mocha を入れています。
- 時間に関係したテストには Sinon.js の Fake timers を使い new Date() の戻り時間を設定しています。
'use strict'; require('co-mocha'); var sinon = require('sinon'); var expect = require('expect.js'); var railsLog = require('../rails-log'); describe('RailsLog', function(){ var log; var config = { "log_path" : "./test/sample.log", "effective_time" : 120, "crucial_condition" : {"path" : "^/(cart|order)"}, "pointless_condition" : {"user_agent" : "Googlebot|bingbot"} }; beforeEach(function(){ log = railsLog(config); }) describe('addOneLine', function(){ var accessLine = 'I, [2014-07-06T08:45:33.076218 #1314] INFO -- : Started GET "/products?s=3"' + ' for 203.140.209.190 at 2014-07-06 08:45:33 +0900 by Mozilla/4.0 (compatible; MSIE 8.0;' + ' Windows NT 5.1; Trident/4.0; GTB7.5; .NET CLR 1.0.3705; .NET CLR 1.1.4322; YTB730; ' + '.NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C)'; it('指定したログ文字列がアクセスログなら追加されtrueが戻る', function(){ expect(log.addOneLine(accessLine)).to.be.ok(); expect(log.logs.length).to.eql(1); }) it('指定したログ文字列がアクセスログでなければ追加されずfalseが戻る', function(){ expect(log.addOneLine('I, [2014-07-06T08:44:01.643727 #1311] INFO -- : Completed 200 OK in 12ms ' + '(Views: 6.9ms | ActiveRecord: 1.6ms)')).to.not.be.ok(); expect(log.logs.length).to.eql(0); }) it('ログは解析されている', function(){ log.addOneLine(accessLine); expect(log.logs[0]).to.eql({ip: "203.140.209.190", method: "GET", path: "/products?s=3", time: {}, user_agent: "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; GTB7.5; .NET CLR 1.0.3705; .NET CLR 1.1.4322; " + "YTB730; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C)"}); expect(log.logs[0].time).to.eql(new Date(2014,7 - 1,6,8,45,33)); }) it('同じログ文字列ならアクセスログなら追加されない', function(){ expect(log.addOneLine(accessLine)).to.be.ok(); expect(log.addOneLine(accessLine)).to.not.be.ok(); expect(log.logs.length).to.eql(1); }) }) describe('readTail', function(){ it('ログファイルが読み込まれる', function*(){ yield log.readTail(2000); expect(log.logs.length).to.eql(2); expect(log.logs[0].path).to.eql("/admin/scheduled_tasks/beat"); expect(log.logs[1].path).to.eql("/products/211"); }) it('pointless_conditionに該当するログは読み込まれない', function*(){ config["pointless_condition"] = {"user_agent" : "Googlebot|Wget"}; log = railsLog(config); yield log.readTail(2000); expect(log.logs.length).to.eql(1); }) }) describe('isCrucial', function(){ var accessLineIn = 'I, [2014-07-06T08:45:33.076218 #1314] INFO -- : Started GET "/order/confirm"' + ' for 203.140.209.190 at 2014-07-06 08:45:33 +0900 by Mozilla/4.0'; var accessLineOut = 'I, [2014-07-06T08:35:33.076218 #1314] INFO -- : Started GET "/order/confirm"' + ' for 203.140.209.190 at 2014-07-06 08:35:33 +0900 by Mozilla/4.0'; var clock; beforeEach(function(){ clock = sinon.useFakeTimers((new Date(2014,6,6, 8,45,35)).getTime()); }) afterEach(function(){ clock.restore(); }) it('crucial_conditionに該当するログが無ければfalseを戻す', function(){ expect(log.isCrucial()).to.not.be.ok(); }) it('crucial_conditionに該当しeffective_time以内のログがあればtrueを戻す', function(){ log.addOneLine(accessLineIn); expect(log.isCrucial()).to.be.ok(); }) it('crucial_conditionに該当しeffective_time以外のログがあればfalseを戻す', function(){ log.addOneLine(accessLineOut); expect(log.isCrucial()).to.not.be.ok(); }) }) describe('isIdle', function(){ var accessLine = 'I, [2014-07-06T08:45:33.076218 #1314] INFO -- : Started GET "/products"' + ' for 203.140.209.190 at 2014-07-06 08:45:33 +0900 by Mozilla/4.0'; var clock; beforeEach(function(){ clock = sinon.useFakeTimers((new Date(2014,6,6, 8,45,35)).getTime()); }) afterEach(function(){ clock.restore(); }) it('effective_time以内のログが無ければtrueを戻す', function(){ expect(log.isIdle()).to.be.ok(); }) it('effective_time以内のログがあればfalseを戻す', function(){ log.addOneLine(accessLine); expect(log.isIdle()).to.not.be.ok(); }) }) describe('last', function(){ var accessLine = 'I, [2014-07-06T08:45:33.076218 #1314] INFO -- : Started GET "/products"' + ' for 203.140.209.190 at 2014-07-06 08:45:33 +0900 by Mozilla/4.0 no='; var clock; beforeEach(function(){ for(var i = 0; i < 10; i++) { log.addOneLine(accessLine + i); } }) it('指定した行数のログを戻す', function(){ expect(log.last(5).length).to.eql(5); expect(log.last(5)[0].user_agent).to.eql("Mozilla/4.0 no=5"); expect(log.last(5)[4].user_agent).to.eql("Mozilla/4.0 no=9"); }) it('ログの終わりから指定した行を戻す', function(){ expect(log.last(5)[0].user_agent).to.eql("Mozilla/4.0 no=5"); expect(log.last(5)[4].user_agent).to.eql("Mozilla/4.0 no=9"); }) }) })
感想とか
良い点
- npm, Mocha などRubyに似たツールがあり、Ruby プログラマーには入りやすい世界でした
- Node.js は ブラウザーでJSを書くのに比べ書きやすい気がする
- Generatorを使うとコールバック地獄にならない(??)
- Mochaは幾つかのスタイルが選べるがexpect.jsと組み合わせると RSpec と似た表記で書けうれしい
- Angula.jsのデータバインディングは素晴らしい、チャンスがあれば Rails + Angula.js でアプリを作ってみたい
良くない点
- いろいろと覚える事がある、またどのライブラリーを使ったら良いのかが判らない。Rubyも同じだけど ^^;
- JSはRubyに比べると、いろいろと面倒くさい。たとえば null, undefined に対して .toString() 出来ないなど細かい事でコードが増えがち。またイテレータ系のメソッドも少ない
- function() を書くのが面倒! CoffeeScriptはまだGeneratorに対応してないようなので今回は見送りましたが、 早くCoffeeScript か、ES6 の Arrow functions が使えるようになると良いな
- Generatorは使うと非同期なコードが書きやすくなるが、やはり非同期で動作するので色々とハマりやすい
- Angula.js 素晴らしいが、ちょっとしたhtmlタグのミスでも動かなくなったりする。慣れの問題かな? それかIDE WebStorm を使えば良いのだろうか?
次のステップは、Rails + Angula.js でView は全てJSのアプリを書いてみたい。