ruby + mongoDBでapacheログの解析システムを作る


備忘録です。
もうツールに操られるのは嫌なので、
rubyとmongoDBで生ログの解析ができるようにしようと思っています。

mongoDBを選んだのは、特に意味があるわけではないですけど、
大量のデータになったら、MySQLじゃきついんではないかなという
思い込みと、noSQL使ってみたかったというだけです。

まだまだプロトですが、
・ログをパースして、DBにぶち込むスクリプト(ruby_log.rb)
・簡易的に集計するスクリプト(count.rb)
だけ作りました。

パースの方は
・ApacheLogRegexを使って、ログをパース
・色々フィルターかけて、扱いやすい形に変換
・MongoDBにぶち込む
という流れです。
MongoDBについては、あえてDataMapperは使わないでやっています。
mongo_mapper使っちゃうと、MongoDBの良さが半減しそうな
感じがしたので、避けています。
どうせRailsとか使わないし。
(candyはちょっと使ってみたいと思ったんですけど、
うまく動作せず断念。。。)
リファクタリングすれば、もうちょっと綺麗に書けそうな気はします。
ほんとは設定系の内容は別ファイルにした方がよさそうですけどね。

集計の方はMap/Reduceを使ってみました。
いまいちMap/Reduceの使い方がよく分からなくて、
単純集計しか出来ていません。
あと、エンコード周りは面倒なので、後回しにしています。。。。
なので、検索キーワードはかなり微妙な集計結果になってるかもしれません。
もうちょっとMap/Reduceを調べて、複雑な集計をできるようにしようと思います。

ruby_log.rb

# -*- coding: utf-8 -*-
Encoding.default_external="UTF-8"
require 'mongo'
require 'apachelogregex'
require 'time'

#set the path of apache log file
LOGFILE = 'logfile.log'

    class LogFilter
        def main(obj)
            if(pattern(obj))
                res = self.file_type(obj)
            end
            return res
        end
        def file_type(obj)
            val = Hash.new()
            val['page'] = self.pages(obj['%r'])
            val['referer'] = obj['%{Referer}i']
            val['ip'] = obj['%h']
            val['ua'] = obj['%{User-Agent}i']
            val['ip_ua'] = obj['%h'] + "__" + obj['%{User-Agent}i']
            val['time'] = self.p_time(obj['%t'])
            search_kw = self.parse_kw(obj)
            if(search_kw)
                search_kw.each_pair do |x,y|
                    val['search'] = x.to_s
                    val['kw'] = y.to_s
                end
            else
                val['search'] = "none"
                val['kw'] = "none"
            end
            return val
        end
        def pattern(hsh)
        #set the file type to be removed
            file_paturns = [
                    /.gif/i,
                    /.png/i,
                    /.jpg/i,
                    /.swf/i,
                    /.css/i,
                    /.js/i,
                    /.ico/i,
                    /.mp3/i,
                    /.txt/i
                ]
        #set the user agent to be removed
            ua_fil = [
                    /bot/i,
                    /msn/i,
                    /ichiro/i,
                    /Hatena/i,
                    /Yahoo! Slurp/,
                    /findlinks/
                ]
            file_paturns.each do |reg|
                if(hsh['%r'].match(reg))
                    return false
                    break
                end
            end
            ua_fil.each do |reg|
                if(hsh['%{User-Agent}i'].match(reg))
                    return false
                    break
                end
            end
            return true
        end
        def pages(str)
            return str.gsub(/^GETs|^POSTs|^HEADs/,"").gsub(/sHTTP.+$/, "")
        end
    def p_time(str)
      str = str.gsub("[","").gsub("]","")
      mm = /^(d+)/([a-zA-Z]+)/(d+):([d:]+) ([+-]d+)/.match(str)
      post_time = Time.parse("#{mm[1]} #{mm[2]} #{mm[3]} #{mm[4]} #{mm[5]}")
      return post_time
     end
        def parse_kw(hsh)
            #set the search engine list
            list = {
                "google" => ["q", /google.co.jp/search?/],
                "google_com" => ["q", /google.com/search?/],
                "yahoo" => ["p", /search.yahoo.co.jp/search/]
            }
             str = hsh["%{Referer}i"]
             tmp = Hash.new

             list.each_pair do |x,y|
                    if(str.match(y[1]))
                        reg = /#{y[0]}=[^&]*/
                        tmp[x] = (str.scan(reg)[0]).gsub!("#{y[0]}=","")
                    end
            end
            if(!tmp.empty?)
                return tmp
            else
                return false
            end
         end
    end


fil = LogFilter.new
    con  = Mongo::Connection.new
    db      = con.db('log')
    logs = db.collection('Log')

# difinition of apache log format
    format = '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"'
    parser = ApacheLogRegex.new(format)

    i = 0
    File.foreach(LOGFILE) do |line|
        begin
                 res = fil.main(parser.parse(line))
                 if(res!=nil)
                    #p res
                    logs.insert(res)
                 end
        rescue ApacheLogRegex::ParseError => e
            puts "Error parsing log file: " + e.message
        end
    end

    puts "end file:#{LOGFILE}"

count.rb

# -*- coding: utf-8 -*-
Encoding.default_external="UTF-8"

require 'rubygems'
require 'mongo'
require 'uri'

class SingleMp
    def initialize(str)
        @str = str
        @con     = Mongo::Connection.new
        @db     = @con.db('log')
        @logs = @db.collection('Log')
    end
    attr_accessor :str

    def main
        self.printMp(self.getMp)
    end

    def getMp
        @map = "function() { emit(this.#{@str}, 1); }"
        @reduce = "function (k, vals) {" +
            "var sum = 0;" +
            "for (var i in vals) {" +
                    "sum += vals[i];" +
            "}" +
            "return sum;" +
        "}"
        @res = @logs.map_reduce(@map, @reduce)

        return @res
    end
    def printMp(obj)
        obj.find().each do |x|
            if(x['_id']!=nil)
                print (@str + ":t" + URI.unescape(x['_id']) + "t=>t" + x['value'].to_s + "n")
            end
        end
    end
end




mp_page = SingleMp.new('page')
mp_page.main

puts ("n---------------------nn")

mp_ua = SingleMp.new('ua')
mp_ua.main

puts ("n---------------------nn")

mp_ipua = SingleMp.new('ip_ua')
mp_ipua.main

puts ("n---------------------nn")
mp_ipua = SingleMp.new('referer')
mp_ipua.main


puts ("n---------------------nn")
mp_ipua = SingleMp.new('kw')
mp_ipua.main

puts ("n---------------------nn")
mp_ipua = SingleMp.new('search')
mp_ipua.main

*11/22 timeのパースがうまくいってなかったので、修正しました。
もっとスマートにパースできなかなー。

One Comment

  1. sliderman より:

    ちょうどやりたかったことを掲載いただいていて本当に助かります!

    が、実行するとエラーになってしまいます。

    ruby_log.rb:63:in `pattern’: undefined method `[]‘ for nil:NilClass (NoMethodError)
    from ruby_log.rb:62:in `each’
    from ruby_log.rb:62:in `pattern’
    from ruby_log.rb:15:in `main’
    from ruby_log.rb:122
    from ruby_log.rb:120:in `foreach’
    from ruby_log.rb:120

    もし差し支えなければRuby、gem list –localでの各バージョン結果などを教えていただくことは可能でしょうか?

    ご検討いただけますと助かりますm(__)m

Leave a Reply