From 849be91286cba8f7c12d0b0d6c272c549a7f78ea Mon Sep 17 00:00:00 2001 From: Sietse Snel Date: Wed, 16 Oct 2024 08:28:28 +0200 Subject: [PATCH] YDA-5909: add request data to tech support info Add information about requests being handled to the tech support info that is collected by the monitor thread. --- app.py | 35 ++++++++++++++++++++++++++++++----- monitor.py | 11 ++++++++++- unit-tests/test_monitor.py | 2 +- user/user.py | 2 ++ 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index e27dddb6..a2154adc 100644 --- a/app.py +++ b/app.py @@ -4,10 +4,11 @@ __license__ = 'GPLv3, see LICENSE' import json +import threading from os import path from typing import Any, Dict, Optional -from flask import Flask, g, redirect, request, Response, send_from_directory, url_for +from flask import Flask, g, redirect, request, Response, send_from_directory, session, url_for from flask_session import Session from flask_wtf.csrf import CSRFProtect @@ -128,10 +129,12 @@ def load_admin_setting() -> Dict[str, Any]: Session(app) # Start monitoring thread for extracting tech support information -with app.app_context(): - # Monitor signal file can be set to empty to completely disable monitor thread. - if app.config.get("MONITOR_SIGNAL_FILE", "/var/www/yoda/show-tech.sig") != "": - monitor_thread = Monitor(app.config) +# Monitor signal file can be set to empty to completely disable monitor thread +monitor_enabled: bool = app.config.get("MONITOR_SIGNAL_FILE", "/var/www/yoda/show-tech.sig") != "" +monitor_data: Dict[int, Dict[str, Any]] = {} +if monitor_enabled: + with app.app_context(): + monitor_thread: Monitor = Monitor(app.config, monitor_data) monitor_thread.start() # Register blueprints @@ -203,6 +206,28 @@ def protect_pages() -> Optional[Response]: return redirect(url_for('user_bp.gate', redirect_target=request.full_path)) +@app.before_request +def add_monitor_data() -> None: + if monitor_enabled: + monitor_data[threading.get_ident()] = {"Username": session.get("login_username", None), + "Login time": session.get("login_time", None), + "Request method": request.method, + "Request endpoint": request.endpoint, + "Request arguments": request.args} + + +@app.after_request +def cleanup_monitor_data(response: Response) -> Response: + if monitor_enabled: + try: + thread_id: int = threading.get_ident() + monitor_data.pop(thread_id) + except KeyError: + # No need to do anything if thread monitor data is not present + pass + return response + + @app.after_request def add_security_headers(response: Response) -> Response: """Add security headers.""" diff --git a/monitor.py b/monitor.py index b5e83903..89fb9c27 100644 --- a/monitor.py +++ b/monitor.py @@ -13,6 +13,7 @@ from datetime import datetime from io import StringIO from threading import Timer +from typing import Any, Dict import flask import humanize @@ -21,9 +22,10 @@ class Monitor(Timer): - def __init__(self, config: flask.config.Config): + def __init__(self, config: flask.config.Config, monitor_data: Dict[int, Dict[str, Any]]): self.interval = 1 self.config = config + self.monitor_data = monitor_data Timer.__init__(self, self.interval, self.record_info_if_needed) def get_signal_file(self) -> str: @@ -78,6 +80,13 @@ def get_tshoot_info(self) -> StringIO: for thread_id, stack in sys._current_frames().items(): output.write(f"Thread ID: {thread_id}\n") + + thread_monitor_data = self.monitor_data.get(thread_id, {}) + for monitor_variable in thread_monitor_data: + monitor_value = str(thread_monitor_data.get(monitor_variable)) + output.write(f"{monitor_variable}: {monitor_value}\n") + + output.write("Traceback:\n") for filename, line_number, function_name, line in traceback.extract_stack(stack): output.write(f" {filename}:{line_number} [{function_name}]\n") output.write(f" {line}\n" if line else "") diff --git a/unit-tests/test_monitor.py b/unit-tests/test_monitor.py index a71303c8..560be452 100644 --- a/unit-tests/test_monitor.py +++ b/unit-tests/test_monitor.py @@ -15,7 +15,7 @@ class MonitorTest(TestCase): def test_can_get_tshoot_info(self) -> None: config = {"YODA_VERSION": "test_version"} - monitor = Monitor(config) + monitor = Monitor(config, {}) tshoot_info = monitor.get_tshoot_info().getvalue() self.assertIn("test_version", tshoot_info) self.assertIn("Thread ID", tshoot_info) diff --git a/user/user.py b/user/user.py index e57280ac..5036a66a 100644 --- a/user/user.py +++ b/user/user.py @@ -5,6 +5,7 @@ import json import secrets +from datetime import datetime from typing import List from uuid import uuid4 @@ -91,6 +92,7 @@ def login() -> Response: return render_template('user/login.html', login_placeholder=get_login_placeholder()) session['login_username'] = username + session['login_time'] = datetime.now() g.login_username = username # Check if someone isn't trying to sneak past OIDC login.