17h00 - TP « Débroussailleur de maillog »

Navigation rapide : Lundi / Mardi / Mercredi / Jeudi / Vendredi Mémos : Perl / Python / Ruby

Objectif

Le but du TP est d'y voir un peu plus clair dans les logs d'un serveur de mails, en regroupant pour chaque mail les informations le concernant.

Les logs ressemblent à ça :

Jul 25 04:02:09 tonton postfix/local[5432]: 52BD11744C: to=<mailmaster@tonton.univ-angers.fr>, orig_to=<root>, relay=local, delay=6.8, delays=3.5/0/0/3.3, dsn=2.0.0, status=sent (delivered to command: /usr/bin/procmail)
Jul 25 04:02:09 tonton postfix/qmgr[2125]: 52BD11744C: removed
Jul 25 04:03:05 tonton postfix/smtpd[5426]: disconnect from netsrv.math[172.19.45.20]
Jul 25 04:03:05 tonton postfix/qmgr[2125]: 479521744B: from=<root@netsrv.math>, size=4418700, nrcpt=1 (queue active)
Jul 25 04:03:05 tonton spamc[6085]: skipped message, greater than max message size (512000 bytes)
Jul 25 04:03:05 tonton postfix/local[5427]: 479521744B: to=<mailmaster@smtp.math>, relay=local, delay=60, delays=59/0/0/0.36, dsn=2.0.0, status=sent (delivered to command: /usr/bin/procmail)

Dans ce type de log, chaque opération appliquée à un mail est consignée sur une ligne propre, comportant en particulier le message-ID du mail, sous forme d'une chaîne de 10 caractères hexadécimaux (ici 52BD11744C et 479521744B).

Voici un véritable fichier maillog

Sujet

Nous allons écrire un script qui parcours un tel fichier de log, et construit une structure de données contenant pour chaque mail (identifié par son Message-ID) la liste des lignes (débarrassées éventuellement des informations superflues) le concernant.

Pour un mail donné, on fera attention à conserver l'ordre chronologique des lignes le concernant. Ci-dessous un exemple de la structure de données que l'on pourrait construire :

Exemple

'E4A9F1744B' => [
  ' client=math.univ-angers.fr[193.49.146.25]',
  ' message-id=<4C51974F.9090401@certa.ssi.gouv.fr>',
  ' from=<corresp_ssi-owner@services.cnrs.fr>, size=13207, nrcpt=1',
  ' to=<jaclin@math.univ-angers.fr>, status=sent',
  ' removed'
  ],
'916561744B' => [
  ' client=math.univ-angers.fr[193.49.146.25]',
  ' message-id=<OP-M80-S55-1280040877@rem02.com>',
  ' from=<bounce@rem02.com>, size=6585, nrcpt=1',
  ' to=<jea@tonton.univ-angers.fr>, status=sent',
  ' removed'
]

Etape 1

Question

Ecrire un filtre qui stocke dans une liste chaque ligne lues sur stdin. Le programme envoie ensuite le contenu de cette liste sur stdout. On ne s'intéressera qu'aux lignes contenant un message-ID, les autres seront simplement écartées.

Indication Perl: fonction push, module Data::Dumper

Réponse

bash

 1: #!/bin/bash
 2:
 3: # Pas de liste en bash => recours au tableau + index
 4: typeset -a LST
 5: typeset -i LSTSIZE=0
 6:
 7: # Fichier intermédiaire pour limiter le nombre d'appel
 8: # à grep au sein d'une boucle
 9: # $$ est le PID du script => unicité du fichier tampon
10: BUFFILE=/tmp/maillog-tmp.$$
11:
12: # Ne garder que les lignes avec un message_ID
13: # grep -E => expressions régulières étendues
14: grep -E ' [0-9A-F]{10}:' < /dev/stdin > $BUFFILE
15:
16: # Remplissage du tableau LST
17: while read LINE
18: do
19:   LST[$((LSTSIZE++))]="$LINE"
20: done < $BUFFILE
21:
22: # Affichage des lignes
23: for (( i=0 ; i<$LSTSIZE ; i++))
24: do
25:   echo "${LST[$i]}"
26: done
27:
28: rm $BUFFILE

perl

 1: use strict ;                                # rend obligatoire les déclarations
 2:
 3: my @list ;                                  # une liste
 4: while(<>)
 5: {
 6:   next unless /\s[0-9A-F]{10}:/ ;           # s'il n'y a pas de message_ID, au suivant !
 7:   push(@list,$_) ;
 8: }
 9:
10: for (@list) { print ; }                     # équivalent à : for my $line (@list) { print $line ; }

python

 1: import fileinput
 2: import re
 3:
 4: list = []
 5: p = re.compile('\s[0-9A-F]{10}:')
 6: for line in fileinput.input():
 7:     if p.search(line):
 8:         list.append(line)
 9: for item in list:
10:     print item,

ruby

1: list = []                                   # un tableau ie une liste
2: $stdin.each_line do |line|                  # pour toutes les lignes de $stdin
3:   list.push line unless line.match(/\s[0-9A-F]{10}:/).nil?    # on garde la ligne sauf si il n'y a pas de message_ID
4: end
5: list.each do |item| puts item end

Etape 2

Question

Avant de stocker une ligne dans la liste, supprimer les informations inintéressantes, comme par exemple le nom du host ou le PID du process.

Réponse

bash

 1: #!/bin/bash
 2:
 3: # Pas de liste en bash => recours au tableau + index
 4: typeset -a LST
 5: typeset -i LSTSIZE=0
 6:
 7: # Fichier intermédiaire pour limiter le nombre d'appel
 8: # à grep au sein d'une boucle
 9: # $$ est le PID du script => unicité du fichier tampon
10: BUFFILE=/tmp/maillog-tmp.$$
11:
12: # Ne garder que les lignes avec un message_ID
13: # grep -E => expressions régulières étendues
14: # sed est utilisé pour retirer le PID et le nom du host
15: grep -E ' [0-9A-F]{10}:' < /dev/stdin |  sed 's/^\(\w\+ [0-9]\+ [0-9]\+:[0-9]\+:[0-9]\+\) \w\+ \(.*\)\[[0-9]*\]/\1 \2/g' > $BUFFILE
16:
17: # Remplissage du tableau LST
18: while read LINE
19: do
20:   LST[$((LSTSIZE++))]="$LINE"
21: done < $BUFFILE
22:
23: # Affichage des lignes
24: for (( i=0 ; i<$LSTSIZE ; i++))
25: do
26:   echo "${LST[$i]}"
27: done
28:
29: rm $BUFFILE

perl

 1: # On utilise ici le module Data::Dumper pour le fun
 2:
 3: use Data::Dumper ;                          # pour dump de variables
 4: use strict ;
 5:
 6: my @list ;
 7: while(<>)
 8: {
 9:   next unless /\s[0-9A-F]{10}:/ ;
10:   s/^(\w+ \d+ \d+:\d+:\d+) \w+ (.*?)\[\d+\]/$1 $2/ ;  # suppression du host et du PID
11:   push(@list,$_) ;
12: }
13: print Dumper \@list ;                       # issu de data::Dumper

Explication de la ligne #10 : ce qui s'identifie avec le contenu des () situées dans la première partie du substitute se récupére dans les variables $1, $2, $3, etc.

python

 1: import fileinput
 2: import re
 3:
 4: list = []
 5: p = re.compile('\s[0-9A-F]{10}:')
 6: for line in fileinput.input():
 7:     if p.search(line):
 8:         list.append(re.sub('^(\w+ \d+ \d+:\d+:\d+) \w+ (.*?)\[\d+\]',r'\1 \2',line))
 9: for item in list:
10:     print item,

ruby

 1: require 'pp'                                # PrettyPrinting
 2:
 3: list = []
 4: $stdin.each_line do |line|
 5:   begin                                     # suppression du host et du PID
 6:     list.push "#{$1} #{$2}" unless line.match(/^(\w+ \d+ +\d+:\d+:\d+) \w+ (.*?)\[\d+\]/).nil?
 7:   end unless line.match(/\s[0-9A-F]{10}:/).nil?
 8: end
 9: pp list

Etape 3

Question

Utiliser maintenant un hash pour conserver les lignes intéressantes. La clé du hash sera le message-ID. Pour une clé donnée, la valeur du hash sera la concaténation des lignes relatives à ce message-ID.

Enlever le message-ID des lignes stockées. Faire un dump du hash en fin de programme.

Indication Perl : opérateur de concaténation .
Indication Ruby : utiliser la bibliothèque Oniguruma, cf /usr/lib/ruby/gems/1.8/gems/oniguruma-1.1.0/README.txt

Réponse

bash

 1: #!/bin/bash
 2:
 3: # # Les tableaux associatifs ne peuvent pas être utilisés pour simuler
 4: # # une table de hachage. En effet on arrive vite à un probleme de
 5: # # capacité (au niveau de l'index) avec l'exemple ci-dessous :
 6:
 7: # typeset -a HSH
 8: # HSH['B31E017463']="Valeur n°1"
 9: # HSH['B31E017463']="Valeur n°2"
10: # HSH['B73DF1745B']="Valeur n°3"
11: # HSH['B73DF1745B']="Valeur n°4"
12: # HSH['4FE0D17466']="Valeur n°5"
13:
14: # # L'erreur est sans appel :
15: # #./mail3.sh: line 12: 4FE0D17466: value too great for base (error token is "4FE0D17466")
16: # #
17:
18: # Fichier intermédiaire pour limiter le nombre d'appel
19: # à grep au sein d'une boucle
20: # $$ est le PID du script => unicité du fichier tampon
21: BUFFILE=/tmp/maillog-tmp.$$
22:
23: # Construction du fichier intermédiaire :
24: # - grep sélectionne les lignes avec un message_id
25: # - sed replace le message_id en debut de ligne, retire le PID et
26: #   le nom du host
27: # - comme toutes les lignes commencent par un message_id, sort
28: #   permet de les rassembler
29: grep -E ' [0-9A-F]{10}:' /dev/stdin |  sed 's/^\(\w\+ \+[0-9]\+ [0-9]\+:[0-9]\+:[0-9]\+\) \w\+ \(.*\)\[[0-9]*\]: \([0-9A-F]\{10\}\):\(.*\)/\3\1 \2\4/g' | sort > $BUFFILE
30:
31: # Affichage de toutes les lignes relatives à un message_id
32: MSID=""
33: while read LINE
34: do
35:   K=${LINE:0:10} # 10 premiers caractères => message ID
36:   if [ ! "$K" = "$MSID" ]; then
37:       # Première fois que l'on rencontre ce message_id => nouveau bloc
38:       MSID=$K
39:       echo "Message with ID $MSID:"
40:   fi
41:   echo " ${LINE:10}" # Le reste de la ligne avec le message de log
42: done < $BUFFILE
43:
44: rm $BUFFILE

perl

 1: use Data::Dumper ;
 2: use strict ;
 3:
 4: my %h ;                                    # un hash
 5: while(<>)
 6: {
 7:   next unless /\s[0-9A-F]{10}:/ ;
 8:   s/^(\w+ \d+ +\d+:\d+:\d+) \w+ (.*?)\[\d+\]/$1 $2/ ;
 9:   my($begin,$id,$end) = /^(.*) ([0-9A-F]{10})(: .*)/ ;   # on coupe en 3
10:   $h{$id} .= "\n".$begin.$end ;
11: }
12: print Dumper \%h ;

Explication de la ligne #9 : ce qui s'identifie avec le contenu des () situées dans la RE se récupére dans la liste à gauche de l'affectation.

python

 1: import fileinput
 2: import re
 3:
 4: h = {}
 5: p = re.compile('\s[0-9A-F]{10}:')
 6: reg = re.compile('^(?P<before>.*): (?P<message_id>[0-9A-F]{10}):(?P<after> .*)')
 7: for line in fileinput.input():
 8:     if p.search(line):
 9:         line_inter=re.sub('^(\w+ \d+ +\d+:\d+:\d+) \w+ (.*?)\[\d+\]',r'\1 \2',line)
10:         m=reg.match(line_inter)
11:         if m:
12:             if m.group('message_id') in h.keys():
13:                 h[m.group('message_id')]=h[m.group('message_id')]+m.group('before')+m.group('after')+'\n'
14:             else:
15:                 h[m.group('message_id')]=m.group('before')+m.group('after')+'\n'
16:
17: for key in h.keys():
18:     print key+" => "
19:     print h[key]

ruby

 1: %w(rubygems oniguruma pp).each do |lib| require lib end
 2: # oniguruma est la lib RE par défaut sous ruby 1.9
 3: # supporte différentes syntaxes de regexp, permet le "negative matching" et le "named group matching"
 4:
 5: list   = {}
 6: regexp = Oniguruma::ORegexp.new('^(?<before>.*): (?<message_id>[0-9A-F]{10}): (?<interresting_part>.*)')
 7: $stdin.each_line do |line|
 8:   m = regexp.match(line)
 9:   list[m[:message_id]] = "#{list[m[:message_id]]}\n#{m[:before]}#{m[:interresting_part]}" unless m.nil?
10: end
11: pp list

Etape 4

Question

Conserver le hash, mais cette fois, pour une clé donnée, la valeur du hash sera la liste des lignes relatives à ce message-ID.

Faire un dump du hash en fin de programme.

Réponse

perl

 1: use Data::Dumper ;
 2: use strict ;
 3:
 4: my %h ;
 5: while(<>)
 6: {
 7:   next unless /\s[0-9A-F]{10}:/ ;
 8:   s/^(\w+ \d+ +\d+:\d+:\d+) \w+ (.*?)\[\d+\]/$1 $2/ ;
 9:   my($begin,$id,$end) = /^(.*) ([0-9A-F]{10})(: .*)/ ;
10:   push(@{$h{$id}},$begin.$end) ;
11: }
12: print Dumper \%h ;

Explication de la ligne #10 :

  • $h{$id} : on s'intéresse à l'élément de hash de clé $id
  • @{$h{$id}} : on dit que la valeur de cet élément est une liste
  • push(@{$h{$id}},$begin.$end) : on pousse $begin.$end en tete de liste

python

 1: import fileinput
 2: import re
 3: import pprint
 4:
 5: h = {}
 6: p = re.compile('\s[0-9A-F]{10}:')
 7: reg = re.compile('^(?P<before>.*): (?P<message_id>[0-9A-F]{10}):(?P<after> .*)')
 8: for line in fileinput.input():
 9:     if p.search(line):
10:         line_inter=re.sub('^(\w+ \d+ +\d+:\d+:\d+) \w+ (.*?)\[\d+\]',r'\1 \2',line)
11:         m=reg.match(line_inter)
12:         if m:
13:             if m.group('message_id') in h.keys():
14:                 h[m.group('message_id')].append(m.group('before')+m.group('after'))
15:             else:
16:                 h[m.group('message_id')]=[m.group('before')+m.group('after')]
17: pprint.pprint(h)

ruby

 1: %w(rubygems oniguruma pp).each do |lib| require lib end
 2: # oniguruma est la lib RE par défaut sous ruby 1.9
 3: # supporte différentes syntaxes de regexp, permet le "negative matching" et le "named group matching"
 4:
 5: list   = {}
 6: regexp = Oniguruma::ORegexp.new('^(?<before>.*): (?<message_id>[0-9A-F]{10}): (?<interresting_part>.*)')
 7: $stdin.each_line do |line|
 8:   m = regexp.match(line)
 9:   begin
10:     array = list[m[:message_id]] || []                                   # on traite le cas ou aucune entrée n'existe
11:     list[m[:message_id]] = array << m[:before]+" "+m[:interresting_part] # l'opérateur '+' marche sur les strings
12:   end unless m.nil?                                                      # ce sont des objets comme les autres
13: end
14: pp list

Etape 5

Question

Transformer le programme précédent pour qu'il récupère sur la ligne de commande le nom du fichier de sortie (option -o). Faire une sortie propre du hash (plus propre qu'un dump).

Exemple d'utilisation :

$ ./myprog -o summary.log maillog.1 maillog.2

Ecrire un message d'erreur si l'option -o est omise.

Indication Perl: module Getopt::Long

Réponse

bash

 1: #!/bin/bash
 2:
 3: # Fonction pour afficher l'utilisation du script
 4: function usage() {
 5:     cat <<EOF
 6: $0 -o <output_file> <maillog_file> [ <maillog_file...]
 7: EOF
 8: }
 9:
10: # Vérification de la ligne de commande
11: OUTFILE=
12: FILES=
13: while getopts "o:" OPT
14: do
15:     case $OPT in
16:     o)
17:         OUTFILE=$OPTARG
18:         ;;
19:     ?)
20:         echo "Option $OPT inconnue" > /dev/stderr
21:         usage
22:         exit 1
23:     esac
24: done
25: shift $((OPTIND - 1)) # Elimine de $* les arguments traités par getopt
26: if [ -z "$OUTFILE" ]; then
27:     echo "L'option -o est obligatoire" > /dev/stderr
28:     usage
29:     exit 1
30: fi
31: if [ -z "$*" ]; then
32:     echo "Vous devez spécifiez au moins un fichier à analyser"
33:     usage
34:     exit 1
35: fi
36:
37: # Fichier intermédiaire pour limiter le nombre d'appel
38: # à grep au sein d'une boucle
39: # $$ est le PID du script => unicité du fichier tampon
40: BUFFILE=/tmp/maillog-tmp.$$
41:
42: # Construction du fichier intermédiaire :
43: # - grep sélectionne les lignes avec un message_id dans les N fichiers d'entrée
44: # - sed replace le message_id en debut de ligne, retire le PID et
45: #   le nom du host
46: # - comme toutes les lignes commencent par un message_id, sort
47: #   permet de les rassembler
48: grep -h -E ' [0-9A-F]{10}:' $* |  sed 's/^\(\w\+ \+[0-9]\+ [0-9]\+:[0-9]\+:[0-9]\+\) \w\+ \(.*\)\[[0-9]*\]: \([0-9A-F]\{10\}\):\(.*\)/\3\1 \2\4/g' | sort > $BUFFILE
49:
50: # Affichage de toutes les lignes relatives à un message_id
51: MSID=""
52: while read LINE
53: do
54:   K=${LINE:0:10} # 10 premiers caractères => message ID
55:   if [ ! "$K" = "$MSID" ]; then
56:       # Première fois que l'on rencontre ce message_id => nouveau bloc
57:       [ -z "$MSID" ] || echo ""
58:       MSID=$K
59:       echo "$MSID:"
60:   fi
61:   echo -e "\t${LINE:10}" # Le reste de la ligne avec le message de log
62: done < $BUFFILE > $OUTFILE
63:
64: rm $BUFFILE

perl

 1: use FileHandle ;
 2: use Getopt::Long ;                   # pour récupérer les arguments de ligne de commande
 3: use strict ;
 4:
 5: #-------------
 6: sub usage                                 # ceci est une définition de fonction
 7: {
 8:   my ($msg) = @_ ;
 9:
10:   print << "EOD" ;
11: $msg
12: usage: $0 -o output_file files
13:
14: EOD
15:   exit(1) ;
16: }
17:
18: #-------------
19: my ($verbose,$fout) ;
20:
21: GetOptions(
22:         "o=s" => \$fout
23: ) or usage("syntax error") ;
24: usage("-o arg is missing") unless $fout=~/\S/ ;
25:
26: my $h ;                                 # un pointeur de hash, pour changer
27:
28: # parsing et stockage en ram
29: while(<>)
30: {
31:   next unless /\s[0-9A-F]{10}:/ ;
32:   s/^(\w+ \d+ +\d+:\d+:\d+) \w+ (.*?)\[\d+\]/$1 $2/ ;
33:   my($begin,$id,$end) = /^(.*) ([0-9A-F]{10})(: .*)/ ;
34:   push(@{$h->{$id}},$begin.$end) ;      # noter la flèche !
35: }
36:
37: # écriture dans fichier
38: my $fh = new FileHandle(">$fout") or die $! ;
39: for my $key (keys %$h)                  # pour toutes les clés
40: {
41:   $fh->print("\n$key:\n") ;
42:   for my $i (@{$h->{$key}})             # pour toute la liste
43:   {
44:     $fh->print("\t$i\n") ;
45:   }
46: }
47: $fh->close() ;

Explication

  • lignes 10-14 :
    • c'est un heredoc : tout ce qui est entre «“EOD” et EOD est une chaîne de caractères qui vient remplacer «EOD
    • c'est équivalent à : print “$msg\nusage: $0 -o output_file files\n” ;
    • le délimiteur (ici EOD) peut être n'importe quelle chaîne
    • pour terminer, le délimiteur doit être le seul élément de la ligne (pas d'espace, même en fin de ligne)
  • le module Getopt::Long permet d'extraire les options de la ligne de commande, et d'affecter des variables ou déclencher des fonctions automatiquement en fonction des options trouvées. Voir man Getopt::Long ou sur www.cpan.org.
  • le module FileHandle permet de gérer les fichiers de données de façon objet. Voir man FileHandle ou sur www.cpan.org.

python

 1: #!/usr/bin/python
 2: import fileinput
 3: import re
 4: import getopt
 5: import sys
 6:
 7: filename=None
 8: try:
 9:     opt,args = getopt.getopt(sys.argv[1:], "o:", ["output="])
10: except getopt.GetoptError,err:
11:     print str(err)
12:     sys.exit(2)
13:
14: for option in opt:
15:     if option[0]=='-o' or option[0]=="--output":
16:         filename=option[1]
17:
18: sys.argv[1:]=args
19: if filename==None:
20:     print "l'option -o est obligatoire"
21:     sys.exit(2)
22:
23: h = {}
24: p = re.compile('\s[0-9A-F]{10}:')
25: reg = re.compile('^(?P<before>.*): (?P<message_id>[0-9A-F]{10}):(?P<after> .*)')
26: for line in fileinput.input():
27:         if p.search(line):
28:                 line_inter=re.sub('^(\w+ \d+ +\d+:\d+:\d+) \w+ (.*?)\[\d+\]',r'\1 \2',line)
29:                 m=reg.match(line_inter)
30:                 if m:
31:                         if m.group('message_id') in h.keys():
32:                                 h[m.group('message_id')].append(m.group('before')+m.group('after'))
33:                         else:
34:                                 h[m.group('message_id')]=[m.group('before')+m.group('after')]
35:
36:
37: fh=open(filename,'w')
38: for key in h.keys():
39:     fh.write('\n%s:\n'%key)
40:     for elt in h[key]:
41:         fh.write('\t%s\n'%elt)
42: fh.close()

ruby

 1: #!/usr/bin/env ruby
 2:
 3: #
 4: #
 5: # == Synopsis
 6: #
 7: #  Débroussailler des log d'un serveur de mails
 8: #  Version qui gère les argument de la ligne de commande
 9: #
10: # == Usage
11: #
12: #  ./myprog --output-file output.txt
13: #
14: #
15:
16: %w(rubygems oniguruma pp getoptlong rdoc/usage).each do |lib| require lib end
17:
18: Options  = Struct.new(  :output_file, :verbose )
19: @options = Options.new( nil,          false    )
20:
21: @regexp  = Oniguruma::ORegexp.new('^(?<before>.*): (?<message_id>[0-9A-F]{10}): (?<interresting_part>.*)')
22:
23: #
24: # parse arguments on command line
25: #
26: def parse_command_line
27:         if ARGV.length == 0
28:                 RDoc::usage
29:                 exit 1
30:         end
31:
32:         opts = GetoptLong.new(
33:                 [ '--output-file','-o', GetoptLong::REQUIRED_ARGUMENT ],
34:         )
35:
36:         begin
37:                 opts.each{|opt,arg|
38:                         case opt
39:                         when '--output-file'
40:                                 @options.output_file = arg
41:                         end
42:                 }
43:         rescue Exception
44:                 exit 2
45:         end
46: end
47: def parse_line(line)
48:         m = @regexp.match(line)
49:         begin
50:                 array = @h[m[:message_id]] || []
51:                 @h[m[:message_id]] = array << m[:before]+" "+m[:interresting_part]
52:         end unless m.nil?
53: end
54: def read_from_stdin_and_write_to_file
55:         @h = {}
56:         $stdin.each_line do |line| parse_line(line) end
57:         File.open( "./#{@options.output_file}", File::WRONLY|File::TRUNC|File::CREAT, 0600) do |output|
58:                 PP.pp(@h,output)
59:         end # auto-close output file
60:         return @h
61: end
62: def main
63:         parse_command_line
64:         read_from_stdin_and_write_to_file
65: end
66:
67: main
68:
69: __END__

Etape 6

Question

Ajouter une option -v qui affiche sur stdout le nombre de mails traités, et une option - -help qui affiche la syntaxe d'utilisation de la commande.

Réponse

bash

 1: #!/bin/bash
 2:
 3: # Fonction pour afficher l'utilisation du script
 4: function usage() {
 5:     cat <<EOF
 6: $0 [-v] -o <output_file> <maillog_file> [ <maillog_file...]
 7: $0 -h
 8: EOF
 9: }
10:
11: # Vérification de la ligne de commande
12: OUTFILE=
13: FILES=
14: SHOWMAILCOUNT=0
15: while getopts "vo:" OPT
16: do
17:     case $OPT in
18:     h)
19:         usage
20:         exit 0
21:         ;;
22:     o)
23:         OUTFILE=$OPTARG
24:         ;;
25:     v)
26:         SHOWMAILCOUNT=1
27:         ;;
28:     ?)
29:         echo "Option $OPT inconnue" > /dev/stderr
30:         usage
31:         exit 1
32:     esac
33: done
34: shift $((OPTIND - 1)) # Elimine de $* les arguments traités par getopt
35: if [ -z "$OUTFILE" ]; then
36:     echo "L'option -o est obligatoire" > /dev/stderr
37:     usage
38:     exit 1
39: fi
40: if [ -z "$*" ]; then
41:     echo "Vous devez spécifiez au moins un fichier à analyser"
42:     usage
43:     exit 1
44: fi
45:
46: # Fichier intermédiaire pour limiter le nombre d'appel
47: # à grep au sein d'une boucle
48: # $$ est le PID du script => unicité du fichier tampon
49: BUFFILE=/tmp/maillog-tmp.$$
50:
51: # Construction du fichier intermédiaire :
52: # - grep sélectionne les lignes avec un message_id dans les N fichiers d'entrée
53: # - sed replace le message_id en debut de ligne, retire le PID et
54: #   le nom du host
55: # - comme toutes les lignes commencent par un message_id, sort
56: #   permet de les rassembler
57: grep -h -E ' [0-9A-F]{10}:' $* |  sed 's/^\(\w\+ \+[0-9]\+ [0-9]\+:[0-9]\+:[0-9]\+\) \w\+ \(.*\)\[[0-9]*\]: \([0-9A-F]\{10\}\):\(.*\)/\3\1 \2\4/g' | sort > $BUFFILE
58:
59: # Affichage de toutes les lignes relatives à un message_id et comptage
60: # du nombre de mail traités
61: MSID=""
62: COUNT=0
63: while read LINE
64: do
65:   K=${LINE:0:10} # 10 premiers caractères => message ID
66:   if [ ! "$K" = "$MSID" ]; then
67:       # Première fois que l'on rencontre ce message_id => nouveau bloc
68:       [ -z "$MSID" ] || echo ""
69:       MSID=$K
70:       echo "$MSID:"
71:       ((COUNT++))
72:   fi
73:   echo -e "\t${LINE:10}" # Le reste de la ligne avec le message de log
74: done < $BUFFILE > $OUTFILE
75:
76: # Si -v, affichage du nombre de mails traités
77: if [ $SHOWMAILCOUNT -eq 1 ] ; then
78:     echo "$COUNT mails traités"
79: fi
80:
81: rm $BUFFILE

perl

 1: use FileHandle ;
 2: use Getopt::Long ;
 3: use strict ;
 4:
 5: #-------------
 6: sub usage
 7: {
 8:   my ($msg) = @_ ;
 9:
10:   print << "EOD" ;
11: $msg
12: usage: $0 [-v] -o output_file files
13:        $0 --help
14: EOD
15:   exit(1) ;
16: }
17:
18: #-------------
19: my ($verbose,$fout) ;
20:
21: GetOptions(
22:         "h|help" => \&usage             # envoie vers une fonction
23:         , "v|verbose+" => \$verbose
24:         , "o=s" => \$fout
25: ) or usage("syntax error") ;
26: usage("-o arg is missing") unless $fout=~/\S/ ;
27:
28: my $h ;
29: my $nb = 0 ;
30:
31: while(<>)
32: {
33:   next unless /\s[0-9A-F]{10}:/ ;
34:   s/^(\w+ \d+ +\d+:\d+:\d+) \w+ (.*?)\[\d+\]/$1 $2/ ;
35:   my($begin,$id,$end) = /^(.*) ([0-9A-F]{10})(: .*)/ ;
36:   $nb++ unless exists $h->{$id} ;
37:   push(@{$h->{$id}},$begin.$end) ;
38: }
39:
40: my $fh = new FileHandle(">$fout") or die $! ;
41: for my $key (keys %$h)
42: {
43:   $fh->print("\n$key:\n") ;
44:   for my $i (@{$h->{$key}})
45:   {
46:     $fh->print("\t$i\n") ;
47:   }
48: }
49: $fh->close() ;
50: print "$nb mails traités\n" if $verbose ;

python

 1: #!/usr/bin/python
 2: '''Usage : ./myprog [-h] [-v] -o output_file files'''
 3: import fileinput
 4: import re
 5: import getopt
 6: import sys
 7:
 8:
 9: filename=None
10: verbose=None
11: try:
12:     opt,args = getopt.getopt(sys.argv[1:], "o:hv", ["output=","help","verbose"])
13: except getopt.GetoptError,err:
14:     print str(err)
15:     sys.exit(2)
16:
17: for option in opt:
18:     if option[0]=='-o' or option[0]=="--output":
19:         filename=option[1]
20:     if option[0]=='-h' or option[0]=="--help":
21:         print __doc__
22:         sys.exit(1)
23:     if option[0]=='-v' or option[0]=="--verbose":
24:         verbose=True
25:
26: sys.argv[1:]=args # on nettoie la ligne de commande pour fileinput
27: if filename==None:
28:     print "l'option -o est obligatoire"
29:     sys.exit(2)
30:
31: h = {}
32: p = re.compile('\s[0-9A-F]{10}:')
33: reg = re.compile('^(?P<before>.*): (?P<message_id>[0-9A-F]{10}):(?P<after> .*)')
34: for line in fileinput.input():
35:         if p.search(line):
36:                 line_inter=re.sub('^(\w+ \d+ +\d+:\d+:\d+) \w+ (.*?)\[\d+\]',r'\1 \2',line)
37:                 m=reg.match(line_inter)
38:                 if m:
39:                         if m.group('message_id') in h.keys():
40:                                 h[m.group('message_id')].append(m.group('before')+m.group('after'))
41:                         else:
42:                                 h[m.group('message_id')]=[m.group('before')+m.group('after')]
43:
44:
45: fh=open(filename,'w')
46: for key in h.keys():
47:     fh.write('\n%s:\n'%key)
48:     for elt in h[key]:
49:         fh.write('\t%s\n'%elt)
50: fh.close()
51:
52: if verbose:
53:     print "%s mails traites"%len(h.keys())

ruby

 1: #!/usr/bin/env ruby
 2:
 3: #
 4: #
 5: # == Synopsis
 6: #
 7: #  Débroussailler des log d'un serveur de mails
 8: #  Version qui gère les argument de la ligne de commande
 9: #
10: # == Usage
11: #
12: #  ./myprog --help
13: #  ./myprog --output-file output.txt
14: #  ./myprog ---verbose --output-file output.txt
15: #
16:
17: %w(rubygems oniguruma pp getoptlong rdoc/usage).each do |lib| require lib end
18:
19: Options  = Struct.new(  :output_file, :verbose )
20: @options = Options.new( nil,          false    )
21:
22: @regexp  = Oniguruma::ORegexp.new('^(?<before>.*): (?<message_id>[0-9A-F]{10}): (?<interresting_part>.*)')
23:
24: #
25: # parse arguments on command line
26: #
27: def parse_command_line
28:         if ARGV.length == 0
29:                 RDoc::usage
30:                 exit 1
31:         end
32:
33:         opts = GetoptLong.new(
34:                 [ '--help',       '-h', GetoptLong::NO_ARGUMENT       ],
35:                 [ '--output-file','-o', GetoptLong::REQUIRED_ARGUMENT ],
36:                 [ '--verbose',    '-v', GetoptLong::NO_ARGUMENT       ]
37:         )
38:
39:         begin
40:                 opts.each{|opt,arg|
41:                         case opt
42:                         when '--help'
43:                                 RDoc::usage
44:                         when '--output-file'
45:                                 @options.output_file = arg
46:                         when '--verbose'
47:                                 @options.verbose     = true
48:                         end
49:                 }
50:         rescue GetoptLong::MissingArgument
51:                 exit 2
52:         end
53: end
54: def parse_line(line)
55:         m = @regexp.match(line)
56:         begin
57:                 array = @h[m[:message_id]] || []
58:                 @h[m[:message_id]] = array << m[:before]+" "+m[:interresting_part]
59:         end unless m.nil?
60: end
61: def read_from_stdin_and_write_to_file
62:         @h = {}
63:         $stdin.each_line do |line| parse_line(line) end
64:         File.open( "./#{@options.output_file}", File::WRONLY|File::TRUNC|File::CREAT, 0600) do |output|
65:                 PP.pp(@h,output)
66:         end # auto-close output file
67:         return @h
68: end
69: def main
70:         parse_command_line
71:         h = read_from_stdin_and_write_to_file
72:         puts h.size if @options.verbose
73: end
74:
75: main
76:
77: __END__

Version

$Id: maillog.txt 682 2012-05-28 17:22:12Z jaclin $

maillog.txt · Last modified: 2012/05/28 19:22 (external edit)
 
Except where otherwise noted, content on this wiki is licensed under the following license: CC Attribution-Share Alike 3.0 Unported
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki