Wiz テックブログ

Wizは、最新のIoTやICTサービスをお客様に届ける「ITの総合商社」です。

Apacheのデータをrubyで整える

こんにちは。バックエンドエンジニアの河内です。

先日、Apacheのログデータの解析結果をレポートするタスクが入ってきました。われわれの業務の大半はプロダクト開発なのですが、その中には、実際に要件を策定しコードを書いていく純粋な開発タスクもあれば、その開発のための技術調査(スクラムであれば「スパイク」でしょうか)もあります。

前者はなんとなく工数を把握したうえであとは実装していく…といった感じですが(それでもスケジュール管理は欠かせません)、後者は暗中模索でおこなわなければならないタフなタスクです。

また、それとは別に、開発中の案件とは関係なく不具合などの調査が入ってくることがあるかと思います。なんとなくやることは見えていて、あとはどれだけ速くこなすか…といったタイムトライアル的なタスクです。今回のログデータ解析タスクもそんな分類になるのかな、と思っています。

作業概観

さて、ログを調べた結果、今回解析が必要なデータは以下の5行と導けました。*1

IPアドレスなど、一部の情報は適当にマスクしてます。下記の内容をlogsというファイル名で保存します。

000.000.000.000 - - [02/Nov/2021:15:41:14 +0900] "POST /awesome HTTP/1.1" 200 5009 "https://xxx.dev/awesome" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/181.0.401558652 Mobile/15E148 Safari/604.1"
000.000.000.000 - - [04/Oct/2021:20:04:09 +0900] "POST /awesome HTTP/1.1" 200 4868 "https://xxx.dev/awesome" "Mozilla/5.0 (Linux; Android 11; SO-03L Build/55.2.D.0.447; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/94.0.4606.61 Mobile Safari/537.36 Line/11.17.1/IAB"
000.000.000.000 - - [04/Oct/2021:19:49:50 +0900] "POST /awesome HTTP/1.1" 200 4870 "https://xxx.dev/awesome" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari Line/11.17.0"
000.000.000.000 - - [04/Oct/2021:18:11:32 +0900] "POST /awesome HTTP/1.1" 200 4865 "https://xxx.dev/awesome" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1"
000.000.000.000 - - [03/Oct/2021:01:33:15 +0900] "POST /awesome HTTP/1.1" 200 4866 "https://xxx.dev/awesome" "Mozilla/5.0 (Linux; Android 9; SO-04J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Mobile Safari/537.36"

前回、別のメンバーが同様の対応をおこなったときのフォーマットは以下のとおりだったそうなので、それに従います。

・アクセス日時: 2021-01-01 00:00:00
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.8
    ブラウザ: Google
    ブラウザのバージョン: 181.0.401558652
    デバイス: iPhone

あとは文字列を拾って特定フォーマットに落とし込めばよいのですが…対象は複数行あります。目で追って手で拾う…というのは不正確ですし疲れるので避けたいです。

また、こういうタスクは一回やりおおせても、おかわりとして同じような作業依頼が再びやってくるのが常です。

せっかくテキストは規則的に並んでいることですし、ちゃちゃっとコードを書くことにします。こういうときに自分はrubyを使うことが多いです*2

今回のようなタスクを何度かこなしているので、rbenvで特定バージョンのrubyが動作する環境が用意されています(今回の例はruby 2.6.3p62)。

作業ディレクトリの中は、以下のような感じに配備しました。logsは先ほどの5行のログが書かれたファイルで、処理をおこなうreport.rbがあり、処理後にレポート内容がREPORT.txtに吐き出されるようにします。

./
  logs
  report.rb
  REPORT.txt(report.rbにより生成される)

コードを書いていく

ログをパースする

それでは、report.rbにコードを書いていきます。Apacheログのパーサーライブラリを使い、以下のように書きました。ログフォーマットは、Apacheのものをそのまま流用できるようです。

require 'apachelogregex'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)
    puts result; exit # 出力確認
end

コードで一行目パースしたところで止めてますが、こんな感じで取得できます。

{"%h"=>"000.000.000.000", "%l"=>"-", "%u"=>"-", "%t"=>"[02/Nov/2021:15:41:14 +0900]", "%r"=>"POST /awesome HTTP/1.1", "%>s"=>"200", "%b"=>"5009", "%{Referer}i"=>"https://xxx.dev/awesome", "%{User-Agent}i"=>"Mozilla/5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/181.0.401558652 Mobile/15E148 Safari/604.1"}

日時を読みやすくする

Apacheの日時をrubyでフォーマットする…誰かやってそうですよね? されてる方がいらっしゃったので拝借します。

require 'apachelogregex'
+ require 'time'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)

+    datetime = Time.strptime(result["%t"], '[%d/%b/%Y:%H:%M:%S %z]').strftime("%Y-%m-%d %H:%M:%S")
    puts datetime; exit # 出力確認
end

日時も読みやすくなりました。

2021-11-02 15:41:14

ホスト名を取得する

require 'apachelogregex'
require 'time'
+ require 'resolv'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)

    datetime = Time.strptime(result["%t"], '[%d/%b/%Y:%H:%M:%S %z]').strftime("%Y-%m-%d %H:%M:%S")

+     host_name = Resolv.getname(result["%h"])
    puts host_name; exit # 出力確認
end

サンプルではIPアドレスが適当ですが、実際のIPでアドレスではホスト名も取得できました。

ユーザーエージェントから各種情報を取得する

ユーザーエージェントはとくに目で見ての作業はめんどうなので、ライブラリに任せましょう。

require 'apachelogregex'
require 'time'
require 'resolv'
+ require 'user_agent_parser'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)

    datetime = Time.strptime(result["%t"], '[%d/%b/%Y:%H:%M:%S %z]').strftime("%Y-%m-%d %H:%M:%S")

    host_name = Resolv.getname(result["%h"])

+     user_agent_part = result["%{User-Agent}i"]
+     user_agent = UserAgentParser.parse(user_agent_part)

+     os_name, os_version = user_agent.os.to_s.split(" ")

+     browser_version = user_agent.version.to_s
+     browser_name = user_agent.family.to_s

+     device_name_family = user_agent.device.family.to_s

+     device_name = device_name_family == "iPhone" ? device_name_family : "#{user_agent.device.brand} #{user_agent.device.model}"
end

iPhoneAndroidのときの表示で違和感があったので、条件分岐してます*3

整形する

仕上げに入っていきます。フォーマットに従い出力します(ログ一行ごとのブロックで適当に仕切り線を入れて表示)。

require 'apachelogregex'
require 'time'
require 'resolv'
require 'user_agent_parser'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)

    datetime = Time.strptime(result["%t"], '[%d/%b/%Y:%H:%M:%S %z]').strftime("%Y-%m-%d %H:%M:%S")

    host_name = Resolv.getname(result["%h"])

    user_agent_part = result["%{User-Agent}i"]
    user_agent = UserAgentParser.parse(user_agent_part)

    os_name, os_version = user_agent.os.to_s.split(" ")

    browser_version = user_agent.version.to_s
    browser_name = user_agent.family.to_s

    device_name_family = user_agent.device.family.to_s

    device_name = device_name_family == "iPhone" ? device_name_family : "#{user_agent.device.brand} #{user_agent.device.model}"

+     text = <<~"TXT"
+         ・アクセス日時: #{datetime}
+         ・IPアドレス: #{result['%h']}
+         ・IPアドレスのホスト名: #{host_name}
+         ・UserAgentからの情報
+             OS: #{os_name}
+             OSのバージョン: #{os_version}
+             ブラウザ: #{browser_name}
+             ブラウザのバージョン: #{browser_version}
+             デバイス: #{device_name}
+     TXT
end

+ File.write('REPORT.txt', texts.join("-" * (50) + "\n"))

出力結果は以下のような感じです。

・アクセス日時: 2021-11-02 15:41:14
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.8
    ブラウザ: Google
    ブラウザのバージョン: 181.0.401558652
    デバイス: iPhone
--------------------------------------------------
・アクセス日時: 2021-10-04 20:04:09
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: Android
    OSのバージョン: 11
    ブラウザ: LINE
    ブラウザのバージョン: 11.17.1
    デバイス: SonyEricsson SO-03L
--------------------------------------------------
・アクセス日時: 2021-10-04 19:49:50
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.2
    ブラウザ: LINE
    ブラウザのバージョン: 11.17.0
    デバイス: iPhone
--------------------------------------------------
・アクセス日時: 2021-10-04 18:11:32
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.7.1
    ブラウザ: Mobile Safari
    ブラウザのバージョン: 14.1.2
    デバイス: iPhone
--------------------------------------------------
・アクセス日時: 2021-10-03 01:33:15
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: Android
    OSのバージョン: 9
    ブラウザ: Chrome Mobile
    ブラウザのバージョン: 94.0.4606.61
    デバイス: SonyEricsson SO-04J
--------------------------------------------------
・アクセス日時: 2021-10-02 13:52:21
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.7.1
    ブラウザ: Mobile Safari UI/WKWebView
    ブラウザのバージョン: 
    デバイス: iPhone

まとめ

タイムトライアル的なタスクは、ありもののライブラリや先人のナレッジ、自分の過去のメモを生かしてさくっと解決していく必要があります。一方で、一回だけでなく何度かおかわりが来ることを想定し、再現できるようにしておくとよいのかな、と思います。

最後に

Wizではエンジニアを募集しております。 興味のある方、ぜひご覧下さい。

careers.012grp.co.jp

*1:この行を特定するのにも要件からDBから特定条件でクエリを投げ、得られた日時とログデータのアクセス日時を突合しなければならない…というタスクがありました。

*2:みなさんも、手になじんだ言語があるのではないでしょうか。

*3:たとえば、iPadなどはなかったので今回は考慮してません。アプリ開発でないので、とりあえず条件を満たして早さ優先。