Blog der Heimetli Software AG

Apache-Log mit Python und Plotly auswerten

Mit der Zeit wird es langweilig die Pipeline für die Logauswertung manuell auszuführen. Ein Aufruf müsste genügen um die Analyse durchzuführen.

pd.read_csv() zum lesen des Logfiles

Den Lösungsansatz habe ich bei R gefunden: read.table. Die Suche bei Pandas führte zu einer ähnlichen Funktion, aber die war schon mal deprecated...

Anscheinend ist sie jetzt nicht mehr deprecated, aber ein ungutes Gefühl erzeugt das schon. In der Beschreibung dieser Funktion gibt es eine Referenz auf read_csv. Auf diese Idee wäre ich nie selber gekommen, aber ein bisschen pröbeln mit den Optionen ergab recht schnell einen Dataframe mit den Logdaten.

Das Löschen aller unerwünschten Einträge ist nicht so einfach wie beim zeilenweisen Lesen, aber auch nicht besonders schwierig.

Kein Bar-Chart mit einer Zeitachse

Weil die Zugriffe für jeden Tag kumuliert werden, erschien mir ein Bar-Chart passend für die Darstellung. Das macht aber erstaunliche Probleme, sowohl bei Matplotlib als auch bei Plotly. Zeitachsen für Bar-Charts sind schlicht nicht vorgesehen!

Dutzende von Versuchen mit Matplotlib brachten nur ziemlich unverständliche Fehlermeldungen. Schlussendlich gelang es mit dem Datum als String der Library eine kategorische Achse vorzutäuschen. Das ist nicht so abstrus wie es auf den ersten Blick aussieht, weil jeder Tag im Log vorkommt. Wenn es Löcher gäbe, müssten die fehlenden Tage mit einem Count von 0 aufgeführt werden...

Weil es so viele Balken gibt, sind sie sehr schmal und nicht so einfach den Angaben auf den Achsen zuzuordnen. Plotly dagegen kann Tooltips mit definierbaren Angaben anzeigen, und das bringt eine viel bessere Orientierung.

Probleme mit Plotly

Ab und zu brachte Plotly einen Chart, aber meistens blieb der Browser hängen bis zum Timeout. Das Script lief bis zum Timeout und wurde dann ohne jegliche Meldung beendet!

Es gibt keinerlei Anhaltspunkte was schief läuft und deshalb auch keine Möglichkeit irgendwas zu debuggen :-(

Stack Overflow kam mir dann zu Hilfe mit der Zeile: fig.write_html( "tmp.html", auto_open=True )

Das schreibt den Plot in ein File, und ruft den Browser damit auf. So funktioniert es seither absolut problemlos.

Bars einfärben

Die nächste Ueberraschung tauchte auf als ich die Wochenenden farbig markieren wollte. Plotly meint dass die gleich eingefärbten Balken zusammengehören, und ordnet alle Samstage und Sonntage rechts im Chart an!

Es blieb nichts anderes übrig als die Position aller Balken fest vorzugeben, mit der Option category_orders.

All diese Erfahrungen haben viel Zeit gekostet, aber das Ziel wurde erreicht:

Logauswertung mit Plotly

Die farbigen Balken zeigen deutlich dass der Wochentag auf diesem Server keinerlei Einfluss auf die Zugriffszahlen hat. Das ist schon erstaunlich.

Eine kleine Unschönheit gibt es übrigens noch: im Tooltip wird das Weekend-Flag aufgeführt. Das habe ich bisher nicht wegbekommen.

Das Python-Sript für die Auswertung

# Import the libraries
import plotly.express as px
import pandas as pd
import datetime

print( "Reading the apache log" )

# Read the logfile
df = pd.read_csv( "access.log", header=None, sep=" ", quotechar="\"", escapechar="\\" )
df.head()

print( "Cleaning the log" )

# Drop unused columns
df.drop( [1,2,4,8], axis="columns", inplace=True )

# Remove all errors
df = df[df[6] < 400]

# Filter our own requests
df = df[df[0]!="81.6.49.243"]

# Filter IPs with suspicious access patterns
suspicious = [
  "82.80.249.137",
  "82.80.249.159",
  "82.80.249.249",
  "146.4.22.190",
  "212.227.250.21",
  "95.217.74.38"
]

df = df[df[0].apply( lambda ip: ip not in suspicious )]

# IP is no longer useful, drop it
df.drop( [0], axis="columns", inplace=True )

# Preparation for the bot filter
df[9] = df[9].str.lower()
df.head()

# Filter the bots
bots = [
  "adsbot",
  "adscanner",
  "ahrefsbot",
  "alphabot",
  "alphaseobot",
  "applebot",
  "aspiegelbot",
  "bingbot",
  "blexbot",
  "borneobot",
  "bot@linkfluence",
  "bot@tracemyfile",
  "brands-bot",
  "ccbot",
  "clarabot",
  "cliqzbot",
  "coccocbot",
  "discordbot",
  "dnsresearchbot",
  "domainstatsbot",
  "dotbot",
  "duckduckbot",
  "exabot",
  "facebot",
  "frobots",
  "gigabot",
  "googlebot",
  "internet-structure-research-project-bot",
  "jobboersebot",
  "kazbtbot",
  "keybot",
  "linguee bot",
  "mauibot",
  "mj12bot",
  "msnbot",
  "nimbostratus-bot",
  "niuebot",
  "obot",
  "our-bot",
  "adbeat_bot",
  "petalbot",
  "pinterestbot",
  "pooplebot",
  "ru_bot",
  "scraperbot",
  "semrushbot",
  "seobilitybot",
  "seokicks",
  "serpstatbot",
  "seznambot",
  "sidetrade indexer bot",
  "smtbot",
  "statvoobot",
  "surdotlybot",
  "tigerbot",
  "tmmbot",
  "triplecheckerrobot",
  "twitterbot",
  "vebidoobot",
  "webtechbot",
  "wiederfreibot",
  "x28-job-bot",
  "yacybot",
  "yandexbot",
  "zoominfobot",
  "spider@seocompany.store",
  "barkrowler",
  "website-datenbank.de",
  "crawler_eb_germany",
  "searchatlas.com",
  "adstxtcrawler",
  "backlinkcrawler",
  "cipacrawler",
  "domaincrawler",
  "grapeshotcrawler",
  "mbcrawler",
  "webcrawler",
  "crawler4j",
  "sslyze",
  "localsearch",
  "winhttprequest",
  "webdatastats" ]

for bot in bots:
    df = df[df[9].str.contains(bot)==False]

# Keep just two columns
df.drop( [5,7,9], axis="columns", inplace=True )

print( "Group by days" )

# Convert the string to a date
df[3] = df[3].apply( lambda d: datetime.datetime.strptime(d[1:12],"%d/%b/%Y") )

# Group and count the requests
grp = df.groupby( [3] ).count().reset_index()

print( "Add computed columns" )

# Rename the columns
grp.columns = ["date","count"]

# Add a column for the weekday
grp["weekday"] = grp["date"].apply( lambda d: d.strftime("%a") )

# Add a column for the weekend
grp["weekend"] = grp["date"].apply( lambda d: "no" if d.weekday() < 5 else "yes" )

# Convert the date back to a string!
grp["date"] = grp["date"].apply( lambda d: d.strftime("%d.%m.%y") )

print( "Generate the plot" )

# Plot the data
fig = px.bar( grp,
              x="date",
              y="count",
              color="weekend",
              color_discrete_map={ "yes":"red", "no":"blue" },
              hover_data=["date","count","weekday"],
              category_orders={ "date": grp["date"].tolist() } )
fig.update_yaxes( title="", visible=True, showticklabels=True )
fig.update_layout( showlegend=False )

# Write plot to file and show it
fig.write_html( "tmp.html", auto_open=True )

Die Print-Statements sind nur dazu da um dem User zu zeigen zu geben was das Script gerade macht. Es läuft eigentlich nicht lange, aber heute wollen die User sofortige Reaktionen.

Bei den Bots sind ein paar dazugekommen, die vermehren sich anscheinend wöchentlich.

Wenn Sie selber mit dem Script herumspielen wollen, dann laden Sie es hier herunter.