node.jsとSocket.IOでログをリアルタイムっぽく表示

node.jsとSocketIOでなんか作ってみたい!!!
というわけでログファイルをリアルタイムっぽく表示するページを作ってみました。
※実際は一定時間ごとにログの追記分を配信しています

「開発サーバのアクセスログ、エラーログ、MySQLクエリログなどなど、わざわざターミナルでtailするのめんどくさいよ!」という人いたらぜひ><


以下、ログファイル1つだけなサンプルの構築手順です。

セットアップ

今回はCentOS6.0を使ってます。

依存パッケージのインストール

このへん入ってれば動きそうです

[root@cent6 ~]# yum groupinstall 'Base'
[root@cent6 ~]# yum groupinstall 'Development tools'
[root@cent6 ~]# yum install openssl-devel

5系でやる場合はtarのアップデートが必要だったと思います。

使うポートにアクセスできるようにしときましょう

やりかたは人それぞれ

[root@cent6 ~]# /etc/init.d/iptables stop
node.jsをインストール

本家から最新版ダウンロードしましょう

[root@cent6 src]# wget http://nodejs.org/dist/v0.6.5/node-v0.6.5.tar.gz
[root@cent6 src]# tar xvzf node-v0.6.5.tar.gz
[root@cent6 src]# cd node-v0.6.5
[root@cent6 node-v0.6.5]# ./configure
[root@cent6 node-v0.6.5]# make
[root@cent6 node-v0.6.5]# make install
npm(node.jsのパッケージマネージャ)をインストール

[root@cent6 ~]# curl http://npmjs.org/install.sh | sh
npmはnode.jsに統合されていました!@さんご指摘ありがとうございます><

expressとnode-devをグローバルインストール

コマンドとして使うものは-gつきでインストールしておきます

[root@cent6 ~]# npm install express -g
[root@cent6 ~]# npm install node-dev -g

expressはnode.jsのWEBアプリケーションフレームワーク
node-devはデバッグ用のnodeの代替コマンドで、ファイル更新時の再起動が必要なくなってます。

プロジェクト作成

開発用のフォルダ作成して必要なパッケージをインストールします。
ejsはexpressで使うテンプレートエンジン、
socket.ioは言わずと知れたWebSocket用のライブラリです

[mikeda@cent6 ~]$ mkdir -p work/node/
[mikeda@cent6 ~]$ cd work/node/
[mikeda@cent6 node]$ npm install express ejs socket.io
[mikeda@cent6 node]$ ls node_modules/
ejs  express  socket.io


expressプロジェクトを作成します。
テンプレートエンジンはejs、プロジェクト名はlog_watchです。

[mikeda@cent6 node]$ express -t ejs log_watch

   create : log_watch
   create : log_watch/package.json
   create : log_watch/app.js
   create : log_watch/public
   create : log_watch/routes
   create : log_watch/routes/index.js
   create : log_watch/views
   create : log_watch/views/layout.ejs
   create : log_watch/views/index.ejs
   create : log_watch/public/javascripts
   create : log_watch/public/images
   create : log_watch/public/stylesheets
   create : log_watch/public/stylesheets/style.css

   dont forget to install dependencies:
   $ cd log_watch && npm install

いったん起動してみましょう

[mikeda@cent6 node]$ cd log_watch/
[mikeda@cent6 log_watch]$ node-dev app.js

ブラウザでサーバの3000番ポートにアクセスします
こういう画面が出ればOKです

プログラム修正

app.js

サーバサイドで動作するアプリケーション部分です
通常のHTTPアクセスとSocketIOを両方やってるのでわかりづらいかも。

/**
 * Module dependencies.
 */

var express  = require('express')
  , routes   = require('./routes')
  , socketio = require('socket.io')
  , fs       = require('fs')

var app = module.exports = express.createServer();
var io  = socketio.listen(app);

// 環境に合わせて設定
var server   = '192.168.189.129';
var port     = 3000;
var log_file = '/tmp/tmp.log';

// Configuration

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'ejs');
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});

app.configure('production', function(){
  app.use(express.errorHandler());
});

// Routes
// routes/index.jsに分離するようになったみたいだけど面倒だからいったんここに
//app.get('/', routes.index);
app.get('/', function(req, res){
  res.render('index', {
    title:  'Log Watch',
    server: server,
    port:   port
  });
});

// ファイルの監視
fs.open(log_file, "r", "0666",function(err,fd){
  if(err){ throw err; }

  //1秒ごとにファイルを見てファイルサイズに差が会ったら差分出力
  fs.watchFile(log_file, {interval:1000}, function(cur, prev){
//    if(+cur.mtime !== +prev.mtime){
    if(cur.size !== prev.size){  // !==だとローテート時にまずいかな
      var buf_size = 1024;

      for(var pos=prev.size; pos<cur.size; pos+=buf_size){
        if(err){ throw err; }

        var buf = new Buffer(buf_size);
        fs.read(fd, buf, 0, buf_size, pos,
          function(err, bytesRead, buffer){
            var log = buffer.toString('utf8', 0, bytesRead);
            // changeをログの内容つけて送信する
            io.sockets.emit('change', log);
          }
        );
      }
    }
  });
});

app.listen(port);
console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);
views/index.ejs

HTMLのボディです。Textareaを1つ作ります。

<h1><%= title %></h1>
<textarea id="log" cols="50" rows="10" readonly></textarea>
views/layout.ejs

HTMLのレイアウトです。
socket.ioライブラリ、クライアントサイドのJSファイルを読み込むようにします。(ホントは外出ししたほうがいいと思います。)

<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
    <script src="/socket.io/socket.io.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
    <script>
      var server = "<%= server %>";
      var port   = <%= port %>;
    </script>
    <script src="/javascripts/client.js"></script>
  </head>
  <body>
    <%- body %>
  </body>
</html>
public/javascripts/client.js

クライアントサイドのJavaScriptです。
WebSocketでサーバに接続してchangeメッセージを受け取ったら内容をTextareaに追記します。

$(document).ready(function() {
    var socket = io.connect('http://'+ server +':'+ port);

    socket.on('connect', function() {
        console.log('connect');
    });

    socket.on('disconnect', function(){
        console.log('disconnect');
    });

    socket.on('change', function(log) {
        console.log('change:' + log);
        $('#log').append(log);
    });

});


複数ブラウザからアクセスしてログを追記してみましょう

いけました!!!!