あるユーザーがブラウザーに皆さんのサイトの URL を入力します。するとユーザーは http://cool.si.te/ から http://cool.si.te/index.html へと導かれ、その HTML がロードされます。それに伴い、CSS や JavaScript、画像がロードされます。Web サーバーは、こうしたすべてのアクティビティーを、ほとんどのユーザーが気にもかけない些細なことまで含めて監視しています。もし何らかの問題が発生し、その原因が、画像ファイルが見つからないことを悪用した攻撃によるものに見える場合には、その痕跡をログ・ファイルの中から見つけ出す方法を既に皆さんはご存知かと思います。また皆さんは Web トラフィック・アナライザーも使用してログ・ファイルを読み取り、サイトのトラフィックの傾向を監視しているかもしれません。しかし大抵の場合、Web サーバーのログはシステム管理者の下でほとんど使われることのないまま保管されています。ログの有効な使い方はごまんとあることを考えると、これは残念なことです。この記事では、より多くの価値を Web サーバーのログから引き出すための考え方と手法をいくつか説明します。ここではそうした手法を一般的な Apache サーバーに対してテストしましたが、他の多くのツールも同じログ・フォーマットを使用しているため、ここで紹介する情報は幅広く応用が可能なことがわかると思います。
Apache の構成や操作に関する主なエラーはエラー・ログにレポートされますが、この記事では、すべての HTTP リクエストのログが記録されているアクセス・ログに焦点を絞ります。この昔ながらのフォーマットの起源は NCSA (National Center for Supercomputing Applications: 米国立スーパーコンピュータ応用研究所) にあります。NCSA は Web に関する重要な技術革新の本拠地であり、そこから生まれた技術には、(後に Netscape ブラウザーとなった) Mosaic や、(Apache の最初のリリースのメイン・コード・ベースであった) HTTPd、そして動的 Web コンテンツ用の最初のメカニズムであった CGI (Common Gateway Interface) などがあります。NCSA の HTTPd は Common Log Format と呼ばれるデフォルトのフォーマットととなり、その後 Apache に採用されました。Common Log Format は今でも Web 上の多くのツールで使われています。
リスト 1 は Common Log Format で記述された行の一例です。
リスト 1. Common Log Format で記述された行
125.125.125.125 - uche [20/Jul/2008:12:30:45 +0700] "GET /index.html HTTP/1.1" 200
2345
|
表 1 はこの行の各フィールドを説明したものです。
表 1. Common Log Format で記述された行の各フィールド
| フィールド名 | 値の例 | 説明 |
|---|---|---|
| host | 125.125.125.125 | このリクエストを行った HTTP クライアントの IP アドレスまたはホスト名 |
| identd | - | 認証サーバー・プロトコル (RFC 931) でのクライアントの識別子。このフィールドはほとんど使われません。使わない場合の値は「-」です。 |
| username | uche | HTTP 認証されたユーザー名 (401 レスポンスによるハンドシェークを使います)。これは一部のサイトに見られるログインとパスワードのダイアログであり、Web ページに組み込まれた (ID 情報がサーバー・サイド・セッションに保存される) ログイン・フォームとは異なります。このフィールドを使わない場合 (例えば制限のないリソースに対するリクエストの場合など) には、値は「-」です。 |
| date/time | [20/Jul/2008:12:30:45 +0700] | 日付、時刻、タイムゾーンを表し、フォーマットは [dd/MMM/yyyy:hh:mm:ss +-hhmm] です。 |
| request line | "GET /index.html HTTP/1.1" | HTTP リクエストの先頭行を示し、メソッド (GET)、リクエストされたリソース、そして HTTP プロトコルのバージョンが含まれています。 |
| status code | 200 | レスポンスに使用される数値コードであり、リクエストに対する処理の結果を示します (例えば、成功、失敗、リダイレクト、あるいは認証要件など) |
| bytes | | レスポンス本体の転送バイト数 |
現在の多くのツールは、より多くの内容を表現可能な Combined Log Format をデフォルトで使用しています。リスト 2 はその一例です。
リスト 2. Combined Log Format の行
125.125.125.125 - uche [20/Jul/2008:12:30:45 +0700] "GET /index.html HTTP/1.1" 200
2345
"http://www.ibm.com/" "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9a8)
Gecko/2007100619
GranParadiso/3.0a8" "USERID=Zepheira;IMPID=01234"
|
この行は本来 1 行なのですが、記事の書式の制約から数行に分割してあります。Combined Log Format では、Common Log Format に referrer、user agent、cookie という 3 つのフィールドが追加されています。これらのフィールドは、cookie フィールドのみを (引用符を含めてすべて) 省略することや、cookie とuser agent の両フィールドを省略すること、あるいは 3 つのフィールドすべてを省略することもできます。表 2 は追加された 3 つのフィールドを説明したものです。
表 2. Combined Log Format で記述された行の各フィールド
| フィールド名 | 値の例 | 説明 |
|---|---|---|
| referrer | "http://www.ibm.com/" | あるサイトから別のサイトへとユーザー・エージェントがリンクをたどる場合、どの URL から現在のサイトが参照されてきたのかをレポートすることがよくあります。 |
| user agent | "Mozilla/5.0 (X11; U; Linux x86_64; en-US;
rv:1.9a8) Gecko/2007100619 GranParadiso/3.0a8" | そのリクエストを行ったユーザー・エージェントに関する情報を提供するストリング (例えばブラウザーのバージョンや Web クローラーなど)。 |
| cookie | "USERID=Zepheira;IMPID=01234" | HTTP サーバーが送信したクッキーの実際のキーと値のペアは、レスポンスに入れてクライアントに返送することができます。 |
ほとんどの人は Web サーバーのデフォルトを使用しますが、Apache のログのフォーマットは容易にカスタマイズすることができます。当然のことですが、カスタマイズする際にはカスタマイズの内容に応じて適切な調整を行う必要があり、この記事で紹介するコードの大部分も調整する必要があります。
これらのフォーマットがいかに適切に構成されているかを見てきましたが、必要なログ情報を得るためには、正規表現を使うと非常に簡単です。リスト 3 は、ログの 1 行を解析し、その情報の要約を書き出す単純なプログラムです。このプログラムは Python で作成されていますが、本質的な部分は正規表現の中に含まれているため、他の任意の言語に容易に移植することができます。
リスト 3. ログの 1 行を解析するコード
import re
#This regular expression is the heart of the code.
#Python uses Perl regex, so it should be readily portable
#The r'' string form is just a convenience so you don't have to escape backslashes
COMBINED_LOGLINE_PAT = re.compile(
r'(?P<origin>\d+\.\d+\.\d+\.\d+) '
+ r'(?P<identd>-|\w*) (?P<auth>-|\w*) '
+ r'\[(?P<date>[^\[\]:]+):(?P<time>\d+:\d+:\d+) (?P<tz>[\-\+]?\d\d\d\d)\] '
+ r'"(?P<method>\w+) (?P<path>[\S]+) (?P<protocol>[^"]+)" (?P<status>\d+)
(?P<bytes>-|\d+)'
+ r'( (?P<referrer>"[^"]*")( (?P<client>"[^"]*")( (?P<cookie>"[^"]*"))?)?)?\s*\Z'
)
logline = raw_input("Paste the Apache log line then press enter: ")
match_info = COMBINED_LOGLINE_PAT.match(logline)
print #Add a new line
#Print all named groups matched in the regular expression
for key, value in match_info.groupdict().items():
print key, ":", value
|
COMBINED_LOGLINE_PAT というパターンは Combined Log Format 用に設計されていますが、Combined Log Format は Common Log Format に 3 つのオプション・フィールドを追加しただけなので、このパターンは両方のフォーマットに使用することができます。このパターンは、名前つき捕捉グループという Python 特有の機能を使用して、ログの 1 行の各フィールドに論理名を割り当てています。他の形式の正規表現に移植するには、通常のグループを使用して、各フィールドを数値順に参照するだけです。私が非常に詳細にパターンを設定していることに注意してください。ステータス行をひとまとめにして取得するのではなく、より使いやすいように、HTTP メソッドとリクエスト・パス、そしてプロトコルのバージョンを別々に取得しています。また date/time も日付、時刻、タイムゾーンに分割しています。図 1 は、リスト 2 のログ行のサンプルに対してリスト 3 のコードを実行した場合の出力を示しています。
図 1. リスト 3 を実行した場合の出力
ログ・ファイルの中から数々の興味深い情報を得るためには、ロボットによるアクセスと人間によるアクセスとを区別する必要があります。Google や Yahoo! といった主要な検索エンジンは非常に強力なインデックス機能を使用しています。そのため、皆さんのサイトの人気がそれほど高くない場合であっても、ログの大部分がそうしたスパイダーによるトラフィックで占められているかもしれません。スパイダーによるトラフィックを 100% の精度で除外することはほぼ不可能ですが、ロボットに共通するパターンがログ・ファイルの中にないかどうかをチェックすることで、大方の目的を達成することができます。この場合の鍵は client フィールドです。リスト 4 は別の Python プログラムですが、このプログラムも他の言語への移植が容易にできるように設計されています。このプログラムはログ・ファイルを標準入力へとパイプし、ロボットによるトラフィックと判断されたもの以外のすべての行を標準出力へとパイプします。
リスト 4. 検索エンジンによるスパイダー・トラフィックをログ・ファイルから削除する
import re
import sys
#This regular expression is the heart of the code.
#Python uses Perl regex, so it should be readily portable
#The r'' string form is just a convenience so you don't have to escape backslashes
COMBINED_LOGLINE_PAT = re.compile(
r'(?P<origin>\d+\.\d+\.\d+\.\d+) '
+ r'(?P<identd>-|\w*) (?P<auth>-|\w*) '
+ r'\[(?P<date>[^\[\]:]+):(?P<time>\d+:\d+:\d+) (?P<tz>[\-\+]?\d\d\d\d)\] '
+ r'"(?P<method>\w+) (?P<path>[\S]+) (?P<protocol>[^"]+)" (?P<status>\d+)
(?P<bytes>-|\d+)'
+ r'( (?P<referrer>"[^"]*")( (?P<client>"[^"]*")( (?P<cookie>"[^"]*"))?)?)?\s*\Z'
)
#Patterns in the client field for sniffing out bots
BOT_TRACES = [
(re.compile(r".*http://help\.yahoo\.com/help/us/ysearch/slurp.*"),
"Yahoo robot"),
(re.compile(r".*\+http://www\.google\.com/bot\.html.*"),
"Google robot"),
(re.compile(r".*\+http://about\.ask\.com/en/docs/about/webmasters.shtml.*"),
"Ask Jeeves/Teoma robot"),
(re.compile(r".*\+http://search\.msn\.com\/msnbot\.htm.*"),
"MSN robot"),
(re.compile(r".*http://www\.entireweb\.com/about/search_tech/speedy_spider/.*"),
"Speedy Spider"),
(re.compile(r".*\+http://www\.baidu\.com/search/spider_jp\.html.*"),
"Baidu spider"),
(re.compile(r".*\+http://www\.gigablast\.com/spider\.html.*"),
"Gigabot robot"),
]
for line in sys.stdin:
match_info = COMBINED_LOGLINE_PAT.match(line)
if not match_info:
sys.stderr.write("Unable to parse log line\n")
continue
isbot = False
for pat, botname in BOT_TRACES:
if pat.match(match_info.group('client')):
isbot = True
break
if not isbot:
sys.stdout.write(line)
|
スパイダー・クライアントに対する正規表現のリストは、それほど膨大なものではありません。新しい検索エンジンは次々と登場していますが、このリストのパターンを参考に、トラフィックの解析中に気付いた新しいスパイダーを含むようにリストを拡張していくのは容易なはずです。
Web サーバーのログを解析して Web の統計を表示するための一般的なツールはたくさんありますが、この記事でここまでに説明したビルディング・ブロックを使用すれば、ログ情報を独自の特別な方法で表示するのも簡単なことです。さらにもう 1 つのビルディング・ブロックとして、Apache のログ・フォーマットを JSON (JavaScript Object Notation) に変換するブロックを追加することもできます。情報が JSON 形式で得られると、情報の分析、操作、表示を (クライアント・サイドを含めて) JavaScript を使って容易に行うことができます。
その一方で、皆さん自身がツールを作成する必要すらないかもしれません。このセクションでは、Apache のログ・ファイルを、Exhibit で使われている JSON の形式に変換する方法を説明します (Exhibit は MIT の SIMILE プロジェクトによる強力なデータ表示ツールです)。このツールについては、以前の記事、「Practical linked, open data with Exhibit」(「参考文献」を参照) の中で説明しました。必要なことは Exhibit に JSON を提供することだけです。すると Exhibit は、データの表示、フィルタリング、検索のためのリッチで動的なシステムを作成してくれます。リスト 5 (apachelog2exhibit.py) は、これまでの例を基に、Apache のログを Exhibit の JSON 形式に変換します。
リスト 5 (apachelog2exhibit.py). スパイダー以外のトラフィックによるログ・エントリーを Exhibit の JSON 形式に変換する
import re
import sys
import time
import httplib
import datetime
import itertools
# You'll need to install the simplejson module
# http://pypi.python.org/pypi/simplejson
import simplejson
# This regular expression is the heart of the code.
# Python uses Perl regex, so it should be readily portable
# The r'' string form is just a convenience so you don't have to escape backslashes
COMBINED_LOGLINE_PAT = re.compile(
r'(?P<origin>\d+\.\d+\.\d+\.\d+) '
+ r'(?P<identd>-|\w*) (?P<auth>-|\w*) '
+ r'\[(?P<ts>(?P<date>[^\[\]:]+):(?P<time>\d+:\d+:\d+)) (?P<tz>[\-\+]?\d\d\d\d)\] '
+ r'"(?P<method>\w+) (?P<path>[\S]+) (?P<protocol>[^"]+)" (?P<status>\d+)
(?P<bytes>-|\d+)'
+ r'( (?P<referrer>"[^"]*")( (?P<client>"[^"]*")( (?P<cookie>"[^"]*"))?)?)?\s*\Z'
)
# Patterns in the client field for sniffing out bots
BOT_TRACES = [
(re.compile(r".*http://help\.yahoo\.com/help/us/ysearch/slurp.*"),
"Yahoo robot"),
(re.compile(r".*\+http://www\.google\.com/bot\.html.*"),
"Google robot"),
(re.compile(r".*\+http://about\.ask\.com/en/docs/about/webmasters.shtml.*"),
"Ask Jeeves/Teoma robot"),
(re.compile(r".*\+http://search\.msn\.com\/msnbot\.htm.*"),
"MSN robot"),
(re.compile(r".*http://www\.entireweb\.com/about/search_tech/speedy_spider/.*"),
"Speedy Spider"),
(re.compile(r".*\+http://www\.baidu\.com/search/spider_jp\.html.*"),
"Baidu spider"),
(re.compile(r".*\+http://www\.gigablast\.com/spider\.html.*"),
"Gigabot robot"),
]
MAXRECORDS = 1000
# Apache's date/time format is very messy, so dealing with it is messy
# This class provides support for managing timezones in the Apache time field
# Reuses some code from: http://seehuhn.de/blog/52
class timezone(datetime.tzinfo):
def __init__(self, name="+0000"):
self.name = name
seconds = int(name[:-2])*3600+int(name[-2:])*60
self.offset = datetime.timedelta(seconds=seconds)
def utcoffset(self, dt):
return self.offset
def dst(self, dt):
return timedelta(0)
def tzname(self, dt):
return self.name
def parse_apache_date(date_str, tz_str):
'''
Parse the timestamp from the Apache log file, and return a datetime object
'''
tt = time.strptime(date_str, "%d/%b/%Y:%H:%M:%S")
tt = tt[:6] + (0, timezone(tz_str))
return datetime.datetime(*tt)
def bot_check(match_info):
'''
Return True if the matched line looks like a robot
'''
for pat, botname in BOT_TRACES:
if pat.match(match_info.group('client')):
return True
break
return False
entries = []
# enumerate lets you iterate over the lines in the file, maintaining a count variable
# itertools.islice lets you iterate over only a subset of the lines in the file
for count, line in enumerate(itertools.islice(sys.stdin, 0, MAXRECORDS)):
match_info = COMBINED_LOGLINE_PAT.match(line)
if not match_info:
sys.stderr.write("Unable to parse log line\n")
continue
# If you want to include robot clients, comment out the next two lines
if bot_check(match_info):
continue
entry = {}
timestamp = parse_apache_date(match_info.group('ts'), match_info.group('tz'))
timestamp_str = timestamp.isoformat()
# To make Exhibit happy, set id and label fields that give some information
# about the entry, but are unique across all entries (ensured by appending count)
entry['id'] = match_info.group('origin') + ':' + timestamp_str + ':' + str(count)
entry['label'] = entry['id']
entry['origin'] = match_info.group('origin')
entry['timestamp'] = timestamp_str
entry['path'] = match_info.group('path')
entry['method'] = match_info.group('method')
entry['protocol'] = match_info.group('protocol')
entry['status'] = match_info.group('status')
entry['status'] += ' ' + httplib.responses[int(entry['status'])]
if match_info.group('bytes') != '-':
entry['bytes'] = match_info.group('bytes')
if match_info.group('referrer') != '"-"':
entry['referrer'] = match_info.group('referrer')
entry['client'] = match_info.group('client')
entries.append(entry)
print simplejson.dumps({'items': entries}, indent=4)
|
単純に Apache のログ・ファイルを python apachelog2exhibit.py というコマンドへパイプ入力し、出力される JSON をキャプチャーします。リスト 6 は出力された JSON の簡単な例です。
リスト 6. Apache のログから得られた Exhibit の JSON の例
{
"items": [
{
"origin": "208.111.154.16",
"status": "200 OK",
"protocol": "HTTP/1.1",
"timestamp": "2009-04-27T08:21:42-05:00",
"bytes": "2638",
"auth": "-",
"label": "208.111.154.16:2009-04-27T08:21:42-05:00:2",
"identd": "-",
"method": "GET",
"client": "Mozilla/5.0 (compatible; Charlotte/1.1;
http://www.searchme.com/support/)",
"referrer": "-",
"path": "/uche.ogbuji.net",
"id": "208.111.154.16:2009-04-27T08:21:42-05:00:2"
},
{
"origin": "65.103.181.249",
"status": "200 OK",
"protocol": "HTTP/1.1",
"timestamp": "2009-04-27T09:11:54-05:00",
"bytes": "6767",
"auth": "-",
"label": "65.103.181.249:2009-04-27T09:11:54-05:00:4",
"identd": "-",
"method": "GET",
"client": "Mozilla/5.0 (compatible; MJ12bot/v1.2.4;
http://www.majestic12.co.uk/bot.php?+)",
"referrer": "-",
"path": "/",
"id": "65.103.181.249:2009-04-27T09:11:54-05:00:4"
}
]
}
|
Exhibit を使用するためには、Exhibit ライブラリーの JavaScript とデータの JSON 表現をロードする HTML ページを作成します。リスト 7 は、ログ・ファイル情報を表示するための Exhibit の HTML ページの非常に簡単な例を示しています。
リスト 7. Exhibit のログ・ビューアーの HTML
<html>
<head>
<title>Apache log entries</title>
<link href="logview.js" type="application/json" rel="exhibit/data" />
<script src="http://static.simile.mit.edu/exhibit/api-2.0/exhibit-api.js"
type="text/javascript"></script>
<script src="http://static.simile.mit.edu/exhibit/extensions-2.0/time/time-extension.js"
type="text/javascript"></script>
<style>
#main { width: 100%; }
#timeline { width: 100%; vertical-align: top; }
td { vertical-align: top; }
.entry { border: thin solid black; width: 100%; }
#facets { padding: 0.5em; width: 20%; }
.label { display: none; }
</style>
</head>
<body>
<h1>Apache log entries</h1>
<table id="main">
<tr>
<!-- The main display area for Exhibit -->
<td ex:role="viewPanel">
<div id="what-lens" ex:role="view"
ex:viewClass="Exhibit.TileView"
ex:label="What">
</div>
</div>
<!-- Timeline view for the feed data -->
<div id="timeline" ex:role="view"
ex:viewClass="Timeline"
ex:label="When"
ex:start=".timestamp"
ex:colorKey=".status"
ex:topBandUnit="day"
ex:topBandPixelsPerUnit="200"
ex:topBandUnit="week">
</div>
</td>
<!-- Boxes to allow users narrow down their view of feed data -->
<td id="facets">
<div ex:role="facet" ex:facetClass="TextSearch"></div>
<div ex:role="facet" ex:expression=".path" ex:facetLabel="Path"></div>
<div ex:role="facet" ex:expression=".referrer" ex:facetLabel="Referrer"></div>
<div ex:role="facet" ex:expression=".origin" ex:facetLabel="Origin"></div>
<div ex:role="facet" ex:expression=".client" ex:facetLabel="Client"></div>
<div ex:role="facet" ex:expression=".status" ex:facetLabel="Status"></div>
</td>
</tr>
</table>
</body>
</html>
|
図 2 は、この単純な HTML ソースから得られた出力ビューの 1 つで、多くの内容を整然と表示しています。面倒な処理はすべて Exhibit が行ってくれます。ファセット (facet) ボックスを利用すると、表示される項目を絞り込むことができ、例えば 1 つのアドレスからのアクセス・パターンを分析したりすることができます。
図 2. Exhibit のログ・ファイル・ビューアー (「WHAT」ビュー)
図 3 はタイムライン・ビューという別の出力ビューを示しています。このビューを表示するためには、デフォルトのビューの先頭から WHEN をクリックします。
図 3. Exhibit のログ・ファイル・ビューアー (「When」ビュー)
ログ・ファイルの情報には数多くの使い道があります。例えば私の場合、ブラウザー・サイドの XSLT といった新しい Web 技術をいつ適用すればよいかを判断するために、ログ・ファイルの情報を使用していたことがあります。XSLT 対応のブラウザーを使っている (スパイダー以外の) サイト訪問者の割合を調べるために使用していたのです。またブログ・エントリー用の便利なタグを提案するために、検索エンジンから送信された referrer フィールドを調べていたこともあります。ログ・ファイルの一般的な統計処理や分析を行うツールはたくさんありますが、ログのあらゆる使い方に対応した既存のツールというものはありません。そのため、ログを直接処理する方法を学ぶことで、Web アーキテクトにとって貴重なスキルを身に付けることができます。
学ぶために
- 最新バージョンの Apache でサポートされているログ・ファイルのフォーマットについて学んでください。
- この記事で説明した手法を紹介した記事として、David Mertz 著の「Using regular expressions」を読んでください。
- さまざまな形式の正規表現について、その特徴や対応する構文について学んでください。
- この記事のコードを PHP に移植するために必要なことを学ぶ資料として、Martin Streicher 著の連載記事「PHP での正規表現をマスターする」の第 1 回と第 2 回を読んでください。
- Exhibit について学ぶために、Uche Ogbuji 著による「Practical linked, open data with Exhibit」を読んでください。
- developerWorks の Technical events and webcasts で最新情報を入手してください。
- developerWorks の Web development ゾーンに用意された、Web 技術に特化した記事やチュートリアルを利用して、Web 開発のスキルを磨いてください。
- My developerWorks を調べてみてください。そして Web 開発に関する、あるいは皆さんにとって関心のある任意の話題に関する、グループ、ブログ、アクティビティーを見つけ、あるいは作成してください。
製品や技術を入手するために
- オープンソースの Apache HTTP サーバーは最も広く使われています。
- リスト 5 は simplejson ライブラリーを使用しています。

Uche Ogbuji は次世代の Web 技術によるソリューションを専門とする会社 Zepheira, LLC のパートナーです。Ogbuji 氏は XML、RDF、およびナレッジ・マネージメント・アプリケーション用のオープンソース・プラットフォームである 4Suite の中心的開発者であり、またチームによる Web 開発のための Jacqard アジャイル手法、そして Versa RDF 問い合わせ言語などの開発リーダーでもあります。彼はナイジェリア出身のコンピューター・エンジニア兼ライターとして米国コロラド州ボルダーに住み、そこで働いています。彼に関して詳しくは、彼のブログである Copia を見てください。