#!/usr/bin/env python3 import requests import json import time import curses import math import traceback class Monitor: _speedSamples = 8 _globalspeed = [] def __init__(self, url): self.data = dict() self.win = curses.initscr() curses.start_color() curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) self._url = url while len(self._globalspeed) < self._speedSamples: self._globalspeed.append((0, 0, 0, 0)) def __del__(self): curses.endwin() def on_timer(self, event): """called on timer event""" self.update_data() def jsonrpc(self, meth, params): r = requests.post( self._url, headers={"Content-Type": "application/json", "Host": "localhost"}, json={ "jsonrpc": "2.0", "id": "0", "method": "{}".format(meth), "params": params, }, ) return r.json() def update_data(self): """update data from lokinet""" try: j = self.jsonrpc("llarp.admin.dumpstate", {}) self.data = j["result"] except Exception as ex: self.data = None def _render_path(self, y, path, name): """render a path at current position""" self.win.move(y, 1) self.win.addstr("({}) ".format(name)) y += 1 self.win.move(y, 1) y += 1 self.win.addstr("[tx:\t{}]\t[rx:\t{}]".format(self.speedOf(path['txRateCurrent']), self.speedOf(path['rxRateCurrent']))) self.win.move(y, 1) y += 1 self.win.addstr("me -> ") for hop in path["hops"]: self.win.addstr(" {} ->".format(hop["router"][:4])) self.win.addstr(" [{} ms latency]".format(path["intro"]["latency"])) self.win.addstr(" [{} until expire]".format(self.timeTo(path["expiresAt"]))) if path["expiresSoon"]: self.win.addstr("(expiring)") elif path["expired"]: self.win.addstr("(expired)") return y def timeTo(self, ts): """ return time until timestamp in seconds formatted""" now = time.time() * 1000 return "{} seconds".format(int((ts - now) / 1000)) def speedOf(self, rate): """turn int speed into string formatted""" units = ["B", "KB", "MB", "GB"] idx = 0 while rate > 1000 and idx < len(units): rate /= 1000.0 idx += 1 return "{} {}ps".format("%.2f" % rate, units[idx]) def get_all_paths(self): for k in self.data['services']: status = self.data['services'][k] for path in (status['paths'] or []): yield path for s in (status['remoteSessions'] or []): for path in s['paths']: yield path for s in (status['snodeSessions'] or []): for path in s['paths']: yield path def display_service(self, y, name, status): """display a service at current position""" self.win.move(y, 1) self.win.addstr("service [{}]".format(name)) build = status["buildStats"] ratio = build["success"] / (build["attempts"] or 1) y += 1 self.win.move(y, 1) self.win.addstr("build success: {} %".format(int(100 * ratio))) y += 1 self.win.move(y, 1) paths = status["paths"] self.win.addstr("paths: {}".format(len(paths))) for path in paths: y = self._render_path(y, path, "inbound") for session in (status["remoteSessions"] or []): for path in session["paths"]: y = self._render_path( y, path, "[active] {}".format(session["currentConvoTag"]) ) for session in (status["snodeSessions"] or []): for path in session["paths"]: y = self._render_path(y, path, "[snode]") return y # for k in status: # self.win.move(y + 1, 1) # y += 1 # self.win.addstr('{}: {}'.format(k, json.dumps(status[k]))) def display_links(self, y, data): self.txrate = 0 self.rxrate = 0 for link in data["outbound"]: y += 1 self.win.move(y, 1) self.win.addstr("outbound sessions:") y = self.display_link(y, link) for link in data["inbound"]: y += 1 self.win.move(y, 1) self.win.addstr("inbound sessions:") y = self.display_link(y, link) y += 2 self.win.move(y, 1) self.win.addstr( "throughput:\t\t[{}\ttx]\t[{}\trx]".format( self.speedOf(self.txrate), self.speedOf(self.rxrate) ) ) bloat_tx, bloat_rx = self.calculate_bloat(self.data['links']['outbound'], self.get_all_paths()) y += 1 self.win.move(y, 1) self.win.addstr("goodput:\t\t[{}\ttx]\t[{}\trx]".format(self.speedOf(self.txrate-bloat_tx), self.speedOf(self.rxrate-bloat_rx))) y += 1 self.win.move(y, 1) self.win.addstr("overhead:\t\t[{}\ttx]\t[{}\trx]".format(self.speedOf(bloat_tx), self.speedOf(bloat_rx))) self._globalspeed.append((self.txrate, self.rxrate, bloat_tx, bloat_rx)) while len(self._globalspeed) > self._speedSamples: self._globalspeed.pop(0) return self.display_speedgraph(y + 2, self._globalspeed) def display_speedgraph(self, y, samps, maxsz=40): """ display global speed graph """ def scale(x, n): while n > 0: x /= 2 n -= 1 return int(x) txmax, rxmax = 1024, 1024 for tx, rx, _tx, _rx in samps: if tx > txmax: txmax = tx if rx > rxmax: rxmax = rx rxscale = 0 while rxmax > maxsz: rxscale += 1 rxmax /= 2 txscale = 0 while txmax > maxsz: txscale += 1 txmax /= 2 def makebar(samp, badsamp, max): bar = "#" * (samp - badsamp) pad = " " * (max - samp) return pad, bar, '#' * badsamp txlabelpad = int(txmax / 2)# - 1 rxlabelpad = int(rxmax / 2)# - 1 if txlabelpad <= 0: txlabelpad = 1 if rxlabelpad <= 0: rxlabelpad = 1 txlabelpad = " " * txlabelpad rxlabelpad = " " * rxlabelpad y += 1 self.win.move(y, 1) self.win.addstr( "{}tx{}{}rx{}".format(txlabelpad, txlabelpad, rxlabelpad, rxlabelpad) ) for tx, rx, btx, brx in samps: y += 1 self.win.move(y, 1) txpad, txbar, btxbar = makebar(scale(tx,txscale),scale(btx,txscale), int(txmax)) rxpad, rxbar, brxbar = makebar(scale(rx,rxscale),scale(brx,rxscale), int(rxmax)) self.win.addstr(txpad) self.win.addstr(btxbar, curses.color_pair(1)) self.win.addstr(txbar) self.win.addstr('|') self.win.addstr(rxbar) self.win.addstr(brxbar, curses.color_pair(1)) self.win.addstr(rxpad) return y + 2 def calculate_bloat(self, links, paths): """ calculate bandwith overhead """ lltx = 0 llrx = 0 tx = 0 rx = 0 for link in links: sessions = link["sessions"]["established"] for s in sessions: lltx += s['tx'] llrx += s['rx'] for path in paths: tx += path['txRateCurrent'] rx += path['rxRateCurrent'] if lltx > tx: lltx -= tx if llrx > rx: llrx -= rx return lltx, llrx def display_link(self, y, link): y += 1 self.win.move(y, 1) sessions = link["sessions"]["established"] for s in sessions: y = self.display_link_session(y, s) return y def display_link_session(self, y, s): y += 1 self.win.move(y, 1) self.txrate += s["txRateCurrent"] self.rxrate += s["rxRateCurrent"] self.win.addstr( "{}\t[{}\ttx]\t[{}\trx]".format( s["remoteAddr"], self.speedOf(s["txRateCurrent"]), self.speedOf(s["rxRateCurrent"]) ) ) if (s['txMsgQueueSize'] or 0) > 1: self.win.addstr(" [out window: {}]".format(s['txMsgQueueSize'])) if (s['rxMsgQueueSize'] or 0) > 1: self.win.addstr(" [in window: {}]".format(s['rxMsgQueueSize'])) def display(acks, label, num='acks', dem='packets'): if acks[dem] > 0: self.win.addstr(" [{}: {}]".format(label, round(float(acks[num]) / float(acks[dem]), 2))) if ('recvMACKs' in s) and ('sendMACKs' in s): display(s['sendMACKs'], 'out MACK density') display(s['recvMACKs'], 'in MACK density') d = {'recvAcks': 'in acks', 'sendAcks': 'out acks', 'recvRTX': 'in RTX', 'sendRTX': 'out RTX'} for k in d: v = d[k] if (k in s) and (s[k] > 0): self.win.addstr(" [{}: {}]".format(v, s[k])) return y def display_dht(self, y, data): y += 2 self.win.move(y, 1) self.win.addstr("DHT:") y += 1 self.win.move(y, 1) self.win.addstr("introset lookups") y = self.display_bucket(y, data["pendingIntrosetLookups"]) y += 1 self.win.move(y, 1) self.win.addstr("router lookups") return self.display_bucket(y, data["pendingRouterLookups"]) def display_bucket(self, y, data): txs = data["tx"] self.win.addstr(" ({} lookups)".format(len(txs))) for tx in txs: y += 1 self.win.move(y, 1) self.win.addstr("search for {}".format(tx["tx"]["target"])) return y def display_data(self): """draw main window""" if self.data is not None: self.win.addstr(1, 1, "lokinet online") # print(self.data) services = self.data["services"] or {} y = 3 try: y = self.display_links(y, self.data["links"]) for k in services: y = self.display_service(y, k, services[k]) y = self.display_dht(y, self.data["dht"]) except Exception as exc: pass else: self.win.move(1, 1) self.win.addstr("lokinet offline") def run(self): while True: self.win.clear() self.win.box() self.update_data() self.display_data() self.win.refresh() time.sleep(1) if __name__ == "__main__": import sys mon = Monitor( "http://{}/jsonrpc".format( len(sys.argv) > 1 and sys.argv[1] or "127.0.0.1:1190" ) ) mon.run()