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'^ *(?P<time>... .. ..:..:..) (?P<host>\S*) (?P<data>.*)')
13 NEWMSG_RE = re.compile(r'^\[\d+:\d+\](?P<dir><<|->)(?P<peer>\d+\.\d+\.\d+\.\d+:\d+)$')
14 SIP_URI_RE = re.compile(r'^sip:(?P<user>.*)@(?P<host>[^:]+)(:(?P<port>\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)
150