SPA3000 syslog parser and logwatch module

This is a script suitable for use with logwatch that parses the syslog debug output from a Sipura SPA3000 VoIP ATA and prints a list of call made and received calls and their times. It might also work with other Sipura models.

   1 #!/usr/bin/env python2.4
   2 
   3 # logwatch script for parsing syslog output from Sipura SPA3000 devices
   4 # andrewb@cse.unsw.edu.au, 2007/01/12
   5 
   6 import sys, re, time, datetime
   7 
   8 # list of all IP addresses for the Sipura; used to detect PSTN calls
   9 # FIXME: make this automatic, or configurable elsewhere
  10 LOCAL_IPS = ['127.0.0.1', '192.168.0.6']
  11 
  12 SYSLOG_RE = re.compile(r'^ *(?Ptime... .. ..:..:..) (?Phost\S*) (?Pdata.*)')
  13 NEWMSG_RE = re.compile(r'^\[\d+:\d+\](?Pdir|-)(?Ppeer\d+\.\d+\.\d+\.\d+:\d+)$')
  14 SIP_URI_RE = re.compile(r'^sip:(?Puser.*)@(?Phost[^:]+)(:(?Pport\d+))?$')
  15 SYSLOG_TIME = '%b %d %H:%M:%S'
  16 DISPLAY_TIME = '%b %d %H:%M'
  17 
  18 STATE_SETUP = 1
  19 STATE_RINGING = 2
  20 STATE_INPROGRESS = 3
  21 STATE_TERMINATED = 4
  22 
  23 class SIPCallState:
  24     def __init__(self):
  25         self.originator = None
  26         self.state = None
  27         self.uri = None
  28         self.starttime = None
  29         self.duration = None
  30 
  31     def got_cmd(self, ts, cmd, arg):
  32         if cmd == 'INVITE':
  33             self.originator = False
  34             self.state = STATE_SETUP
  35         elif cmd == 'BYE' or cmd == 'CANCEL':
  36             self.state = STATE_TERMINATED
  37             if not self.uri:
  38                 self.uri = arg
  39             if self.starttime and not self.duration:
  40                 self.duration = ts - self.starttime
  41         elif cmd == 'ACK':
  42             if self.state == STATE_SETUP or self.state == STATE_RINGING:
  43                 assert(self.uri == arg)
  44                 self.state = STATE_INPROGRESS
  45                 self.starttime = ts
  46 
  47     def got_reply(self, ts, status):
  48         if self.state == STATE_SETUP and status / 10 == 18:
  49             self.state = STATE_RINGING
  50         elif self.state in [STATE_SETUP, STATE_RINGING] and status / 100 == 2:
  51             self.state = STATE_INPROGRESS
  52             self.starttime = ts
  53         elif status / 100 = 4:
  54             self.state = STATE_TERMINATED
  55 
  56     def sent_cmd(self, ts, cmd, arg):
  57         if cmd == 'INVITE':
  58             self.originator = True
  59             self.state = STATE_SETUP
  60             self.uri = arg
  61         else:
  62             self.got_cmd(ts, cmd, arg)
  63 
  64     def sent_reply(self, ts, status):
  65         return self.got_reply(ts, status)
  66 
  67 class StatsGatherer:
  68     def __init__(self):
  69         self.last_peer = self.last_inout = None
  70         self.peers = {}
  71         self.calls_made = []
  72         self.calls_received = []
  73 
  74     def process_entry(self, timestamp, line):
  75         match = NEWMSG_RE.match(line)
  76         if match:
  77             self.last_inout = (match.group('dir') == '-')
  78             ip, port = match.group('peer').split(':', 1)
  79             if ip in LOCAL_IPS:
  80                 ip = LOCAL_IPS[0]
  81             self.last_peer = ip + ':' + port
  82         else:
  83             words = line.split()
  84             is_reply = words[0].startswith('SIP/')
  85             is_command = words[-1].startswith('SIP/')
  86             if is_reply or is_command:
  87                 assert(len(words)  2)
  88                 callstate = self.peers.setdefault(self.last_peer, SIPCallState())
  89                 if is_reply:
  90                     status = int(words[1])
  91                     if self.last_inout:
  92                         callstate.sent_reply(timestamp, status)
  93                     else:
  94                         callstate.got_reply(timestamp, status)
  95                 else:
  96                     if self.last_inout:
  97                         callstate.sent_cmd(timestamp, words[0], words[1])
  98                     else:
  99                         callstate.got_cmd(timestamp, words[0], words[1])
 100                 if callstate.state == STATE_TERMINATED:
 101                     self.__process_call(callstate)
 102                     del self.peers[self.last_peer]
 103 
 104     def __process_call(self, callstate):
 105         if not callstate.duration:
 106             return
 107         if callstate.originator:
 108             self.calls_made.append((callstate.uri, callstate.starttime, callstate.duration))
 109         else:
 110             self.calls_received.append((callstate.uri, callstate.starttime, callstate.duration))
 111 
 112     def print_stats(self, fh):
 113         for (calls, desc, received) in [(self.calls_made, 'made', False),
 114                                         (self.calls_received, 'received', True)]:
 115             if calls != []:
 116                 fh.write('Calls %s:\n' % desc)
 117             for (uri, start, duration) in calls:
 118                 if uri is None:
 119                     uri = 'unknown'
 120                 else:
 121                     match = SIP_URI_RE.match(uri)
 122                     if match:
 123                         host = match.group('host')
 124                         if host in LOCAL_IPS:
 125                             if received:
 126                                 uri = '(PSTN)'
 127                             else:
 128                                 uri = match.group('user')
 129                         else:
 130                             uri = match.group('user') + '@' + host
 131                 fh.write(' %s  %-30s %s\n' % (start.strftime(DISPLAY_TIME), uri, duration))
 132 
 133 def process_syslog(fh, statobj):
 134     prevline = None
 135     for line in fh.readlines():
 136         if line == prevline:
 137             continue
 138         match = SYSLOG_RE.match(line)
 139         timestr = match.group('time')
 140         timestamp = datetime.datetime(*(time.strptime(timestr, SYSLOG_TIME)[:6]))
 141         contents = match.group('data').strip()
 142         if contents != '':
 143             statobj.process_entry(timestamp, contents)
 144         prevline = line
 145 
 146 if __name__ == '__main__':
 147     statobj = StatsGatherer()
 148     process_syslog(sys.stdin, statobj)
 149     statobj.print_stats(sys.stdout)

spa3k.py

Published by

ab

Well, AB's just this guy, you know?