diff --git a/aprsd/flask.py b/aprsd/flask.py index 017b6eb..e8fc0aa 100644 --- a/aprsd/flask.py +++ b/aprsd/flask.py @@ -486,6 +486,64 @@ class SendMessageNamespace(Namespace): LOG.debug(f"WS json {data}") +class LogMonitorThread(threads.APRSDThread): + + def __init__(self): + super().__init__("LogMonitorThread") + + def loop(self): + global socketio + try: + record = threads.logging_queue.get(block=True, timeout=5) + json_record = self.json_record(record) + socketio.emit( + "log_entry", json_record, + namespace="/logs", + ) + except Exception: + # Just ignore thi + pass + + return True + + def json_record(self, record): + entry = {} + entry["filename"] = record.filename + entry["funcName"] = record.funcName + entry["levelname"] = record.levelname + entry["lineno"] = record.lineno + entry["module"] = record.module + entry["name"] = record.name + entry["pathname"] = record.pathname + entry["process"] = record.process + entry["processName"] = record.processName + if hasattr(record, "stack_info"): + entry["stack_info"] = record.stack_info + else: + entry["stack_info"] = None + entry["thread"] = record.thread + entry["threadName"] = record.threadName + entry["message"] = record.getMessage() + return entry + + +class LoggingNamespace(Namespace): + + def on_connect(self): + global socketio + LOG.debug("Web socket connected") + socketio.emit( + "connected", {"data": "/logs Connected"}, + namespace="/logs", + ) + self.log_thread = LogMonitorThread() + self.log_thread.start() + + def on_disconnect(self): + LOG.debug("WS Disconnected") + self.log_thread.stop() + + def setup_logging(config, flask_app, loglevel, quiet): flask_log = logging.getLogger("werkzeug") @@ -548,4 +606,5 @@ def init_flask(config, loglevel, quiet): # eventlet.monkey_patch() socketio.on_namespace(SendMessageNamespace("/sendmsg", config=config)) + socketio.on_namespace(LoggingNamespace("/logs")) return socketio, flask_app diff --git a/aprsd/main.py b/aprsd/main.py index 99cf0c9..ca47e4b 100644 --- a/aprsd/main.py +++ b/aprsd/main.py @@ -195,6 +195,20 @@ def setup_logging(config, loglevel, quiet): imap_logger.setLevel(log_level) imap_logger.addHandler(fh) + if ( + utils.check_config_option( + config, ["aprsd", "web", "enabled"], + default_fail=False, + ) + ): + qh = logging.handlers.QueueHandler(threads.logging_queue) + q_log_formatter = logging.Formatter( + fmt=utils.QUEUE_LOG_FORMAT, + datefmt=utils.QUEUE_DATE_FORMAT, + ) + qh.setFormatter(q_log_formatter) + LOG.addHandler(qh) + if not quiet: sh = logging.StreamHandler(sys.stdout) sh.setFormatter(log_formatter) @@ -506,10 +520,7 @@ def server( keepalive = threads.KeepAliveThread(config=config) keepalive.start() - try: - web_enabled = utils.check_config_option(config, ["aprsd", "web", "enabled"]) - except Exception: - web_enabled = False + web_enabled = utils.check_config_option(config, ["aprsd", "web", "enabled"], default_fail=False) if web_enabled: flask_enabled = True diff --git a/aprsd/threads.py b/aprsd/threads.py index 8592aa5..e682e89 100644 --- a/aprsd/threads.py +++ b/aprsd/threads.py @@ -17,6 +17,7 @@ RX_THREAD = "RX" EMAIL_THREAD = "Email" rx_msg_queue = queue.Queue(maxsize=20) +logging_queue = queue.Queue(maxsize=50) msg_queues = { "rx": rx_msg_queue, } diff --git a/aprsd/utils.py b/aprsd/utils.py index e2acd2b..1f55e06 100644 --- a/aprsd/utils.py +++ b/aprsd/utils.py @@ -26,12 +26,17 @@ LOG_LEVELS = { "DEBUG": logging.DEBUG, } +DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p" DEFAULT_LOG_FORMAT = ( - "[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]" + "[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]" " %(message)s - [%(pathname)s:%(lineno)d]" ) -DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p" +QUEUE_DATE_FORMAT = "[%m/%d/%Y] [%I:%M:%S %p]" +QUEUE_LOG_FORMAT = ( + "%(asctime)s [%(threadName)-20.20s] [%(levelname)-5.5s]" + " %(message)s - [%(pathname)s:%(lineno)d]" +) # an example of what should be in the ~/.aprsd/config.yml DEFAULT_CONFIG_DICT = { @@ -288,7 +293,7 @@ def conf_option_exists(conf, chain): def check_config_option(config, chain, default_fail=None): result = conf_option_exists(config, chain.copy()) - if not result: + if result is None: raise Exception( "'{}' was not in config file".format( chain, diff --git a/aprsd/web/static/css/prism.css b/aprsd/web/static/css/prism.css new file mode 100644 index 0000000..8511262 --- /dev/null +++ b/aprsd/web/static/css/prism.css @@ -0,0 +1,189 @@ +/* PrismJS 1.24.1 +https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+log&plugins=show-language+toolbar */ +/** + * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML + * Based on https://github.com/chriskempson/tomorrow-theme + * @author Rose Pritchard + */ + +code[class*="language-"], +pre[class*="language-"] { + color: #ccc; + background: none; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 1em; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; + +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background: #2d2d2d; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.comment, +.token.block-comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #999; +} + +.token.punctuation { + color: #ccc; +} + +.token.tag, +.token.attr-name, +.token.namespace, +.token.deleted { + color: #e2777a; +} + +.token.function-name { + color: #6196cc; +} + +.token.boolean, +.token.number, +.token.function { + color: #f08d49; +} + +.token.property, +.token.class-name, +.token.constant, +.token.symbol { + color: #f8c555; +} + +.token.selector, +.token.important, +.token.atrule, +.token.keyword, +.token.builtin { + color: #cc99cd; +} + +.token.string, +.token.char, +.token.attr-value, +.token.regex, +.token.variable { + color: #7ec699; +} + +.token.operator, +.token.entity, +.token.url { + color: #67cdcc; +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +.token.inserted { + color: green; +} + +div.code-toolbar { + position: relative; +} + +div.code-toolbar > .toolbar { + position: absolute; + top: .3em; + right: .2em; + transition: opacity 0.3s ease-in-out; + opacity: 0; +} + +div.code-toolbar:hover > .toolbar { + opacity: 1; +} + +/* Separate line b/c rules are thrown out if selector is invalid. + IE11 and old Edge versions don't support :focus-within. */ +div.code-toolbar:focus-within > .toolbar { + opacity: 1; +} + +div.code-toolbar > .toolbar > .toolbar-item { + display: inline-block; +} + +div.code-toolbar > .toolbar > .toolbar-item > a { + cursor: pointer; +} + +div.code-toolbar > .toolbar > .toolbar-item > button { + background: none; + border: 0; + color: inherit; + font: inherit; + line-height: normal; + overflow: visible; + padding: 0; + -webkit-user-select: none; /* for button */ + -moz-user-select: none; + -ms-user-select: none; +} + +div.code-toolbar > .toolbar > .toolbar-item > a, +div.code-toolbar > .toolbar > .toolbar-item > button, +div.code-toolbar > .toolbar > .toolbar-item > span { + color: #bbb; + font-size: .8em; + padding: 0 .5em; + background: #f5f2f0; + background: rgba(224, 224, 224, 0.2); + box-shadow: 0 2px 0 0 rgba(0,0,0,0.2); + border-radius: .5em; +} + +div.code-toolbar > .toolbar > .toolbar-item > a:hover, +div.code-toolbar > .toolbar > .toolbar-item > a:focus, +div.code-toolbar > .toolbar > .toolbar-item > button:hover, +div.code-toolbar > .toolbar > .toolbar-item > button:focus, +div.code-toolbar > .toolbar > .toolbar-item > span:hover, +div.code-toolbar > .toolbar > .toolbar-item > span:focus { + color: inherit; + text-decoration: none; +} diff --git a/aprsd/web/static/js/logs.js b/aprsd/web/static/js/logs.js new file mode 100644 index 0000000..f85d292 --- /dev/null +++ b/aprsd/web/static/js/logs.js @@ -0,0 +1,26 @@ +function init_logs() { + const socket = io("/logs"); + socket.on('connect', function () { + console.log("Connected to logs socketio"); + }); + + socket.on('connected', function(msg) { + console.log("Connected to /logs"); + console.log(msg); + }); + + socket.on('log_entry', function(data) { + update_logs(data); + }); + +}; + + +function update_logs(data) { + var code_block = $('#logtext') + entry = data["message"] + const html_pretty = Prism.highlight(entry, Prism.languages.log, 'log'); + code_block.append(html_pretty + "
"); + var div = document.getElementById('logContainer'); + div.scrollTop = div.scrollHeight; +} diff --git a/aprsd/web/static/js/prism.js b/aprsd/web/static/js/prism.js new file mode 100644 index 0000000..f232b27 --- /dev/null +++ b/aprsd/web/static/js/prism.js @@ -0,0 +1,2247 @@ +/* PrismJS 1.24.1 +https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+json+json5+log&plugins=show-language+toolbar */ +/// + +var _self = (typeof window !== 'undefined') + ? window // if in browser + : ( + (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) + ? self // if in worker + : {} // if in node js + ); + +/** + * Prism: Lightweight, robust, elegant syntax highlighting + * + * @license MIT + * @author Lea Verou + * @namespace + * @public + */ +var Prism = (function (_self) { + + // Private helper vars + var lang = /\blang(?:uage)?-([\w-]+)\b/i; + var uniqueId = 0; + + // The grammar object for plaintext + var plainTextGrammar = {}; + + + var _ = { + /** + * By default, Prism will attempt to highlight all code elements (by calling {@link Prism.highlightAll}) on the + * current page after the page finished loading. This might be a problem if e.g. you wanted to asynchronously load + * additional languages or plugins yourself. + * + * By setting this value to `true`, Prism will not automatically highlight all code elements on the page. + * + * You obviously have to change this value before the automatic highlighting started. To do this, you can add an + * empty Prism object into the global scope before loading the Prism script like this: + * + * ```js + * window.Prism = window.Prism || {}; + * Prism.manual = true; + * // add a new - - - - @@ -16,14 +12,17 @@ + + + -