Lambdaで昔懐かしのアクセスカウンタを作る

Lambdaを使うとS3アクセスログの処理が出来るので、EC2を使わずにちょっとした動的サイトとして使うことが出来そうです。簡単そうなところで、簡易的なアクセスログカウンタを作ってみました。S3のstatic website hostingのアドオンとしてどうでしょうか?

仕組み

S3のログ更新があったあと、Lambdaでログをパースして、index.htmlへのアクセス数をカウントします。カウントした数字は、S3上に「counter.txt」という名前のファイルでおいておきます。新規でログが来た場合に、このファイルの数字を更新していきます。


index.htmlの方には、このcounter.txtの数字を読み込んで、カウンター画像を作るコードをjavascriptで入れておきます。こうすることで、counter.txtが更新されると、カウンターがアップしていく仕組みです。


0.pngから0.pngまでをS3上においておき、カウンターに合わせてimgタグをおいてきます。

index.html内のJavaScriptはこのような形になります。

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script language="JavaScript">
$(function(){
   $.get("counter.txt", 
     function(data){
       var num = Number(data);
       var images = getImageFiles(num,5);
       for(var i=0;i<images.length;i++){
             $("div.counter").append('<img src="'+images[i]+'">');
       }
     }
  );
});
function getImageFiles(num,digit){
  var txt=""+num;
  var images = [];
  while(txt.length<digit){
    txt = "0" + txt;
  }
  for(var i = 0;i < txt.length;i++){
    images[i] = txt.charAt(i)+".png";
  }
  return images;
}
</script>

準備

上記のスクリプトが入ったHTML、0.pngから9.pngの数値画像、counter.txtをS3上に配置し、static website hostingをonにします。
次にアクセスログを出すバケットを決めて、S3とLambdaを関連づけます。詳細はLambdaを使って、CloudTrailログをCloudSearchに入れて検索するの事前準備(入力側)が参考になります。

Lambda Function

設定するLambda Functionはこのような形となります。

var aws = require('aws-sdk');
exports.aws = aws;

exports.handler = function(event, context) {
  var bucket = event.Records[0].s3.bucket.name;
  var key = event.Records[0].s3.object.key;
  if (key.indexOf("logs") !== 0) {
    context.done(null, "no update");
    return;
  }
  var region = event.Records[0].awsRegion;
  var s3 = new aws.S3({
    params : {
      region : region
    }
  });
  s3.getObject({
    Bucket : bucket,
    Key : key
  }, s3Handler(context, function(data) {
    var log = data.Body.toString();
    // count index.html access
    var lines = log.split("\n");
    var count = 0;
    console.log("line="+lines.length);
    for (var i = 0; i < lines.length; i++) {
      var line = lines[i];
      var attrs = line.split(" ");
      var method = String(attrs[9]);
      var contents = String(attrs[10]);
      if (method.indexOf("GET") !== -1) {
        if (contents === "/" || contents === "/index.html") {
          count++;
        }
      }
    }
    console.log("cout="+count);
    if (count === 0) {
      context.done(null, "no update");
      return;
    }
    s3.getObject({
      Bucket : bucket,
      Key : "counter.txt"
    }, s3Handler(context, function(data) {
      var counter = Number(data.Body.toString());
      counter += count;
      console.log("counter update:" + counter);
      s3.putObject({
        Bucket : bucket,
        Key : "counter.txt",
        Body : String(counter)
      }, s3Handler(context, function(data) {
        context.done(null, "success");
      }));
    }));
  }));
};
function s3Handler(context, f) {
  return function(err, data) {
    if (err) {
      context.done("error", "error" + err);
    } else {
      f(data);
    }
  };
}

処理の流れとしては、まずS3からログファイルを取得して、1行ごとに分解します。その行が"GETかつindex.htmlアクセス"であれば有効なアクセスとして、countを+1します。取得したログで1count以上あったら、S3からcount.txtを取得し、中の数値にcountを足して再度count.txtを更新します。

デモサイト

出来上がりはこのような形です。
http://lambda-access-counter.s3-website-us-west-2.amazonaws.com/

アクセスすると分かりますが、リロードしてもなかなか数字が変わりません。実はS3ログが来るまではカウンターがあがらないので、リアルタイムにカウンターをあげることができません、というオチでしたorz