Files
modoboa/extensions/stats/mailrrd.py
T

382 lines
13 KiB
Python
Executable File

#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
Interface with rrdtool to generate appropriate postfix graph statistics
sent, recv, bounced, rejected messages are tracked.
"""
import sys, os, rrdtool, re, time
import pdb
from optparse import OptionParser
try:
import django.conf
except:
print "[rrd] Warning no Django conf found"
rrdstep = 60
xpoints = 540
points_per_sample = 3
rrd_inited = False
this_minute = 0
rrd_file = "postfix.rrd"
png_file = "postfix.png"
log_file = "/var/log/maillog"
tmp_path = "static/tmp/"
months_map = {
'Jan' : 0, 'Feb' : 1, 'Mar' : 2,
'Apr' : 3, 'May' : 4, 'Jun' : 5,
'Jul' : 6, 'Aug' : 7, 'Sep' : 8,
'Oct' : 9, 'Nov' :10, 'Dec' :11,
'jan' : 0, 'feb' : 1, 'mar' : 2,
'apr' : 3, 'may' : 4, 'jun' : 5,
'jul' : 6, 'aug' : 7, 'sep' : 8,
'oct' : 9, 'nov' :10, 'dec' :11,
}
def str2Time(y,M,d,h,m,s):
"""str2Time
return epoch time from Year Month Day Hour:Minute:Second time format
"""
try:
local = time.strptime("%s %s %s %s:%s:%s" %(y,M,d,h,m,s), \
"%Y %b %d %H:%M:%S")
except:
print "[rrd] ERROR unrecognized %s time format" %(y,M,d,h,m,s)
exit(0)
return int(time.mktime(local))
class parser():
"""parser
Parse a mail log in syslog format:
Month day HH:MM:SS host prog[pid]: log message...
For each epoch record the parser stores number of
'sent', 'received', 'bounced','rejected' messages over
a rrdstep period of time.
parameters : logfile, and starting year
"""
def __init__(self,domain,logfile=log_file,rrdfile=rrd_file,imgFile=png_file,
create=False,year=None,debug=False,verbose=False,graph=None):
"""constructor
"""
self.domain = str(domain) #.replace(".","_")
self.enable_year_decrement = None
self.logfile = logfile
self.rrdfile = tmp_path + self.domain+"_"+rrdfile
self.create = create
self.year = year
self.debug = debug
self.verbose = verbose
self.graph = graph
self.imgFile = self.domain+"_"+imgFile
self.types = ['AVERAGE','MAX']
self.natures = ['sent_recv','boun_reje']
self.legend = {'sent_recv' : ['sent messages','received messages'],
'boun_reje' : ['bounced messages','rejected messages']}
try:
self.f = open(logfile)
except IOError as (errno, strerror):
print "I/O error({0}): {1} ".format(errno, strerror)+logfile
self.data = {}
self.last_month = None
if not self.year:
self.year = time.localtime().tm_year
self.first_minute = 0
self.last_minute = 0
if self.create and os.path.exists(logfile):
print "[rrd] force new RRD"
os.system("rm -f %s" %rrdfile)
elif os.path.exists(self.rrdfile):
self.last_minute = rrdtool.last(self.rrdfile)
self.first_minute = rrdtool.first(self.rrdfile)
if self.debug:
print "[rrd] DEBUG updating rrd from %s"\
%time.asctime(time.localtime(self.last_minute))
self.process_log()
def year_increment(self,month):
"""year_increment
"""
if month == 0:
if self.last_month and self.last_month == 1:
self.year += 1
self.enable_year_decrement = True
elif month == 11:
if self.enable_year_decrement \
and self.last_month and self.last_month != 1:
self.year -= 1
else:
self.enable_year_decrement = False
self.last_month = month
def process_line(self,text):
"""process_line
Parse a log line and look for
'sent','received','bounced',rejected' messages event
Return True if data are up-to-date
else False
"""
# get date
ret = False
m = re.match("(\w+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+(\w+)\s+(\w+)/?\w*[[](\d+)[]]:\s+(.*)",text)
if not m : return ret
mo = m.group(1)
da = m.group(2)
ho = m.group(3)
mi = m.group(4)
se = m.group(5)
host = m.group(6)
prog = m.group(7)
pid = m.group(8)
log = m.group(9)
sent = 0
recv = 0
boun = 0
reje = 0
self.year_increment(months_map[mo])
se = int(int(se)/rrdstep) # rrd step is one-minute => se = 0
cur_t = str2Time(self.year,mo,da,ho,mi,se)
if not self.data.has_key(cur_t):
self.data[cur_t] = {'sent':0,'recv':0, 'boun':0,'reje':0}
# watch events
if re.match(".*\s+status=sent\s+.*",text):
sent = 1
elif re.match(".*\s+status=bounced\s+.*",text):
boun = 1
elif re.match(".*[0-9A-Z]+: client=(\S+).*",text):
recv = 1
elif re.match(".*(?:[0-9A-Z]+: |NOQUEUE: )?reject: .*",text):
reje = 1
elif re.match(".*(?:[0-9A-Z]+: |NOQUEUE: )?milter-reject: .*",text):
if not re.match(".*Blocked by SpamAssassin.*",text):
reje = 1
#else regiter spam
# register events
if sent or recv or boun or reje:
self.data[cur_t]['sent'] = self.data[cur_t]['sent'] + sent
self.data[cur_t]['recv'] = self.data[cur_t]['recv'] + recv
self.data[cur_t]['boun'] = self.data[cur_t]['boun'] + boun
self.data[cur_t]['reje'] = self.data[cur_t]['reje'] + reje
return True
return False
def init_rrd(self,m):
"""init_rrd
Set-up Data Sources (DS)
Set-up Round Robin Archives (RRA):
- day,week,month and year archives
- 2 types : AVERAGE and MAX
parameter : start time
return : last epoch recorded
"""
ds_type = 'ABSOLUTE'
rows = xpoints / points_per_sample
realrows = int(rows * 1.1) # ensure that the full range is covered
day_steps = int(3600*24/(rrdstep * rows))
week_steps = day_steps * 7
month_steps = week_steps * 5
year_steps = month_steps * 12
# Set up data sources for our RRD
ds1 = 'DS:sent:%s:%s:0:U' %(ds_type,rrdstep*2)
ds2 = 'DS:recv:%s:%s:0:U' %(ds_type,rrdstep*2)
ds3 = 'DS:boun:%s:%s:0:U' %(ds_type,rrdstep*2)
ds4 = 'DS:reje:%s:%s:0:U' %(ds_type,rrdstep*2)
# Set up RRD to archive data
rra1 = 'RRA:%s:0.5:%s:%s' %('AVERAGE',day_steps,realrows) # day
rra2 = 'RRA:%s:0.5:%s:%s' %('AVERAGE',week_steps,realrows) # week
rra3 = 'RRA:%s:0.5:%s:%s' %('AVERAGE',month_steps,realrows) # month
rra4 = 'RRA:%s:0.5:%s:%s' %('AVERAGE',year_steps,realrows) # year
rra5 = 'RRA:%s:0.5:%s:%s' %('MAX',day_steps,realrows)
rra6 = 'RRA:%s:0.5:%s:%s' %('MAX',week_steps,realrows)
rra7 = 'RRA:%s:0.5:%s:%s' %('MAX',month_steps,realrows)
rra8 = 'RRA:%s:0.5:%s:%s' %('MAX',year_steps,realrows)
# With those setup, we can now created the RRD
if not os.path.exists(self.rrdfile):
rrdtool.create(self.rrdfile,
'--start',str(m),
'--step',str(rrdstep),
ds1,ds2,ds3,ds4,
rra1,rra2,rra3,rra4,
rra5,rra6,rra7,rra8)
this_minute = m
else:
this_minute = rrdtool.last(self.rrdfile) + rrdstep
return this_minute
def update_rrd(self,t):
"""update_rrd
Update RRD with records at t time.
True : if data are up-to-date for current minute
False : syslog may have probably been already recorded
or something wrong
"""
m = t - (t % rrdstep)
if not self.last_minute :
self.last_minute = self.init_rrd(m)
print "[rrd] create new RRD file"
if m < self.last_minute :
if self.verbose:
print "[rrd] VERBOSE events at %s already recorded in RRD" %m
return False
elif m == self.last_minute :
return True
# Missing some RRD steps
if m > self.last_minute + rrdstep:
for p in range(self.last_minute+rrdstep,m,rrdstep):
if self.verbose:
print "[rrd] VERBOSE update %s:%s:%s:%s:%s (SKIP)" \
%(p,'0','0','0','0')
rrdtool.update(self.rrdfile,"%s:%s:%s:%s:%s" \
%(p,'0','0','0','0'))
if self.verbose:
print "[rrd] VERBOSE update %s:%s:%s:%s:%s" \
%(m,self.data[m]['sent'], self.data[m]['recv'],\
self.data[m]['boun'], self.data[m]['reje'])
rrdtool.update(self.rrdfile,"%s:%s:%s:%s:%s" \
%(m,self.data[m]['sent'], self.data[m]['recv'],\
self.data[m]['boun'], self.data[m]['reje']))
self.last_minute = m
return True
def process_log(self):
"""process_log
Go through entire log file
"""
for line in self.f.readlines():
evt = self.process_line(line)
# Sort everything by time
sortedData = {}
sortedData = [ (i,self.data[i]) for i in sorted(self.data.keys()) ]
for t, dict in sortedData:
if self.update_rrd(t):
if not self.first_minute:
self.first_minute = t
self.last_minute = t
def plot_rrd(self,f=None,color1 = "#990033", color2 = "#330099",
year = None, start = None, end = None, t =None, n = None):
"""plot_rrd
Graph rrd from start to end epoch
"""
if f:
self.imgFile = f
start = str(self.first_minute)
end = str(self.last_minute)
if t:
self.types = t
if n:
self.natures = n
if self.graph:
try:
start = int(time.mktime(time.strptime(self.graph[0], \
"%Y %b %d %H:%M:%S")))
end = int(time.mktime(time.strptime(self.graph[1], \
"%Y %b %d %H:%M:%S")))
except:
print "[rrd] ERROR bad time format for option --graph"
print "[rrd] plot rrd graph from %s to %s" \
%(time.asctime(time.localtime(float(start))),\
time.asctime(time.localtime(float(end))))
if not year: year = time.localtime().tm_year
self.imgFile
if not os.path.exists(tmp_path):
os.mkdir(tmp_path)
for n in self.natures:
for t in self.types:
path = '%s%s_%s_%s'%(tmp_path,n,t,os.path.basename(self.imgFile))
ds1 = n.split('_')[0]
ds2 = n.split('_')[1]
print 'DEF:%s=%s:%s:%s:' %(ds1,ds1, self.rrdfile,t)
print 'DEF:%s=%s:%s:%s:' %(ds2,ds2, self.rrdfile,t)
print 'LINE:%s%s:%s messages' %(ds1,color1,self.legend[n][0])
print 'LINE:%s%s:%s messages' %(ds2,color2,self.legend[n][1])
rrdtool.graph(
path,
'--imgformat','PNG',
'--width','540',
'--height','100',
'--start', str(start),
'--end', str(end),
'--vertical-label', '%s message' %t.lower(),
'--title', '%s message flow per minute' %t.lower(),
'--lower-limit', '0',
'DEF:%s=%s:%s:%s:' %(ds1,self.rrdfile,ds1,t),
'DEF:%s=%s:%s:%s:' %(ds2,self.rrdfile,ds2,t),
'LINE:%s%s:%s' %(ds1,color1,self.legend[n][0]),
'LINE:%s%s:%s' %(ds2,color2,self.legend[n][1])
)
if __name__ == "__main__":
parser = OptionParser()
parser.add_option("-f","--file",dest="filename",default=rrdfile,
help="write rrd data base to FILE",metavar="FILE")
parser.add_option("-i","--imgFile",default=png_file,
help="graph rrd stats to FILE",metavar="FILE")
parser.add_option("-c","--create",default=False,action="store_true",dest="create",
help="force creation of new RRD")
parser.add_option("-g","--graph",nargs=2,dest="graph",
help="generate graph in between time period (y M d YY:MM:SS)", metavar="START STOP")
parser.parse_args(['--graph', 'start', 'stop'])
parser.add_option("-l","--logFile",default="maillog",
help="postfix log in syslog format",metavar="FILE")
parser.add_option("-v","--verbose",default=False,action="store_true",dest="verbose",
help="set verbose mode")
parser.add_option("-d","--debug",default=False,action="store_true",dest="debug",
help="set debug mode")
(options,args) = parseOption()
P = parser(logfile=options.logFile,rrdfile=options.file,
debug=options.debug,verbose=options.verbose,
create=options.create,graph=options.graph,imgFile=options.imgFile)
P.plot_rrd()