Access_Log in der Datenbank

Normalerweise ist der Webserver so konfiguriert, dass jeder Zugriff auf eine Ressource (HTML-Datei, CGI-Script, Images...) in einer Datei aufgezeichnet, also geloggt wird. Die Konfiguration des Webservers hinsichtlich Logging und Management der Logdateien obliegt jedoch dem Provider. Mit einem einfach einzubauenden Log-Mechanismus hingegen, kann der Webmaster selbst bestimmen, was geloggt wird. Ein solches Log, in eine Datenbank geschrieben, ermöglicht statistische Erhebungen und mehr. Der vorliegende Artikel beschreibt, wie das mit Ajax, Perl und einer MySQL-Datenbank gemacht werden kann.

Die Log-Tabelle

Zum Loggen von URL, Referrer und Zeitstempel der Zugriffe wird als Erstes die Tabelle angelegt:

CREATE TABLE log (
  url varchar(100) NOT NULL default '',
  ref varchar(500) default '',
  ts int(4) NOT NULL default '0',
  PRIMARY KEY  (url,ts)
) TYPE=MyISAM; 

Sofern es möglich ist, wird für die Tabelle ein transaktionssicherer Typ (InnoDB) gewählt, obenstehender Typ MyISAM unterstützt das Transaktionskonzept zwar nicht, funktioniert jedoch auch. Durch die Wahl eines zusammengesetzten Primärschlüssels url, ts kann sekundengenau geloggt werden, wobei mehr als ein gleichzeitiger Zugriff innerhalb einer Sekunde wohl kaum vorkommen dürften auf einer privaten WebSite.

Infolge der Wahl der Felder url, ts für den Primärschlüssel werden Abfragen auf die Tabelle in der Perfomance auch verbessert, weil auf diese Felder die WHERE-Klausel angewandt wird, wie weiter unten noch zu sehen ist.

Während für den URL wie dieser hier (siehe Adresseintrag im Browser) 100 Zeichen ausreichend sind, sollte für den Referrer (Spalte ref) etwas mehr Platz reserviert sein, weil ein Solcher recht lang werden kann. Der Referrer beinhaltet den URL von wo aus der Besucher per Klick hergefunden hat, das kann ein Suchmaschineneintrag sein oder eine sonstige Verlinkung. Ab MySQL Version 5.0.3 sind für den Feldtyp varchar 65535 Zeichen möglich.

Loggen mit Ajax

Der asynchrone XMLhttpRequest hat mehrere Charakteristika:

Und so einfach wird das gemacht, zunächst werden die beiden Funktionen für den Request und die Response deklariert:

<script type="text/javascript">
	var req;
	function log_request(url){
		req = (window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
		req.onreadystatechange = log_response;
		req.open("GET", url, true);
		req.send(null);
	}

	function log_response(){
		if (req.readyState == 4) {
			if (req.status == 200) {
				document.getElementById('statistik').innerHTML = req.responseText;
			}
			else{
				document.getElementById('statistik').innerHTML = "Problem mit Empfang der XML-Daten";
			}
		}
	}

</script>

Nun braucht es nur noch eine geeignete Stelle im HTML-Dokument, wo das Ergebnis, die Ajax-Response log_response() angezeigt wird und der Aufruf des serverseitigen "Loggers" (/cgi-bin/log.cgi) erfolgt:

<p id="statistik">&nbsp;</p>

<script type="text/javascript">
	log_request('/cgi-bin/log.cgi?url=' + escape(document.URL) + '&ref=' + escape(document.referrer));
</script>

Ein paar Worte zur Zeichenkodierung in URI's seien hier angebracht. Referrer, die von einer Suchmaschine kommen, sehen beispielsweise so aus:

http://www.example.com/search?hl=de&q=%C3%BC&meta=

Hier sind drei (Parameter=Werte)-Paare im Spiel, welche durch das Kaufmanns-und & getrennt sind. Der Wert für den Parameter q lautet %C3%BC, das ist das Zeichen 'ü' (hier als Beispiel für einen Suchbegriff) kodiert in UTF-8 und hexadezimal mit Prozentzeichen getrennt aufgearbeitet für die zu übertragende URI.

Wichtig: Alle zu erwartenden Referrer sind URI-encodet und in der Mehrzahl der Referrer sind die Zeichen UTF-8 kodiert.

Das Zeichen & dient wie bereits dargelegt, als Trennzeichen für die Parameter in einer URI. Die Funktion escape() im Ajax-Request wandelt dieses Zeichen in den Hexadezimal-Wert %26.

Die Funktion escape() wandelt außerdem auch alle anderen Sonderzeichen für einen URI RFC-2396-gerecht um:

http%3A//www.example.com/search%3Fhl%3Dde%26q%3D%C3%BC%26meta%3D

In dieser Kodierung werden also die Werte der Parameter url und ref in den Ajax-Request gegeben.

Während die HTML-Seite von einem Besucher in den Browser geladen wurde, erfolgt der Request an das serverseitige log.cgi mit o.g. Parametern. Das Perl-Script log.cgi beschreibt nun serverseitig und losgekoppelt von der aufgerufenen HTML-Seite, die Tabelle in der MySQL-Datenbank und gibt das Ergebnis für die entsprechende HTMl-Seite zurück, was im Abschnitt mit der id="statistik" für den Besucher sichtbar angezeigt wird.

Das serverseitige Script

Nach einer Prüfung, ob die im Parameter übergebene URL zulässig ist, wird ein

print "Content-type: text/plain\n\n";

ausgegeben und die Funktion logger($url, $ref) aufgerufen. Die Variablen $url, $ref werden aus der Parameterliste mit dem CGI-Modul geparst:

use strict;
use CGI 'param';

my $url = param('url');
my $ref = param('ref');

# Dabei werden die Sonderzeichen aus einem URI wieder in lesbare Zeichen umgesetzt.
# aus einem URI, hier der referrer, siehe weiter oben
# http%3A//www.example.com/search%3Fhl%3Dde%26q%3D%C3%BC%26meta%3D
# wird wieder Folgendes:
# http://www.example.com/search?hl=de&q=%C3%BC&meta=
# und in dieser Kodierung in die Log-Tabelle eingetragen.

print logger($url, $ref); # Ausgabe der Ajax-Response

###########################################################################
sub logger{
	my $url = shift;
	my $ref = shift;
	my $time = time;
	
	# cleanup, $xd beinhaltet die Vorgabe, nach wieviel Tagen gelöscht wird
	$xd *= 86400;
	$xd = $time - $xd;
	$dbh->do("DELETE FROM log WHERE ts < $xd");
	# $dbh: DataBaseHandle, Erstellung siehe DBI-Modul

	# die neue Tabelle 'log' beschreiben
	$dbh->do("INSERT INTO log VALUES('$url','$ref','$time')");

	# Statistik	
	my ($cnt, $min) = $dbh->selectrow_array("SELECT count(url), min(ts) FROM log WHERE url='$url' GROUP BY url");
	my $date = strftime("%d.%m.%Y", localtime($min)); 
	# Zum Formatieren 'Datum' aus dem Zeitstempel wurde das POSIX-Modul eingebunden:
	# use POSIX qw(strftime);
	
	return "Statistik: $cnt Aufrufe seit $date";
	# Sieht der Besucher im Abschnitt mit der id='statistik'
}
###########################################################################

Einfacher gehts nicht: Drei Statements sind ausreichend zum Bereinigen der Log-Tabelle, zum Schreiben des Log sowie zum Ermitteln der 'Seitenaufrufe seit Eintritt' für die gerade eben angeforderte Webseite.

Auswertung der Log-Tabelle

Wie bereits dargelegt, sind in den meisten zu erwartenden Referrer's die Zeichen UTF-8 kodiert. Daher empfiehlt sich für das Auswerte-Script als Perl-CGI ebenfalls die UTF-8-Kodierung, die einfach dem Header, der ohnehin gesendet werden muss, mitgegeben wird:

print "Content-type: text/html; charset=UTF-8\n\n"; # header

In der Eingangsseite zum Frontend sind alle zur Web-Präsenz gehörigen und im Log erfassten URL's aufgelistet. Es sollte die Möglichkeit geben, entweder nach Anzahl der Aufrufe oder nach dem Zeitpunkt des letzten Aufrufs sortieren zu können. Die dazu zielführenden SQL-Statements sind untenstehend neben dem jeweiligen Screenshot notiert.

Nach Anzahl der Aufrufe

 

Sortierung nach Anzahl der Aufrufe:

SELECT url, count(url) as cnt
FROM log GROUP BY url ORDER BY cnt DESC

Nach Zeit des letzten Zugriffs

 

Sortierung nach Zeitpunkt des letzten Zugriffs:

SELECT url, max(ts) as m 
FROM log GROUP BY url ORDER BY m DESC

Von einer dieser Eingangsseiten ausgehend, führt der Klick auf einen URL zur Liste mit den Details (links Datum und Uhrzeit vom Aufruf, rechts der Referrer, falls vorhanden):

Details zu einer HTML-Seite

Eine solche Liste wird mit untenstehender Funktion ausgegeben (Übergabe $url in die Subfunktion):

###########################################################################
# Tabelle mit Details für eine URL (klick aus browse())
sub xbrowse{
	my $url = shift;
		
	# SQL-Statement prepare and execute
	my $q = qq(SELECT ref, ts FROM log WHERE url='$url' ORDER BY ts DESC);
	my $sth = $dbh->prepare($q);
	$sth->execute;

	print qq(
		<ul>
	);
	while( my $ref = $sth->fetchrow_hashref ){
		my $referrer = uri_unescape $ref->{ref}; # use URI::Escape;
		my $datetime = strftime("%d.%m.%Y %X", localtime($ref->{ts})); # use POSIX qw(strftime);
		print qq(
			<li>$datetime <a href="$ref->{ref}" title="$referrer">$referrer</a></li>
		);
	}
	print "</ul>";
	$sth->finish;
	return;
}
###########################################################################

Beachte: Die Funktion uri_unescape() (Perl-Modul URI::Escape) macht hexkodierte Zeichen wieder für den Browser lesbar, z.B. wird aus dem viel zitierten %C3%BC wieder das Zeichen 'ü'. Für das href-Attribut im a-Tag hingegen wird diese Umwandlung nicht vorgenommen. Ein Klick auf einen Referrer in obenstehende Liste öffnet so beispielsweise die Seite einer Suchmaschine in der richtigen Zeichenkodierung mit allen Suchbegriffen, die eingegeben wurden.

Mein Logger-Script in PHP

Für meine bsiherigen Perl-Anwendungen habe ich eine INI-Datei mit Basis-Variablen, u.a. sind in dieser Datei die Credentials für den MySQL-Zugriff notiert wie folgt:

# basic.ini
[mysql]
user= "Benutzername"
pass= "Passwort im Klartext"
host= "Hostname des MySQL-Servers"
port=3306
base= "Name der Datenbank"

Daneben gibt es noch eine INI-Datei (public.ini), die bei jedem Update des Web's automatisch aktualisiert wird, hierin stehen alle URLs, die eine HTML-Datei repräsentieren als [/url.html]. Beide INI's werden im PHP-Script genutzt, einmal für die Anbindung zur DB und beim Loggen selbst: hier wird geprüft, ob der requestete URL gültig ist. Das Logger-Script (logger.php) selbst wird als AJAX-Request wie obenstehend in jede HTML-Datei eingebunden.

Für zukünftige und weitere PHP-Scripts habe ich eine kleine Lib erstellt, basic.php:

<?php
/* General Settings */

date_default_timezone_set('Europe/Paris');

/* Variablen */

$cfg = parse_ini_file('/home/webuser/html/cgi-bin/basic.ini', true);
$pub = parse_ini_file($cfg['path']['dbdir']."/public.ini", true);

/* Funktionen */

// Verbindung zu MySQL herstellen
function dbase(){
	global $cfg;
	
	$db = mysqli_init();
	$db->options(MYSQLI_OPT_CONNECT_TIMEOUT, 3);
		
	$db->real_connect($cfg['mysql']['host'], $cfg['mysql']['user'], $cfg['mysql']['pass'], $cfg['mysql']['base']);
	
	if (mysqli_connect_errno()) return;
	else return($db);
}

// haben wir CGI-Parameter
function cgiParam(){
	return(isset($_SERVER['CONTENT_LENGTH']) || strlen($_SERVER['QUERY_STRING']));
}
?>

Diese kleine Library wird in logger.php eingebunden mit include, siehe untenstehendes Listing:

<?php

include("basic.php");

$db = dbase();

// Das Script wird von jeder Seite aus aufgerufen
// Es gibt die Variablen count => url; ref => referrer als GET-Parameter

if(cgiParam()){
	if(isset($_GET['count'])){
		$url = parse_url($_GET['count']);
		$u = $url['path'];
		if($u == "/") $u = "/index.html";
		if( isset($pub[$u]) ){
			// Alles OK, $url['path'] ist im Index, wir loggen
			$r = (isset($_GET['ref'])) ? (strlen($_GET['ref'])) ? $_GET['ref'] : 'noref' : '';
			$t = time();

			// Den LogEintrag machen
			$sql = "INSERT INTO log VALUES('$u', '$r', $t, 'otto')";
			$res = $db->query($sql);

			// Cleanup nach 30 Tagen
			$xd = $t - (30 * 86400);
			$sql = "DELETE FROM log WHERE ts < $xd";
			$db->query($sql);
			
			// Ausgabe der Statistik
			$sql = "SELECT count(url) as stat, min(ts) as ts FROM log WHERE url='$u' GROUP BY url";
			$res = $db->query($sql);
			$row = $res->fetch_assoc(); // nur eine Zeile erwartet
			$dat = date("m.d.y", $row['ts']);;
			echo "Statistik: ".$row['stat']." Aufrufe seit ".$dat;
		}
		else echo "Nicht im Index";
	}
	else{
		echo "Falscher Parameter";
	}
}
else{
	echo "Keine Parameter";
}

?>

Das Script läuft fehlerfrei und ist auf meiner WebSite im Einsatz, siehe Quelltext.

Last-Modified: Tue, 22 Jun 2010 19:50:31 GMT