#!/usr/bin/python3 # # Plugin to monitor the types of requsts made to the API # # Uses the query log. # # Parameters: # # config (required) # autoconf (optional - used by munin-config) # import re import os import sys from datetime import datetime, timedelta CONFIG="""graph_title Requests by API call graph_args --base 1000 -l 0 graph_vlabel requests per minute graph_category nominatim z1.label reverse z1.draw AREA z1.type GAUGE z2.label search (successful) z2.draw STACK z2.type GAUGE z3.label search (no result) z3.draw STACK z3.type GAUGE z4.label details z4.draw STACK z4.type GAUGE""" ENTRY_REGEX = re.compile(r'\[[^]]+\] (?P[0-9.]+) (?P\d+) (?P[a-z]+) ') TIME_REGEX = re.compile(r'\[(?P\d\d\d\d)-(?P\d\d)-(?P\d\d) (?P\d\d):(?P\d\d):(?P\d\d)[0-9.]*\] ') class LogFile: """ A query log file, unpacked. """ def __init__(self, filename): self.fd = open(filename, encoding='utf-8', errors='replace') self.len = os.path.getsize(filename) def __del__(self): self.fd.close() def seek_next(self, abstime): self.fd.seek(abstime) self.fd.readline() l = self.fd.readline() e = TIME_REGEX.match(l) if e is None: return None e = e.groupdict() return datetime(int(e['t_year']), int(e['t_month']), int(e['t_day']), int(e['t_hour']), int(e['t_min']), int(e['t_sec'])) def seek_to_date(self, target): # start position for binary search fromseek = 0 fromdate = self.seek_next(0) if fromdate > target: return True # end position for binary search toseek = -100 while -toseek < self.len: todate = self.seek_next(self.len + toseek) if todate is not None: break toseek -= 100 if todate is None or todate < target: return False toseek = self.len + toseek while True: bps = (toseek - fromseek) / (todate - fromdate).total_seconds() newseek = fromseek + int((target - fromdate).total_seconds() * bps) newdate = self.seek_next(newseek) if newdate is None: return False; error = abs((target - newdate).total_seconds()) if error < 1: return True if newdate > target: toseek = newseek todate = newdate oldfromseek = fromseek fromseek = toseek - error * bps while True: if fromseek <= oldfromseek: fromseek = oldfromseek fromdate = self.seek_next(fromseek) break fromdate = self.seek_next(fromseek) if fromdate < target: break; bps *=2 fromseek -= error * bps else: fromseek = newseek fromdate = newdate oldtoseek = toseek toseek = fromseek + error * bps while True: if toseek > oldtoseek: toseek = oldtoseek todate = self.seek_next(toseek) break todate = self.seek_next(toseek) if todate > target: break bps *=2 toseek += error * bps if toseek - fromseek < 500: return True def loglines(self): for l in self.fd: e = ENTRY_REGEX.match(l) if e is None: raise ValueError("Invalid log line:", l) yield e.groupdict() if __name__ == '__main__': if len(sys.argv) > 1 and sys.argv[1] == 'config': print(CONFIG) sys.exit(0) reverse = 0 searchy = 0 searchn = 0 details = 0 if 'NOMINATIM_QUERYLOG' in os.environ: lf = LogFile(os.environ['NOMINATIM_QUERYLOG']) if lf.seek_to_date(datetime.now() - timedelta(minutes=5)): for l in lf.loglines(): if l['type'] == 'reverse': reverse += 1 elif l['type'] == 'search': if l['numres'] == '0': searchn += 1 else: searchy += 1 else: details += 1 print('z1.value', reverse/5) print('z2.value', searchy/5) print('z3.value', searchn/5) print('z4.value', details/5)