diff --git a/.vscode/settings.json b/.vscode/settings.json index 36756514..d0719a38 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,8 @@ "dlfcn.h": "c", "utils.h": "c", "plc_state_manager.h": "c", - "unix_socket.h": "c" + "unix_socket.h": "c", + "plugin_driver.h": "c" }, "editor.rulers": [ 80 diff --git a/webserver/app.py b/webserver/app.py index e702364b..e532b24e 100644 --- a/webserver/app.py +++ b/webserver/app.py @@ -1,4 +1,3 @@ -import logging import os import ssl from pathlib import Path @@ -28,12 +27,15 @@ MAX_FILE_SIZE ) +# from logger import get_logger, LogParser + + app = flask.Flask(__name__) app.secret_key = str(os.urandom(16)) login_manager = flask_login.LoginManager() login_manager.init_app(app) -logger = logging.getLogger(__name__) +# logger = get_logger(use_buffer=True) runtime_manager = RuntimeManager( runtime_path="./build/plc_main", @@ -97,7 +99,7 @@ def restapi_callback_get(argument: str, data: dict) -> dict: """ Dispatch GET callbacks by argument. """ - logger.debug("GET | Received argument: %s, data: %s", argument, data) + # logger.debug("GET | Received argument: %s, data: %s", argument, data) handler = GET_HANDLERS.get(argument) if handler: return handler(data) @@ -166,7 +168,7 @@ def restapi_callback_post(argument: str, data: dict) -> dict: """ Dispatch POST callbacks by argument. """ - logger.debug("POST | Received argument: %s, data: %s", argument, data) + # logger.debug("POST | Received argument: %s, data: %s", argument, data) handler = POST_HANDLERS.get(argument) if not handler: @@ -184,9 +186,10 @@ def run_https(): try: db.create_all() db.session.commit() - logger.info("Database tables created successfully.") + # logger.info("Database tables created successfully.") except Exception as e: - logger.error("Error creating database tables: %s", e) + # logger.error("Error creating database tables: %s", e) + pass try: cert_gen = CertGen(hostname=HOSTNAME, ip_addresses=["127.0.0.1"]) @@ -208,14 +211,17 @@ def run_https(): ) except FileNotFoundError as e: - logger.error("Could not find SSL credentials! %s", e) + # logger.error("Could not find SSL credentials! %s", e) + pass except ssl.SSLError as e: - logger.error("SSL credentials FAIL! %s", e) + # logger.error("SSL credentials FAIL! %s", e) + pass except KeyboardInterrupt: - logger.info("HTTP server stopped by KeyboardInterrupt") + # logger.info("HTTP server stopped by KeyboardInterrupt") + pass finally: runtime_manager.stop() - logger.info("Runtime manager stopped") + # logger.info("Runtime manager stopped") if __name__ == "__main__": diff --git a/webserver/config.py b/webserver/config.py index 39e9eea8..de2dfaf3 100644 --- a/webserver/config.py +++ b/webserver/config.py @@ -1,4 +1,3 @@ -import logging import os import re import secrets @@ -11,12 +10,7 @@ DB_PATH = Path(__file__).resolve().parent.parent / "restapi.db" BASE_DIR = os.path.abspath(os.path.dirname(__file__)) -logger = logging.getLogger(__name__) -logging.basicConfig( - level=logging.DEBUG, # Minimum level to capture - format="[%(levelname)s] %(asctime)s - %(message)s", - datefmt="%H:%M:%S", -) +# logger = logging.getLogger("logger") # Function to validate environment variable values diff --git a/webserver/credentials.py b/webserver/credentials.py index 64c75318..2c6ef43b 100644 --- a/webserver/credentials.py +++ b/webserver/credentials.py @@ -1,7 +1,6 @@ import datetime import ipaddress import os -import logging from cryptography import x509 from cryptography.hazmat.backends import default_backend @@ -9,7 +8,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID -logger = logging.getLogger(__name__) +# logger = logging.getLogger("logger") class CertGen: @@ -40,7 +39,7 @@ def generate_key(self): ) def generate_self_signed_cert(self, cert_file, key_file): - logger.debug("Generating self-signed certificate for %s...", self.hostname) + # logger.debug("Generating self-signed certificate for %s...", self.hostname) self.generate_key() @@ -72,13 +71,13 @@ def generate_self_signed_cert(self, cert_file, key_file): serialization.NoEncryption(), ) ) - logger.debug("Certificate saved to %s", cert_file) - logger.debug("Private key saved to %s", key_file) + # logger.debug("Certificate saved to %s", cert_file) + # logger.debug("Private key saved to %s", key_file) def is_certificate_valid(self, cert_file): """Check if the certificate is valid.""" if not os.path.exists(cert_file): - logger.warning("Certificate file not found: %s", cert_file) + # logger.warning("Certificate file not found: %s", cert_file) return False try: @@ -90,15 +89,15 @@ def is_certificate_valid(self, cert_file): now = datetime.datetime.now(datetime.timezone.utc) if now < cert.not_valid_before_utc: - logger.warning("Certificate is not yet valid. Valid from: %s", cert.not_valid_before_utc) + # logger.warning("Certificate is not yet valid. Valid from: %s", cert.not_valid_before_utc) return False if now > cert.not_valid_after_utc: - logger.warning("Certificate has expired. Expired on: %s", cert.not_valid_after_utc) + # logger.warning("Certificate has expired. Expired on: %s", cert.not_valid_after_utc) return False - logger.info("Certificate is valid. Expires on: %s", cert.not_valid_after_utc) + # logger.info("Certificate is valid. Expires on: %s", cert.not_valid_after_utc) return True except Exception as e: - logger.error("Error loading or parsing certificate: %s", e) + # logger.error("Error loading or parsing certificate: %s", e) return False diff --git a/webserver/__init__.py b/webserver/logger/__init__.py similarity index 81% rename from webserver/__init__.py rename to webserver/logger/__init__.py index f073344b..70cba6d4 100644 --- a/webserver/__init__.py +++ b/webserver/logger/__init__.py @@ -1,6 +1,10 @@ import logging import logging.config +from .logger import get_logger +from .parser import LogParser +from .bufferhandler import BufferHandler +__all__ = ["get_logger", "LogParser", "BufferHandler"] __version__ = "0.1" __author__ = "Autonomy" __license__ = "MIT" diff --git a/webserver/logger/bufferhandler.py b/webserver/logger/bufferhandler.py new file mode 100644 index 00000000..a093b0a8 --- /dev/null +++ b/webserver/logger/bufferhandler.py @@ -0,0 +1,64 @@ +import logging +from collections import deque +from typing import List, Optional +import json +import re +from datetime import datetime + + +class BufferHandler(logging.Handler): + """ + Custom logging handler that stores log records in memory (FIFO). + Logs are formatted using the attached formatter (JSON). + """ + + def __init__(self, capacity: int = 1000): + super().__init__() + self.buffer = deque(maxlen=capacity) + + def emit(self, record: logging.LogRecord) -> None: + try: + self.buffer.append(self.format(record)) + except Exception: + self.handleError(record) + + def get_logs(self, count: Optional[int] = None) -> List[str]: + """Retrieve logs from buffer.""" + if count is None or count > len(self.buffer): + return list(self.buffer) + return list(self.buffer)[-count:] + + def normalize_buffer_logs(self, buffer_records): + """ + Takes a list of log strings from buffer and returns a list of clean JSON dicts. + """ + result = [] + json_extract = re.compile(r'(\{.*\})') # match JSON inside log line + + for record in buffer_records: + match = json_extract.search(record) + if not match: + continue + + try: + raw_json = json.loads(match.group(1)) + # Convert unix timestamp → readable datetime + ts = int(raw_json.get("timestamp", 0)) + dt = datetime.utcfromtimestamp(ts).isoformat() + "Z" + + entry = { + "timestamp": dt, + "level": raw_json.get("level", "INFO"), + "message": raw_json.get("message", "") + } + result.append(entry) + except (json.JSONDecodeError, ValueError): + continue + + return result + + def clear(self) -> None: + self.buffer.clear() + + def __len__(self): + return len(self.buffer) diff --git a/webserver/logger/formatter.py b/webserver/logger/formatter.py new file mode 100644 index 00000000..5096fbcb --- /dev/null +++ b/webserver/logger/formatter.py @@ -0,0 +1,19 @@ +import logging +import time +import json + +class JsonFormatter(logging.Formatter): + """Format log records as JSON strings.""" + + def format(self, record: logging.LogRecord) -> str: + log_dict = { + "timestamp": str(int(record.created)), # epoch seconds + "level": record.levelname, + "message": record.getMessage() + } + + # Include optional fields if present + if hasattr(record, "source"): + log_dict["source"] = record.source + + return json.dumps(log_dict, ensure_ascii=False) diff --git a/webserver/logger/logger.py b/webserver/logger/logger.py new file mode 100644 index 00000000..93b4a25e --- /dev/null +++ b/webserver/logger/logger.py @@ -0,0 +1,38 @@ +import logging +from .formatter import JsonFormatter +from .bufferhandler import BufferHandler + + +def get_logger(name: str = "logger", + level: int = logging.INFO, + use_buffer: bool = False): + """Return a logger instance with custom formatting.""" + + collector_logger = logging.getLogger(name) + collector_logger.setLevel(logging.DEBUG) + + handler = logging.StreamHandler() + handler.setFormatter(JsonFormatter()) + collector_logger.addHandler(handler) + + buffer_handler = None + + if use_buffer: + # Use buffer handler for log messages + buffer_handler = BufferHandler() + buffer_handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) + collector_logger.addHandler(buffer_handler) + + if use_buffer: + # Find buffer handler again if it already exists + if buffer_handler is None: + for h in collector_logger.handlers: + if isinstance(h, BufferHandler): + buffer_handler = h + break + return collector_logger, buffer_handler + else: + return collector_logger, None + + # return collector_logger diff --git a/webserver/logger/parser.py b/webserver/logger/parser.py new file mode 100644 index 00000000..05bcda62 --- /dev/null +++ b/webserver/logger/parser.py @@ -0,0 +1,74 @@ +# logger/parser.py +import logging +import re +import time +import json + +LOG_PATTERN = re.compile(r'^\[(?P\w+)\]\s*(?P.*)$') + +LEVEL_MAP = { + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL, +} + + +class LogParser: + def __init__(self, collector_logger: logging.Logger): + self.collector_logger = collector_logger + + def parse_and_log(self, line: str): + """Parse incoming log line and re-log it in normalized JSON format.""" + sline = line.strip() + if not sline: + return + + timestamp = int(time.time()) + level_name = "INFO" + level = logging.INFO + message = sline + + # Case 1: JSON log already + try: + parsed = json.loads(sline) + if isinstance(parsed, dict) and "message" in parsed: + # Preserve incoming JSON fields, but ensure timestamp is present + parsed.setdefault("timestamp", str(timestamp)) + level_name = parsed.get("level", "INFO") + level = LEVEL_MAP.get(level_name, logging.INFO) + log_entry = parsed + else: + raise ValueError("Not a valid log JSON dict") + except (json.JSONDecodeError, ValueError): + # Case 2: Regex log like "[INFO] Something" + match = LOG_PATTERN.match(sline) + if match: + level_name = match["level"] + level = LEVEL_MAP.get(level_name, logging.INFO) + message = match["message"] + else: + message = sline + + log_entry = { + "timestamp": str(timestamp), + "level": level_name, + "message": message + } + + # Create final JSON string + json_log = json.dumps(log_entry, ensure_ascii=False) + + # Push into Python logging + record = self.collector_logger.makeRecord( + name="external", + level=level, + fn="", + lno=0, + msg=json_log, + args=(), + exc_info=None + ) + record.source = "external" + self.collector_logger.handle(record) diff --git a/webserver/plcapp_management.py b/webserver/plcapp_management.py index 49b87849..3905ee66 100644 --- a/webserver/plcapp_management.py +++ b/webserver/plcapp_management.py @@ -1,6 +1,5 @@ from dataclasses import dataclass, field from enum import Enum, auto -import logging import os import zipfile import subprocess @@ -9,11 +8,12 @@ from runtimemanager import RuntimeManager -logger = logging.getLogger(__name__) +# logger = logging.getLogger("logger") MAX_FILE_SIZE: Final[int] = 10 * 1024 * 1024 # 10 MB per file MAX_TOTAL_SIZE: Final[int] = 50 * 1024 * 1024 # 50 MB total DISALLOWED_EXT = (".exe", ".dll", ".sh", ".bat", ".js", ".vbs", ".scr") +ALLOWED_FILENAME = "create_standard_function_txt.sh" class BuildStatus(Enum): IDLE = auto() @@ -29,7 +29,7 @@ class BuildProcess: exit_code: int | None = None def log(self, msg: str): - logger.info(msg) + # logger.info(msg) self.logs.append(msg) def clear(self): @@ -62,38 +62,42 @@ def analyze_zip(zip_path) -> tuple[bool, list]: # Check for path traversal or absolute paths if filename.startswith("/") or ".." in filename or ":" in filename: - logger.warning("Dangerous path: %s", filename) + # logger.warning("Dangerous path: %s", filename) safe = False # Check uncompressed size if uncompressed_size > MAX_FILE_SIZE: - logger.warning("File too large: %s (%d bytes)", - filename, uncompressed_size) + # logger.warning("File too large: %s (%d bytes)", + # filename, uncompressed_size) safe = False # Check compression ratio (ZIP bomb detection) if compressed_size > 0 and uncompressed_size / compressed_size > 1000: - logger.warning("Suspicious compression ratio in %s", - filename) + # logger.warning("Suspicious compression ratio in %s", + # filename) safe = False # Check disallowed extensions - if ext in DISALLOWED_EXT: - logger.warning("Disallowed extension: %s", - filename) - safe = False + # TODO remove this additional BASH SCRIPT check + if ALLOWED_FILENAME not in filename: + if ext in DISALLOWED_EXT: + print("Disallowed extension: %s", + filename) + safe = False total_size += uncompressed_size valid_files.append(info) # Check total size if total_size > MAX_TOTAL_SIZE: - logger.warning("Total uncompressed size too large: %d bytes", - total_size) + # logger.warning("Total uncompressed size too large: %d bytes", + # total_size) safe = False - if safe == False: - logger.error("PLC Program file failed safety checks, aborting.") + # if safe: + # logger.info("ZIP file looks safe to extract (based on static checks).") + # else: + # logger.warning("ZIP file failed safety checks.") return safe, valid_files @@ -136,7 +140,7 @@ def safe_extract(zip_path, dest_dir, valid_files): # Ensure extraction stays inside destination if not out_path.startswith(os.path.abspath(dest_dir)): - logger.warning("Skipping suspicious path: %s", filename) + # logger.warning("Skipping suspicious path: %s", filename) continue os.makedirs(os.path.dirname(out_path), exist_ok=True) @@ -144,6 +148,8 @@ def safe_extract(zip_path, dest_dir, valid_files): with zf.open(info) as src, open(out_path, "wb") as dst: dst.write(src.read()) + # logger.info("Extracted: %s", out_path) + def run_compile(runtime_manager: RuntimeManager, cwd: str = "core/generated"): """Run compile script synchronously (wait for completion) and update status/logs.""" script_path: str = "./scripts/compile.sh" diff --git a/webserver/restapi.py b/webserver/restapi.py index a3b6d03c..0a6667b5 100644 --- a/webserver/restapi.py +++ b/webserver/restapi.py @@ -13,8 +13,9 @@ ) from flask_sqlalchemy import SQLAlchemy from werkzeug.security import check_password_hash, generate_password_hash +# from logger import get_logger, LogParser -logger = logging.getLogger(__name__) +# logger = get_logger(use_buffer=True) env = os.getenv("FLASK_ENV", "development") @@ -56,7 +57,7 @@ def set_password(self, password: str) -> str: self.password_hash = generate_password_hash( password, method=self.derivation_method ) - logger.debug("Password set for user %s | %s", self.username, self.password_hash) + # logger.debug("Password set for user %s | %s", self.username, self.password_hash) return self.password_hash def check_password(self, password: str) -> bool: @@ -81,13 +82,13 @@ def user_lookup_callback(_jwt_header, jwt_data): def register_callback_get(callback: Callable[[str, dict], dict]): global _handler_callback_get _handler_callback_get = callback - logger.info("GET Callback registered successfully for rest_blueprint!") + # logger.info("GET Callback registered successfully for rest_blueprint!") def register_callback_post(callback: Callable[[str, dict], dict]): global _handler_callback_post _handler_callback_post = callback - logger.info("POST Callback registered successfully for rest_blueprint!") + # logger.info("POST Callback registered successfully for rest_blueprint!") @restapi_bp.route("/create-user", methods=["POST"]) @@ -96,7 +97,7 @@ def create_user(): try: users_exist = User.query.first() is not None except Exception as e: - logger.error("Error checking for users: %s", e) + # logger.error("Error checking for users: %s", e) return jsonify({"msg": "User creation error"}), 401 # if there are no users, we don't need to verify JWT @@ -130,7 +131,7 @@ def get_user_info(user_id): try: user = User.query.get(user_id) except Exception as e: - logger.error("Error retrieving user: %s", e) + # logger.error("Error retrieving user: %s", e) return jsonify({"msg": "User retrieval error"}), 500 if not user: @@ -145,13 +146,13 @@ def get_users_info(): try: verify_jwt_in_request() except Exception: - logger.warning( - "No JWT token provided, checking for users without authentication" - ) + # logger.warning( + # "No JWT token provided, checking for users without authentication" + # ) try: users_exist = User.query.first() is not None except Exception as e: - logger.error("Error checking for users: %s", e) + # logger.error("Error checking for users: %s", e) return jsonify({"msg": "User retrieval error"}), 500 if not users_exist: @@ -161,7 +162,7 @@ def get_users_info(): try: users = User.query.all() except Exception as e: - logger.error("Error retrieving users: %s", e) + # logger.error("Error retrieving users: %s", e) return jsonify({"msg": "User retrieval error"}), 500 return jsonify([user.to_dict() for user in users]), 200 @@ -181,7 +182,7 @@ def change_password(user_id): try: user = User.query.get(user_id) except Exception as e: - logger.error("Error retrieving user: %s", e) + # logger.error("Error retrieving user: %s", e) return jsonify({"msg": "User retrieval error"}), 500 if not user: @@ -206,7 +207,7 @@ def delete_user(user_id): try: user = User.query.get(user_id) except Exception as e: - logger.error("Error retrieving user: %s", e) + # logger.error("Error retrieving user: %s", e) return jsonify({"msg": "User retrieval error"}), 500 if not user: @@ -226,9 +227,9 @@ def login(): try: user = User.query.filter_by(username=username).one_or_none() - logger.debug("User found: %s", user) + # logger.debug("User found: %s", user) except Exception as e: - logger.error("Error retrieving user: %s", e) + # logger.error("Error retrieving user: %s", e) return jsonify({"msg": "User retrieval error"}), 500 if not user or not user.check_password(password): @@ -252,7 +253,8 @@ def revoke_jwt(): # Add the JWT ID to the blacklist jwt_blacklist.add(jti) except Exception as e: - logger.error("Error revoking JWT: %s", e) + # logger.error("Error revoking JWT: %s", e) + pass @restapi_bp.route("/", methods=["GET"]) @@ -267,7 +269,7 @@ def restapi_plc_get(command): return jsonify(result), 200 except Exception as e: - logger.error("Error in restapi_plc_get: %s", e) + # logger.error("Error in restapi_plc_get: %s", e) return jsonify({"error": str(e)}), 500 @@ -283,5 +285,5 @@ def restapi_plc_post(command): result = _handler_callback_post(command, data) return jsonify(result), 200 except Exception as e: - logger.error("Error in restapi_plc_post: %s", e) + # logger.error("Error in restapi_plc_post: %s", e) return jsonify({"error": str(e)}), 500 diff --git a/webserver/runtimemanager.py b/webserver/runtimemanager.py index c74fc8c1..7bf6c42b 100644 --- a/webserver/runtimemanager.py +++ b/webserver/runtimemanager.py @@ -1,3 +1,4 @@ +import json import subprocess import socket import threading @@ -6,9 +7,10 @@ import psutil from unixserver import UnixLogServer from unixclient import SyncUnixClient -import logging +from logger import get_logger, LogParser + +logger, buffer = get_logger(use_buffer=True) -logger = logging.getLogger(__name__) class RuntimeManager: def __init__(self, runtime_path, plc_socket, log_socket): @@ -206,9 +208,13 @@ def get_logs(self): """ Get current logs from the runtime """ - return list(self.log_server.log_buffer) - - + try: + _logs = buffer.normalize_buffer_logs(buffer.get_logs()) + return _logs + except AttributeError as e: + logger.error("Failed to get logs from buffer: %s", e) + return [] + def ping(self): """ Send PING and wait for PONG diff --git a/webserver/test.py b/webserver/test.py new file mode 100644 index 00000000..e068ea90 --- /dev/null +++ b/webserver/test.py @@ -0,0 +1,19 @@ +from logger import get_logger +import worker + +# Create logger with buffer enabled +logger = get_logger(use_buffer=True) + +logger.info("Hello buffered logs!") +logger.warning("This is a warning") +logger.error("Error occurred") + +worker.do_work() + +# Retrieve buffer +for handler in logger.handlers: + if hasattr(handler, "get_logs"): + print("Buffered logs:", handler.get_logs()) + +print(dir(logger)) + diff --git a/webserver/unixclient.py b/webserver/unixclient.py index 1dffeca9..f1382e5c 100644 --- a/webserver/unixclient.py +++ b/webserver/unixclient.py @@ -1,11 +1,12 @@ import socket import os -import logging import re from typing import Optional from threading import Lock -logger = logging.getLogger(__name__) +# from logger import get_logger, LogParser + +# logger = get_logger(use_buffer=True) mutex = Lock() @@ -34,13 +35,13 @@ def connect(self): raise FileNotFoundError(f"Socket not found: {self.socket_path}") try: - logger.info("Connecting to socket %s", self.socket_path) + # logger.info("Connecting to socket %s", self.socket_path) self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.settimeout(1.0) # 1s timeout on blocking calls self.sock.connect(self.socket_path) - logger.info("Connected to server socket %s", self.socket_path) + # logger.info("Connected to server socket %s", self.socket_path) except Exception as e: - logger.error("Failed to connect: %s", e) + # logger.error("Failed to connect: %s", e) raise def send_message(self, msg: str): @@ -51,9 +52,9 @@ def send_message(self, msg: str): data = msg.encode() try: self.sock.sendall(data) - logger.info("Sent message: %s", data) + # logger.info("Sent message: %s", data) except Exception as e: - logger.error("Error sending message: %s", e) + # logger.error("Error sending message: %s", e) raise def recv_message(self, timeout: float = 0.5) -> Optional[str]: @@ -66,21 +67,21 @@ def recv_message(self, timeout: float = 0.5) -> Optional[str]: try: data = self.sock.recv(1024) if not data: - logger.warning("Connection closed by server") + # logger.warning("Connection closed by server") return None message = data.decode("utf-8").strip() - logger.info("Received message: %s", message) + # logger.info("Received message: %s", message) return message except socket.timeout: - logger.debug("Timeout waiting for message") + # logger.debug("Timeout waiting for message") return None except Exception as e: - logger.error("Error receiving message: %s", e) + # logger.error("Error receiving message: %s", e) return None def close(self): if self.sock: - logger.info("Closing connection") + # logger.info("Closing connection") try: self.sock.close() finally: diff --git a/webserver/unixserver.py b/webserver/unixserver.py index c7739214..c849025e 100644 --- a/webserver/unixserver.py +++ b/webserver/unixserver.py @@ -1,10 +1,12 @@ +import json import socket import threading -import collections -import logging import os +from logger import get_logger, LogParser + +logger, buffer = get_logger(use_buffer=True) +parser = LogParser(logger) -logger = logging.getLogger(__name__) class UnixLogServer: def __init__(self, socket_path="/run/runtime/log_runtime.socket"): @@ -13,7 +15,7 @@ def __init__(self, socket_path="/run/runtime/log_runtime.socket"): self.clients = [] self.lock = threading.Lock() self.running = False - self.log_buffer = collections.deque(maxlen=1000) + # self.parser = LogParser(logger) def start(self): """Start the Unix socket server""" @@ -35,8 +37,10 @@ def start(self): self.running = True threading.Thread(target=self._accept_clients, daemon=True).start() logger.info("Log server started at %s", self.socket_path) - except Exception as e: + except (OSError, socket.error) as e: logger.error("Failed to start server: %s", e) + except Exception as e: + logger.error("Failed to start server (unexpected): %s", e) raise def _accept_clients(self): @@ -48,15 +52,20 @@ def _accept_clients(self): self.clients.append(client_sock) threading.Thread(target=self._handle_client, args=(client_sock,), daemon=True).start() logger.info("Client connected") + except (OSError, socket.error) as e: + if self.running: + logger.error("Socket error: %s", e) except Exception as e: logger.error("Error accepting client: %s", e) - def _handle_client(self, client_sock): + def _handle_client(self, client_sock: socket.socket): """Handle communication with a connected client""" try: with client_sock.makefile('r') as f: for line in f: - self.log_buffer.append(line.strip()) + parser.parse_and_log(line) + except (OSError, socket.error) as e: + logger.error("Socket error: %s", e) except Exception as e: logger.error("Error handling client: %s", e) finally: diff --git a/webserver/worker.py b/webserver/worker.py new file mode 100644 index 00000000..7dbbdfd0 --- /dev/null +++ b/webserver/worker.py @@ -0,0 +1,8 @@ +from logger import get_logger + +# IMPORTANT: same logger name ("collector") → same logger instance +logger = get_logger(use_buffer=True) + +def do_work(): + logger.info("Worker is running") + logger.warning("Worker had a minor issue")