#!/bin/env python3 # # simple log parser and status server for the jitsi meet video conference tool # # Reik Kaps 2020 for Leinelab Hannover import re import sys import threading from os.path import basename from datetime import datetime from argparse import ArgumentParser from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs try: from sh import tail from pybadges import badge except ImportError as e: print('[Error] Please install sh via pip! {}'.format(e)) sys.exit(1) # active room dictionary roomList = {} # @see https://regex101.com/r/R1B1OL/1 # general regex for a jicofo log line regex = r"(Jicofo)\s(\d+-\d+-\d+)\s(\d+\:\d+\:\d+\.\d+)\s(\w+):\s\[(\d+)\]\s(.*)\(\)\s(.*)" prog = re.compile(regex) ## # new jitsi room # room_regex = r"(^Joining\sthe\sroom):(.*)" room = re.compile(room_regex) # disposed room disposed_regex = r"^Disposed\sconference\sfor\sroom\:\s(.*)@(.*)\sconference\scount:\s(\d+)" disposed = re.compile(disposed_regex) ## # get the room owner owner_regex = r"Granted\sowner\sto\s([a-z0-9-]+)\@(.*)\/(.*)" owner = re.compile(owner_regex) # get member for room member_regex = r"^Member\s(.*)@(.*)\/(.*)\s(joined)." member = re.compile(member_regex) # remove/leaving member leaving_regex = r"^Member\s(.*)@(.*)\/(.*)\sis\s(leaving)" leaving = re.compile(leaving_regex) class MeetingRoom(): """ represent a Jitsi Meetingroom jid: eg room@conference.meet.leinelab.org ctime: eg 2020-06-01 12:32:02 .. """ def __init__(self, jid, ctime): self.jid = jid self.ctime = ctime self.members = [] def setOwner(self, name): self.owner = name def getOwner(self): if hasattr(self, 'owner'): return self.owner else: return None def addMember(self, name): self.members.append(name) def delMember(self, name): try: self.members.remove(name) except ValueError as e: raise def getMembers(self): return self.members def __str__(self): return '{}: {}'.format(self.jid, len(self.members)) class logThread(threading.Thread): def __init__(self, threadID, name, filename): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.filename = filename def run(self): while True: getLogMsg(self.filename) class MyServer(BaseHTTPRequestHandler): def do_response(self, response_string, ctype="text/plain"): self.send_response(200) self.send_header("Content-type", ctype) self.end_headers() self.wfile.write(response_string) def do_GET(self): query_components = parse_qs(urlparse(self.path).query) if "format" in query_components: respones_format = query_components["format"][0] else: respones_format = 'text' room_name = basename(self.path.split('?')[0]) print('{} - {}'.format(query_components, room_name)) if room_name in roomList: if respones_format == 'json': pass elif respones_format == 'svg': participants = len(roomList[room_name].members) if participants > 1: rtext = '{} participants'.format(str(participants)) else: rtext = '{} participant'.format(str(participants)) s = badge(left_text='{}: {}'.format('Room', room_name), right_text=rtext, left_color='blue', right_color='green') response_string = bytes(s, "utf-8") self.do_response(response_string, "image/svg+xml") else: # default text response response_string = bytes(room_name + '|' + str(roomList[room_name].ctime) + '|' + str(len(roomList[room_name].members)) + '|', "utf-8") self.do_response(response_string) else: if respones_format == 'svg': s = badge(left_text='{}: {}'.format('Room', room_name), right_text='empty', left_color='red', right_color='darkgray') response_string = bytes(s, "utf-8") self.text_response(response_string, "image/svg+xml") else: # text or other formats # return http code 404 (room) not found self.send_response(404) self.end_headers() def process_line(line: str): """ process a logfile line, using global object and vars globals: prog, disposed, owner, member, roomList Attributes ---------- line: str a line form the logfile """ erg = prog.match(line) if erg: # detect room creation new_room = room.match(erg.group(7)) if new_room: room_name = new_room.group(2).split("@")[0].strip() room_date = datetime.fromisoformat('{} {}'. format(erg.group(2), erg.group(3))) print('{} {} created'.format(room_date, room_name)) currentRoom = MeetingRoom(room_name, room_date) if room_name in roomList: # delete old room and recreate it del roomList[room_name] roomList[room_name] = currentRoom else: roomList[room_name] = currentRoom disposed_room = disposed.match(erg.group(7)) if disposed_room: room_name = disposed_room.group(1) print('-> room disposed: {}'.format(room_name)) if room_name in roomList: del roomList[room_name] owner_match = owner.match(erg.group(7)) if owner_match: room_name = owner_match.group(1).strip() room_owner = owner_match.group(3).strip() if room_name in roomList: roomList[room_name].setOwner(room_owner) member_match = member.match(erg.group(7)) if member_match: room_name = member_match.group(1).strip() room_member = member_match.group(3).strip() print('{} enters {}'.format(room_member, room_name)) if room_name in roomList: roomList[room_name].addMember(room_member) # participant is leaving a the room leaving_match = leaving.match(erg.group(7)) if leaving_match: room_name = leaving_match.group(1).strip() room_member = leaving_match.group(3).strip() print('{} leaves {}'.format(room_member, room_name)) if room_name in roomList: roomList[room_name].delMember(room_member) def getLogMsg(logfile: str): """ get log messages and commit it to process_line used tail from sh module Attributes: ----------- logfile: str path as string to logfile """ for line in tail("-f", logfile, _iter=True): process_line(line) if __name__ == '__main__': # start cmdline processing parser = ArgumentParser(description='Jitsi Meet Conference Stats') parser.add_argument('--log', '-l', help='full path to jicofo logfile eg.' + ' /var/log/jitsi/jicofo.log') parser.add_argument('--verbose', '-v', help='be verbose eg. for debugging', action='store_true') parser.add_argument('--port', '-p', help='set the http server port', default=9999) args = parser.parse_args() if args.log: try: loggerThread = logThread(1, "Thread-Logger", args.log) loggerThread.start() except Exception as e: print('Error: unable to start threads {}'.format(e)) hostName = 'localhost' serverPort = args.port webServer = HTTPServer((hostName, serverPort), MyServer) try: print("Server started http://%s:%s" % (hostName, serverPort)) webServer.serve_forever() except: pass