Erstellung eigener Statistiken aus Apache-Logfiles

Veröffentlicht: 8. Juni 2015 (Zuletzt geändert am 25. Juli 2019)
0 Kommentare
Enthalten in 2 Kategorien: Programmierung, Website

Aktualisierung vom 19.07.2019: Die hier beschriebene Methode wird von mir derzeit nicht eingesetzt, da sie bei dynamisch generierten Webseiten durch WordPress nicht optimal funktioniert. Außerdem haben sich mittlerweile die Datenschutzregeln durch die DSGVO dahingehend verändert, dass kaum ein Hoster die IP der Besucher länger als 24 Stunden speichert.

Es gibt so einige Web-Analyse Tools im Internet, teilweise sogar richtig gute wie Google Web-Analytics. Aber all diese haben den Nachteil des Datenschutzes und der Performanceproblematik, wenn man die Grafiken gleich in die Webseite einbinden will. Mit Python und Goaccess kann man seine Logfiles auch einfach selbst in Grafiken verwandeln. In diesem Artikel erkläre anhand einiger Ausschnitte aus dem Python-Code, wie ich diesen Prozess automatisiert habe.

Ansatz

Das Ziel des Skript ist es, die verfügbaren Logfiles vom Webspace zu holen, zu verarbeiten und in Grafiken zu verwandeln. Dies muss mindestens wöchentlich passieren, da in meinem Fall mein Anbieter 1und1 alle Logfiles, die älter als 7 Tage sind anonymisiert. Dies tut er, indem er das letzte Byte der IP-Adressen auf ‘x’ ändert, was dazu führt, dass die Logfiles für jedes sinnvolle Auswertetool nutzlos sind. Das Ergebnis des Skripts sind Grafiken, wie sie auf der Statistikseite zu dieser Domain zu sehen sind. Folgende Schritte muss das Skript dazu abarbeiten:

  1. Hole alle Logfiles der letzten 7 Tage vom ftp server und speichere sie lokal ab
  2. Lösche alle lokal gespeicherten Logfiles, die älter als 1 Jahr sind (optional)
  3. Nutze Goaccess um die Logfiles auszuwerten und eine json-Datei zu erstellen
  4. Parse die json-Datei und extrahiere Daten für die Grafiken als Listen
  5. Erzeuge Grafiken für Besucher und Seitenaufrufe pro Monat, Infos zu Betriebssystem, Browser und Lage der Besucher sowie beliebte und referenzierte Seiten
  6. Lege die Grafiken an einen definierten Ort ab
  7. Führe das Skript alle 7 Tage selbständig automatisch aus

Zugriff auf die Logfiles über FTP

Über das Python-Modul ftplib ist der Zugriff auf den FTP Server sehr simpel. Das Modul ist Teil der Standardbibliothek, muss also nicht nachinstalliert werden. Zunächst wird die FTP-Verbindung durch Angabe von URL und Zugangsdaten geöffnet. Mit ftp.cwd("/logs") wird in das Verzeichnis ‘logs’ gewechselt. In diesem Verzeichnis befinden sich die vom Anbieter abgelegten Logfiles. In der nachfolgenden Schleife werden alle Dateiennamen in der Liste filelist in den aktuellen lokalen Ordner kopiert. Da es sich in meinem Fall um gepackte .gz Dateien handelt, muss die lokale Datei im Binärmodus erstellt werden.

import ftplib   # ftp connection
def downloadLogsFromFtpServer(filelist):
    # open ftp connection
    ftp = ftplib.FTP('redpiramide.de', 'USERNAME', 'PASSWORD')
    ftp.cwd("/logs") #changing to log directory

    for filename in filelist:
        localfile = open(filename, 'wb')
		# download in binary mode
        ftp.retrbinary('RETR '+ filename, localfile.write)
        localfile.close()
		
    ftp.quit()

Die Dateiliste enthält die nach dem von 1und1 definierten Namenschema benannten Dateien der letzten 7 Tage zum Zeitpunkt der Ausführung des Skripts. Mit ftp.quit() wird die FTP-Verbindung sauber wieder geschlossen.

Auswertung mit Goaccess

Durch Einbindung der Geo-Lib in Goaccess ist es möglich, die Herkunft der Besucher anhand ihrer IP zu identifizieren.Vergrößern
Durch Einbindung der Geo-Lib in Goaccess ist es möglich, die Herkunft der Besucher anhand ihrer IP zu identifizieren.

Die Auswertung der heruntergeladenen Dateien geschieht mit dem Programm Goaccess. Dabei handelt es sich um ein Open-Source Tool, das eigentlich für die Erstellung von Statistiken in Echtzeit auf dem Webserver genutzt wird. Bei der Installation (auf einem Linux-System) gibt es nichts zu beachten. Folgen Sie einfach den Installationsanweisungen und beachten Sie, dass Sie ggf. die Abhängigkeiten vorher installieren müssen. Kompilieren Sie das Programm selbst, die Version in den üblichen Paketquellen sind teils deutlich veraltet. Nach der Installation müssen Sie ggf. das Format ihres Logfiles dem Programm mitteilen. In meinem Fall musste ich in der Datei /usr/local/etc/goaccess.conf die Option log-format %h %^[%d:%t %^] "%r" %s %b %^ "%R" "%u" mit einem zusätzlichen ignoriertem Feld (%^)) anpassen, da 1und1 an dieser Stelle die eigene Domain einfügt.
In unserem Fall starten wir den Auswerteprozess einmalig für alle gepackten Logfiles (.gz Dateien) im aktuellen Ordner. Zum Start des Programms aus Python wird das, ebenfalls zur Standardbibliothek gehörende, Modul subprocess genutzt. Durch die Option shell=True wird der erste Parameter von call() in der Kommandozeile ausgeführt.

import subprocess   # call other applications as process
goaccess = subprocess.call("zcat -f access.log.*.gz | goaccess --real-os 
           --ignore-crawlers -c -o json > report.json", shell=True)

Die Ergebnisse der Auswertung werden im json-Format in die Datei report.json umgeleitet. Im nächsten Schritt muss diese Datei im Python-Skript ausgewertet werden.

Parsen der erzeugten json-Datei

Auch für diesen Zweck bietet Python eine Standardbibliothek namens json an, die die Weiterverarbeitung sehr vereinfacht. Nachdem die Datei geöffnet wurde, wandelt die Codezeile data = json.load(jsonFile) den Inhalt der Datei in ein Python Dictionary names data um. Der Zugriff auf die Elemente erfolgt dann direkt über den entsprechenden Key. Ein Beispiel. Der nachfolgende Ausschnitt aus der json-Datei gibt die Besucher (visitors) und Seitenaufrufe (hits) pro Tag an. Der Tag ist dabei im Element data codiert. Die Daten 20150607 entsprechen dem 07.06.2015. Die Angaben wiederholen sich für jeden Tag, der in den Logfiles vorkommt. Für ein Jahr also maximal 365 mal.

[...]
"visitors": [
{
	"hits": 7,
	"visitors": 4,
	"percent": 0.92,
	"bytes": 22773,
	"data": "20150607"
},
{
	"hits": 2,
	"visitors": 2,
	"percent": 0.26,
	"bytes": 10458,
	"data": "20150606"
},
[...]

In der Schleife for visitor in data["visitors"]: werden alle diese einzelnen Tage durchlaufen. In die Variable m wird die Monatszahl aus dem data-Element gezogen. Diese dient als Index um alle Seitenaufrufe (visitor["hits"]) eines Monats aufzuaddieren und in der Liste hitsPerMonth zu speichern. Analog geschieht dieser Vorgang mit den Besuchern. Diese Listen werden später für die Grafiken benötigt.

import json     # encode and decode json files
months = ["Jan.", "Feb.", u"März", "April", "Mai", "Juni", "Juli", "August",
          "Sept.", "Okt.", "Nov.", "Dez."]
hitsPerMonth = [0] * len(months)
visitorsPerMonth = [0] * len(months)

with open('report.json') as jsonFile:     
    data = json.load(jsonFile)  # loads the whole file to python dictonaries
    for visitor in data["visitors"]:
	    m = int(visitor["data"][4:6])-1  # month
        hitsPerMonth[m] += visitor["hits"]
        visitorsPerMonth[m] += visitor["visitors"]

Ein anderes Beispiel ist das Extrahieren der Herkunft der Besucher. Hier addiert Goaccess die Besucher und Seitenaufrufe über alle eingelesenen Logfiles automatisch. Wir müssen an dieser Stelle nur alle items im Element geolocation durchgehen und die Daten in eine eigene Liste (listOfTopLocations) von Dictionaries (topLocations) kopieren. Anschließend erfolgt die Sortierung dieser Dictonaries absteigend nach Seitenaufrufen. Um eine Liste von Dictonaries einfach nach einem speziellen Key zu sortieren, wird das Modul itemgetter genutzt. Die enstehende Liste wird später für die Erstellung eines Balkendiagramms genutzt.

from operator import itemgetter # to sort a dict by specific key
for location in data["geolocation"]:
    for country in location["items"]:
        topLocations =  {"name" : "" , "hits" : 0, "visitors" : 0 }
		topLocations["name"] = country["data"]
		topLocations["hits"] = country["hits"]
		topLocations["visitors"] = country["visitors"]
		listOfTopLocations.append(topLocations)
    listOfTopLocations = sorted(listOfTopLocations, key=itemgetter("hits"))

Erzeugen von Grafiken

Balkendiagramm über Besucher und Seitenaufrufe pro Monat über das letzte Jahr. Erstellt mit Matplotlib aus den extrahierten Daten von Goaccess.Vergrößern
Balkendiagramm über Besucher und Seitenaufrufe pro Monat über das letzte Jahr. Erstellt mit Matplotlib aus den extrahierten Daten von Goaccess.

Für die Umwandlung der extrahierten Listen in einen Graphen wird die sehr beliebte, vielseitige und mächtige matplotlib eingesetzt. Dieses Modul gehört nicht zur Standardbibliothek, kann aber über den Distributions-Paketmanager (z.B. apt-get) oder über den Python eigenen Paketmanager (pip oder easy-install) nachinstalliert werden. Matplotlib benötigt zwingend das Modul numpy. Im Normalfall installiert der Paketmanager alle Abhängigkeiten mit.

Der folgende Codeausschnitt zeigt meine Implementierung zur Erzeugung eines Balkendiagramms für Besucher und Seitenaufrufe pro Monat über das letzte Jahr. Dabei werden die beiden Balken pro Monat nebeneinander dargestellt. Zusätzlich wird die absolute Anzahl pro Monat in den beiden letzten Schleifen direkt über die Balken geschrieben. Für die automatische Positionierung des Gitternetzes wird die Klasse AutoMinorLocator verwendet. Das fertige Plot wird über plt.savefig(filename, dpi=100) in eine Date mit dem Namen filename abgespeichert. Beachten Sie, dass für die Speicherung von .jpg oder .png files die entsprechenden Bibliotheken im System vorhanden sein müssen. Zusätzlich ist es ggf. notwendig den Renderer auf Agg zu setzen, damit die Speicherung auch ohne angeschlossenes Display (z.B. beim Zugriff über SSH) fehlerfrei durchläuft. Dazu können Sie entweder import matplotlib und in der nächsten Zeile matplotlib.use('Agg') vor dem Import von matplotlib.pyplot schreiben oder den Standard-Renderer in der Datei .matplotlibrc einstellen.

import numpy as np # used by matplotlib
import matplotlib.pyplot as plt # for creating plots/charts etc.
from matplotlib.ticker import AutoMinorLocator
def createMonthyDualPlot(y1, y2, title, filename):
    n_groups = len(y1)
    bar_width = 0.4
    fig, ax = plt.subplots()
    fig.set_size_inches(20,10)
    plt.rcParams.update({'font.size': 22})
    index = np.arange(n_groups)
    rects1 = plt.bar(index, y1, bar_width,
                     alpha=0.6,
                     color='r',
                     label='Aufrufe')
    rects2 = plt.bar(index + bar_width, y2, bar_width,
                     alpha=0.6,
                     color='b',
                     label='Besucher')

    plt.xlabel(u'Monat')
    plt.ylabel(u'Anzahl')
    plt.title(title)
    plt.xticks(index+bar_width, months)
    plt.rc('legend',**{'fontsize':16 , 'loc':'lower right'})
    plt.legend()        
    ax.yaxis.set_minor_locator(AutoMinorLocator())
    ax.yaxis.grid(True, which='both')
    
    for rect in rects1:
        height = rect.get_height()
        if height > 0:
            ax.text(rect.get_x()+rect.get_width()/2., height,
			'%d'%int(height), ha='center', va='bottom')
                
    for rect in rects2:
        height = rect.get_height()
        if height > 0:
            ax.text(rect.get_x()+rect.get_width()/2., height,
			'%d'%int(height), ha='center', va='bottom')

    plt.tight_layout()
    plt.savefig(filename, dpi=100)

Wöchentliche Ausführung

Das Skript muss mindestens alle 7 Tage ausgeführt werden, damit es lückenlos die Daten in den Logfiles nutzen kann. Damit man dies nicht manuell tun muss und so so ggf. vergisst, empfiehlt es sich hier einen sogenannten Cronjob anzulegen. Am Einfachsten ist dies durch Verlinken oder Aufrufen des Skriptes im Ordner /etc/cron.weekly und Nutzung von Anacron. Achten Sie darauf, dass Dateinamen in diesem Ordner keine Sonderzeichen (auch kein Leerzeichen) sowie keine Dateiendung enthalten dürfen. Ich nutze meinen Raspberry-Pi für die regelmäßige Abarbeitung des Skripts. Dazu habe ich in o.g. Ordner ein kleines Shell-Skript abgelegt, dass in den Nutzerordner wechselt und dort das Programm python mit dem Dateinamen des Skripts aufruft. Das funktioniert super.

Damit bin ich am Ende dieses Artikels. Wie das Endergebnis aussehen kann, sehen Sie auf meiner Statistikseite. Ich hoffe Sie hatten Spaß beim Lesen und Erfolg beim Umsetzen des Geschriebenen. Falls Sie Hilfe benötigen, schreiben Sie mich doch einfach an. Vielen Dank.

zurück

Teile deinen Kommentar oder Feedback zu diesem Beitrag.

Die Angabe der E-Mail-Adresse ist optional und wird nicht veröffentlicht. Sie wird zur Einbindung deines Gravatars und ggf. zur Kontaktaufnahme verwendet. Erforderliche Felder sind mit * markiert.

*

*

*

Durch Betätigen von "Kommentar abschicken", stimmen Sie der Veröffentlichung der eingegebenen Daten zu. Weitere Informationen finden Sie in der Datenschutzerklärung.
Sie können Quellcode über <pre><code>Quellcode</code></pre> in Kommentare einfügen.