[PYTHON] Analysez les journaux d'accès Apache avec Pandas et Matplotlib

Aperçu

Même si vous essayez d'analyser les journaux d'accès tels que Apache et Tomcat, il n'y a pas d'outil étonnamment bon. J'aimerais avoir un outil qui prend en charge divers formats de sortie, ne filtre que les informations nécessaires et le rend facile à comprendre et à visualiser, mais je ne le trouve pas facilement.

Ainsi, dans le monde de l'analyse de données, j'ai essayé d'analyser et de visualiser facilement les journaux d'accès Apache sur Jupyter Notebook en utilisant Pandas et Matplotlib, qui sont standard dans le monde de l'analyse de données. logo.png

Lire le journal d'accès

Importer les bibliothèques requises

Tout d'abord, importez le minimum requis Pandas et Matplotlib.

import pandas as pd
from pandas import DataFrame, Series
import matplotlib.pyplot as plt

Cadre environnemental

Réglez l'environnement selon vos préférences.

#Les graphiques, etc. sont dessinés dans Notebook
%matplotlib inline

#Modification de la longueur de chaîne maximale des colonnes DataFrame de la valeur par défaut de 50 à 150
pd.set_option("display.max_colwidth", 150)

Chargement des journaux d'accès

Pour savoir comment charger le journal d'accès, j'ai fait référence à cette entrée de blog. Avant de charger le journal d'accès, définissez les fonctions d'analyse de type requises.

from datetime import datetime
import pytz

def parse_str(x):
    return x[1:-1]

def parse_datetime(x):
    dt = datetime.strptime(x[1:-7], '%d/%b/%Y:%H:%M:%S')
    dt_tz = int(x[-6:-3])*60+int(x[-3:-1])
    return dt.replace(tzinfo=pytz.FixedOffset(dt_tz))

Ensuite, chargez le journal d'accès avec pd.read_csv (). Le format de sortie du journal d'accès que j'avais était légèrement différent, je l'ai donc modifié comme suit. Cette fois, par souci de simplicité, seules quelques colonnes sont extraites.

df = pd.read_csv(
    '/var/log/httpd/access_log',
    sep=r'\s(?=(?:[^"]*"[^"]*")*[^"]*$)(?![^\[]*\])',
    engine='python',
    na_values='-',
    header=None,
    usecols=[0, 4, 6, 7, 8, 9, 10],
    names=['ip', 'time', 'response_time', 'request', 'status', 'size', 'user_agent'],
    converters={'time': parse_datetime,
                'response_time': int,
                'request': parse_str,
                'status': int,
                'size': int,
                'user_agent': parse_str})

Analyser le journal d'accès

Après avoir chargé le journal d'accès, analysons-le.

Confirmer sans traiter les données

Commencez par vérifier les 5 premières et 5 dernières lignes du fichier.

df

Screenshot from 2020-03-24 16-47-29.png

Vous pouvez voir qu'il y a eu 114 004 demandes en environ 1 heure et 20 minutes à partir de 18 h 20 le 24 mars.

Confirmation du temps de réponse total

Affiche le temps de réponse moyen et maximum.

print('Résultat total du temps de réponse (microsecondes)\n')
print('valeur minimum: {}'.format(str(df['response_time'].min()).rjust(10)))
print('Valeur moyenne: {}'.format(str(round(df['response_time'].mean())).rjust(10)))
print('Médian: {}'.format(str(round(df['response_time'].median())).rjust(10)))
print('Valeur maximum: {}'.format(str(df['response_time'].max()).rjust(10)))

print('\n Pire temps de réponse 15')
df.sort_values('response_time', ascending=False).head(15)

Screenshot from 2020-03-24 14-03-41.png

Les 15 pires temps de réponse étaient toutes les demandes adressées au service d'authentification OpenAM, et il a fallu environ 15 secondes pour renvoyer une réponse avec le statut HTTP 406. Vous pouvez voir qu'il y a eu un problème avec l'application qui fonctionne avec OpenAM.

Confirmation des valeurs manquantes

En regardant les 15 pires temps de réponse, size (taille de la réponse) et ʻuser_agent (agent utilisateur) sont NaN`. Vérifions le nombre de valeurs manquantes.

df.isnull().sum()

Screenshot from 2020-03-24 17-00-58.png

Si «size» est «NaN», c'est une redirection. De plus, environ 30% de ʻuser_agent` sont inconnus. Puisque ce n'est pas seulement le navigateur de l'utilisateur final qui accède à OpenAM, il est probable que beaucoup d'entre eux n'ont pas d'en-tête "User-Agent". Découvrons quels autres agents utilisateurs sont disponibles.

Confirmation de l'agent utilisateur

ua_df = DataFrame(df.groupby(['user_agent']).size().index)
ua_df['count'] = df.groupby(['user_agent']).size().values
ua_df

Screenshot from 2020-03-24 17-15-03.png

Il y avait 490 types. Il semble que ce soit le résultat car non seulement les utilisateurs finaux, mais aussi de nombreuses applications travaillent ensemble.

Visualisez les journaux d'accès

Je ne l'ai pas du tout visualisé jusqu'à présent, donc je pense que ce n'était pas intéressant. Montrons-le sous forme de graphique circulaire. Tout d'abord, dessinons le pourcentage du code d'état de la réponse dans un graphique circulaire.

plt.figure(figsize = (5, 5))
labels = ['2xx', '3xx', '4xx', '5xx']
plt.pie(df.groupby([df['status'] // 100]).size(), autopct = '%1.1f%%', labels = labels, shadow = True, startangle = 90)
plt.axis('equal')
df.groupby([df['status'] // 100]).size()
plt.show()

Screenshot from 2020-03-24 17-29-01.png

Hmmm, les étiquettes de code d'état à faible pourcentage se chevauchent et ne semblent pas bonnes ...

Définir des fonctions utilitaires pour la visualisation

Alors, définissons une fonction qui regroupe un petit nombre d'éléments (1% ou moins) en «autres». Si vous utilisez la fonction de Matplotlib, vous n'avez peut-être pas besoin d'une telle fonction, mais je ne l'ai pas trouvée, alors j'en ai fait une simple.

#Pour DataFrame
def replace_df_minors_with_others(df_before, column_name):
    elm_num = 1
    for index, row in df_before.sort_values([column_name], ascending=False).iterrows():
        if (row[column_name] / df_before[column_name].sum()) > 0.01:
            elm_num = elm_num + 1
    
    df_after = df_before.sort_values([column_name], ascending=False).nlargest(elm_num, columns=column_name)
    df_after.loc[len(df_after)] = ['others', df_before.drop(df_after.index)[column_name].sum()]
    return df_after

#Pour les dictionnaires
def replace_dict_minors_with_others(dict_before):
    dict_after = {}
    others = 0
    total = sum(dict_before.values())
    for key in dict_before.keys():
        if (dict_before.get(key) / total) > 0.01:
            dict_after[key] = dict_before.get(key)
        else:
            others = others + dict_before.get(key)
    dict_after = {k: v for k, v in sorted(dict_after.items(), reverse=True, key=lambda item: item[1])}
    dict_after['others'] = others
    return dict_after

Visualisation de l'agent utilisateur

Maintenant, utilisons cette fonction pour afficher les types d'agent utilisateur dans un graphique circulaire.

plt.figure(figsize = (15, 10))
ua_df_with_others = replace_df_minors_with_others(ua_df, 'count')
plt.pie(ua_df_with_others['count'], labels = ua_df_with_others['user_agent'], autopct = '%1.1f%%', shadow = True, startangle = 90)
plt.title('User Agent')
plt.show()

index.png

Même si vous affichez directement l'en-tête "User-Agent", c'est difficile à comprendre. Qu'est-ce que "Mozilla / 5.0 (Windows NT 6.1; WOW64; Trident / 7.0; rv: 11.0) comme Gecko" ...

Alors, convertissons-le en un affichage facile à comprendre à l'aide d'une bibliothèque appelée Woothee. Installez avec la commande suivante.

$ pip install woothee

Utilisons ceci pour afficher le système d'exploitation client et l'agent utilisateur dans un graphique circulaire.

import woothee

ua_counter = {}
os_counter = {}

for index, row in ua_df.sort_values(['count'], ascending=False).iterrows():
    ua = woothee.parse(row['user_agent'])
    uaKey = ua.get('name') + ' (' + ua.get('version') + ')'
    if not uaKey in ua_counter:
        ua_counter[uaKey] = 0
    ua_counter[uaKey] = ua_counter[uaKey] + 1
    osKey = ua.get('os') + ' (' + ua.get('os_version') + ')'
    if not osKey in os_counter:
        os_counter[osKey] = 0
    os_counter[osKey] = os_counter[osKey] + 1

plt.figure(figsize = (15, 10))
plt.subplot(1,2,1)
plt.title('Client OS')
os_counter_with_others = replace_dict_minors_with_others(os_counter)
plt.pie(os_counter_with_others.values(), labels = os_counter_with_others.keys(), autopct = '%1.1f%%', shadow = True, startangle = 90)

plt.subplot(1,2,2)
plt.title('User Agent')
ua_counter_with_others = replace_dict_minors_with_others(ua_counter)
plt.pie(ua_counter_with_others.values(), labels = ua_counter_with_others.keys(), autopct = '%1.1f%%', shadow = True, startangle = 90)
plt.show()

Screenshot from 2020-03-24 17-46-52.png

«INCONNU» a augmenté, mais est-il devenu plus facile à comprendre? Même ainsi, de nombreuses personnes utilisent encore IE sous Windows ...

Visualisation du code d'état de réponse d'erreur

Examinons ensuite le pourcentage du code d'état (400 ou plus) de la réponse qui a provoqué une erreur.

error_df = df[df['status'] >= 400]
plt.figure(figsize = (10, 10))
labels = ['4xx', '5xx']
plt.pie(error_df.groupby([error_df['status'] // 100]).count().time, labels=labels, counterclock=False, startangle=90)

labels2 = ['400', '401', '403', '404', '406', '500', '501', '502']
plt.pie(error_df.groupby(['status']).count().time, labels=labels2, counterclock=False, startangle=90, radius=0.7)

centre_circle = plt.Circle((0,0),0.4, fc='white')
fig = plt.gcf()
fig.gca().add_artist(centre_circle)
plt.title('Error Status Code')
plt.show()

Screenshot from 2020-03-24 17-55-55.png

La caractéristique de ce journal d'accès est qu'il existe de nombreuses réponses d'erreur avec le code d'état 406, qui n'est généralement pas familier.

Visualisation de l'état de charge

Maintenant, sortons un graphique différent. Tout d'abord, vérifiez l'état de la charge.

plt.figure(figsize = (15, 5))
access = df['request']
access.index = df['time']
access = access.resample('S').count()
access.index.name = 'Time'
access.plot()
plt.title('Total Access')
plt.ylabel('Access')
plt.show()

Screenshot from 2020-03-24 18-04-18.png

Il y a un accès constant, parfois plus de 100 par seconde.

Visualisation de la relation entre la taille de la réponse et le temps

Voyons s'il existe une relation entre la taille de la réponse et l'heure.

plt.figure(figsize = (15, 5))
plt.title('size v.s. response_time')
plt.scatter(df['size']/1000, df['response_time']/1000000, marker='.')
plt.xlabel('Size(KB)')
plt.ylabel('Response Time')
plt.grid()

Screenshot from 2020-03-24 18-09-32.png

Bien qu'il n'y ait pas de relation claire, si la taille de la réponse n'est pas 0, on ne peut pas dire que plus la taille est grande, moins cela prend de temps.

finalement

Je pense que Pandas et Matplotlib peuvent être utilisés pour analyser les journaux d'accès. Si vous pouvez maîtriser ces bibliothèques, cela sera utile pour le dépannage.

Je suis encore au stade du prototype, mais je suis également engagé sur GitHub.

Recommended Posts

Analysez les journaux d'accès Apache avec Pandas et Matplotlib
Lisez CSV et analysez avec Pandas et Seaborn
Implémentez "Data Visualization Design # 3" avec pandas et matplotlib
Fusionner les journaux d'accès Apache
Traçage de données polyvalent avec pandas + matplotlib
Je veux analyser les journaux avec Python
CentOS 6.4, Python 2.7.3, Apache, mod_wsgi, Django
Dessinez une étiquette d'axe hiérarchique avec matplotlib + pandas
Graphiques de fonctions triangulaires avec numpy et matplotlib
Chargez CSV avec des pandas et jouez avec Index
Mémo d'apprentissage Python pour l'apprentissage automatique par Chainer Chapitres 11 et 12 Introduction à Pandas Matplotlib
Les bases de Pandas pour les débutants ③ Créez un histogramme avec matplotlib
Installez pip et pandas avec Ubuntu ou VScode
Visualisez de manière interactive les données avec Treasure Data, Pandas et Jupyter.
Animation avec matplotlib
Japonais avec matplotlib
Animation avec matplotlib
histogramme avec matplotlib
Faire une animation avec matplotlib
Agréger les journaux Git à l'aide de Git Python et analyser les associations à l'aide d'Orange
Lisez et analysez l'ensemble de données au format arff avec python scipy.io
Comment accéder avec cache lors de la lecture_json avec pandas
Comment extraire des valeurs Null et des valeurs non Null avec des pandas
[Linux] [Python] [Pandas] Charger la base de données Microsoft Access (* .mdb) avec Pandas
Étudiez l'échange de données Java et Python avec Apache Arrow
Extraire la valeur maximale avec les pandas et modifier cette valeur
[Linux] Créez un auto-certificat avec Docker et apache
Animez les valeurs alpha et bêta des principales valeurs boursières mondiales avec pandas + matplotlib