From 35b6ac3fbc5c0b5b989e4a7fe9937b2f68c4aafb Mon Sep 17 00:00:00 2001 From: Holger Sielaff Date: Mon, 29 Sep 2025 18:26:33 +0200 Subject: [PATCH] initial --- example_usage.py | 62 ++++++ .../zabbix_agentd.d/python-exceptions.conf | 59 ++++++ .../lib/python3.12/zabbix_monitor/__init__.py | 0 .../zabbix_exception_handler.py | 197 ++++++++++++++++++ .../zabbix_monitor/zabbix_monitor.py | 23 ++ .../zabbix_template_python_exceptions.yaml | 133 ++++++++++++ 6 files changed, 474 insertions(+) create mode 100644 example_usage.py create mode 100644 src/etc/zabbix/zabbix_agentd.d/python-exceptions.conf create mode 100644 src/usr/lib/python3.12/zabbix_monitor/__init__.py create mode 100644 src/usr/lib/python3.12/zabbix_monitor/zabbix_exception_handler.py create mode 100644 src/usr/lib/python3.12/zabbix_monitor/zabbix_monitor.py create mode 100644 src/usr/share/doc/zabbix-python-monitoring/zabbix_template_python_exceptions.yaml diff --git a/example_usage.py b/example_usage.py new file mode 100644 index 0000000..3b94684 --- /dev/null +++ b/example_usage.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Example usage of Zabbix Python Exception Handler + +This demonstrates how to use the Zabbix exception monitoring in your Python applications. +""" + +import sys +import os + +# Add the Zabbix module path +sys.path.insert(0, '/usr/lib/python3.12') + +# Method 1: Import the main handler (auto-initializes) +import zabbix_exception_handler + +# Method 2: Alternative simple import (also auto-initializes) +# import zabbix_monitor + +# Method 3: Manual initialization (if you want more control) +# from zabbix_exception_handler import ZabbixPythonExceptionHandler +# ZabbixPythonExceptionHandler.init() + + +def demonstrate_exception(): + """Function that will cause an exception for testing""" + print("About to cause an exception for demonstration...") + + # This will cause a ZeroDivisionError + result = 10 / 0 + return result + + +def demonstrate_file_error(): + """Function that will cause a file not found error""" + print("About to cause a file error for demonstration...") + + # This will cause a FileNotFoundError + with open('/nonexistent/file.txt', 'r') as f: + content = f.read() + return content + + +if __name__ == "__main__": + print("Zabbix Python Exception Handler Example") + print("=" * 50) + print() + + print("Exception handler is now active!") + print("Any unhandled exceptions will be logged to: /var/log/zabbix/python.exceptions.log") + print() + + # Choose which type of exception to demonstrate + choice = input("Choose exception type (1=ZeroDivision, 2=FileNotFound, 3=None): ") + + if choice == "1": + demonstrate_exception() + elif choice == "2": + demonstrate_file_error() + else: + print("No exception generated. Handler is still active for any future exceptions.") + print("Your application can continue normally...") \ No newline at end of file diff --git a/src/etc/zabbix/zabbix_agentd.d/python-exceptions.conf b/src/etc/zabbix/zabbix_agentd.d/python-exceptions.conf new file mode 100644 index 0000000..b9cb87b --- /dev/null +++ b/src/etc/zabbix/zabbix_agentd.d/python-exceptions.conf @@ -0,0 +1,59 @@ +# Zabbix Agent configuration for Python Exception Monitoring +# UserParameter ohne jq-Abhängigkeit für passive checks + +# Vollständigen Log-Inhalt lesen (für neue Einträge) +UserParameter=python.exception.log.content,cat /var/log/zabbix/python.exceptions.log 2>/dev/null || echo "" + +# Neueste Exception (letzte Zeile) +UserParameter=python.exception.latest,tail -n 1 /var/log/zabbix/python.exceptions.log 2>/dev/null || echo "" + +# Get last exception timestamp (mit jq) +UserParameter=python.exception.last,tail -n 1 /var/log/zabbix/python.exceptions.log 2>/dev/null | jq -r '.timestamp // "never"' 2>/dev/null || echo "never" + +# Get last exception message (mit jq) +UserParameter=python.exception.message,tail -n 1 /var/log/zabbix/python.exceptions.log 2>/dev/null | jq -r '.message // ""' 2>/dev/null || echo "" + +# Get last exception file (mit jq) +UserParameter=python.exception.file,tail -n 1 /var/log/zabbix/python.exceptions.log 2>/dev/null | jq -r '.file // ""' 2>/dev/null || echo "" + +# Get last exception line (mit jq) +UserParameter=python.exception.line,tail -n 1 /var/log/zabbix/python.exceptions.log 2>/dev/null | jq -r '.line // 0' 2>/dev/null || echo "0" + +# Get last exception unique_id (mit jq) +UserParameter=python.exception.unique_id,tail -n 1 /var/log/zabbix/python.exceptions.log 2>/dev/null | jq -r '.unique_id // ""' 2>/dev/null || echo "" + +# Get last exception class/type (Python-spezifisch) +UserParameter=python.exception.class,tail -n 1 /var/log/zabbix/python.exceptions.log 2>/dev/null | jq -r '.exception_class // ""' 2>/dev/null || echo "" + +# Get last exception script name (Python-spezifisch) +UserParameter=python.exception.script,tail -n 1 /var/log/zabbix/python.exceptions.log 2>/dev/null | jq -r '.script_name // ""' 2>/dev/null || echo "" + +# Get Python version (Python-spezifisch) +UserParameter=python.exception.version,tail -n 1 /var/log/zabbix/python.exceptions.log 2>/dev/null | jq -r '.python_version // ""' 2>/dev/null || echo "" + +# Get working directory (Python-spezifisch) +UserParameter=python.exception.workdir,tail -n 1 /var/log/zabbix/python.exceptions.log 2>/dev/null | jq -r '.working_directory // ""' 2>/dev/null || echo "" + +# Count recent exceptions (last hour) +UserParameter=python.exception.count.hour,awk -v since="$(date -d '1 hour ago' '+%Y-%m-%d %H:%M:%S')" '$0 > since' /var/log/zabbix/python.exceptions.log 2>/dev/null | wc -l + +# Count recent exceptions (last 24 hours) +UserParameter=python.exception.count.day,awk -v since="$(date -d '1 day ago' '+%Y-%m-%d %H:%M:%S')" '$0 > since' /var/log/zabbix/python.exceptions.log 2>/dev/null | wc -l + +# Log-Datei Größe (für Änderungsüberwachung) +UserParameter=python.exception.log.size,stat -c%s /var/log/zabbix/python.exceptions.log 2>/dev/null || echo "0" + +# Letzte Änderungszeit der Log-Datei +UserParameter=python.exception.log.mtime,stat -c%Y /var/log/zabbix/python.exceptions.log 2>/dev/null || echo "0" + +# Anzahl Zeilen in Log-Datei +UserParameter=python.exception.log.lines,wc -l < /var/log/zabbix/python.exceptions.log 2>/dev/null || echo "0" + +# Check ob Log-Datei existiert und lesbar ist +UserParameter=python.exception.log.accessible,[ -r /var/log/zabbix/python.exceptions.log ] && echo "1" || echo "0" + +# Check if Python exception handler is active (prüft ob das Modul importierbar ist) +UserParameter=python.exception.handler.active,python3 -c "import sys; sys.path.insert(0, '/usr/lib/python3.12'); import zabbix_exception_handler; print('1')" 2>/dev/null || echo "0" + +# Alternative check: Prüfe ob Handler-Modul existiert +UserParameter=python.exception.handler.available,[ -f /usr/lib/python3.12/zabbix_exception_handler.py ] && echo "1" || echo "0" \ No newline at end of file diff --git a/src/usr/lib/python3.12/zabbix_monitor/__init__.py b/src/usr/lib/python3.12/zabbix_monitor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/usr/lib/python3.12/zabbix_monitor/zabbix_exception_handler.py b/src/usr/lib/python3.12/zabbix_monitor/zabbix_exception_handler.py new file mode 100644 index 0000000..4dce2ad --- /dev/null +++ b/src/usr/lib/python3.12/zabbix_monitor/zabbix_exception_handler.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +""" +Zabbix Python Exception Handler +Captures and logs Python exceptions for monitoring +""" + +import sys +import json +import traceback +import os +import hashlib +import datetime +import subprocess +from typing import Optional, Dict, Any + + +class ZabbixPythonExceptionHandler: + """Python exception handler for Zabbix monitoring""" + + _log_file = '/var/log/zabbix/python.exceptions.log' + _previous_handler = None + _initialized = False + + @classmethod + def init(cls): + """Initialize the exception handler""" + if cls._initialized: + return + + # Store previous exception handler + cls._previous_handler = sys.excepthook + + # Set our exception handler + sys.excepthook = cls.handle_exception + + cls._initialized = True + + @classmethod + def handle_exception(cls, exc_type, exc_value, exc_traceback): + """Handle uncaught exceptions""" + if exc_traceback is not None: + cls._log_exception(exc_type, exc_value, exc_traceback) + + # Call previous handler if exists + if cls._previous_handler: + cls._previous_handler(exc_type, exc_value, exc_traceback) + else: + # Default behavior: print traceback and exit + traceback.print_exception(exc_type, exc_value, exc_traceback) + sys.exit(1) + + @classmethod + def _log_exception(cls, exc_type, exc_value, exc_traceback): + """Log exception details to Zabbix log file""" + try: + # Ensure log directory exists + log_dir = os.path.dirname(cls._log_file) + if not os.path.isdir(log_dir): + os.makedirs(log_dir, mode=0o755, exist_ok=True) + # Try to set proper ownership for zabbix log directory + try: + import pwd, grp + zabbix_uid = pwd.getpwnam('zabbix').pw_uid + zabbix_gid = grp.getgrnam('zabbix').gr_gid + os.chown(log_dir, zabbix_uid, zabbix_gid) + except (KeyError, PermissionError): + pass # Silently ignore if zabbix user doesn't exist or no permissions + + # Get hostname/script name + script_name = sys.argv[0] if sys.argv else 'python' + hostname = os.environ.get('HTTP_HOST', + os.environ.get('SERVER_NAME', + os.path.basename(script_name))) + + # Extract exception details + tb_list = traceback.extract_tb(exc_traceback) + if tb_list: + last_frame = tb_list[-1] + filename = last_frame.filename + line_number = last_frame.lineno + else: + filename = '' + line_number = 0 + + exception_message = str(exc_value) + + # Create unique identifier for deduplication + unique_id = hashlib.md5( + f"{hostname}|{filename}|{line_number}|{exception_message}".encode('utf-8') + ).hexdigest() + + # Get pretty traceback with code context + pretty_traceback = cls._make_pretty_traceback(exc_type, exc_value, exc_traceback) + + # Prepare log entry + log_entry = { + 'timestamp': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'host': hostname, + 'file': filename, + 'line': line_number, + 'message': exception_message, + 'stacktrace': pretty_traceback, + 'unique_id': unique_id, + 'request_uri': os.environ.get('REQUEST_URI', ''), + 'user_agent': os.environ.get('HTTP_USER_AGENT', ''), + 'remote_addr': os.environ.get('REMOTE_ADDR', ''), + 'exception_class': exc_type.__name__, + 'script_name': script_name, + 'python_version': sys.version.split()[0], + 'working_directory': os.getcwd() + } + + # Convert to JSON and write to log + json_entry = json.dumps(log_entry, ensure_ascii=False, separators=(',', ':')) + '\n' + + # Write to log file with proper locking + with open(cls._log_file, 'a', encoding='utf-8') as f: + f.write(json_entry) + + # Set proper permissions on log file + if os.path.exists(cls._log_file): + try: + os.chmod(cls._log_file, 0o644) + import pwd, grp + www_data_uid = pwd.getpwnam('www-data').pw_uid + zabbix_gid = grp.getgrnam('zabbix').gr_gid + os.chown(cls._log_file, www_data_uid, zabbix_gid) + except (KeyError, PermissionError): + pass # Silently ignore if users don't exist or no permissions + + except Exception as e: + # Silently fail to avoid infinite loops + try: + import logging + logging.error(f"Zabbix Python Exception Handler failed: {e}") + except: + pass # Ultimate fallback - do nothing + + @classmethod + def _make_pretty_traceback(cls, exc_type, exc_value, exc_traceback) -> str: + """Create a pretty formatted traceback with code context""" + result = [f"# Exception: {exc_type.__name__}: {exc_value}"] + result.append("Traceback (most recent call last):") + + tb_list = traceback.extract_tb(exc_traceback) + + for i, frame in enumerate(tb_list): + frame_info = f"* #{i} @ File: {frame.filename}:{frame.lineno}" + if frame.name: + frame_info += f" - {frame.name}()" + + result.append(frame_info) + + # Try to get code context around the error line + if frame.filename and os.path.exists(frame.filename): + try: + context_lines = cls._get_code_context(frame.filename, frame.lineno) + if context_lines: + result.append("") # Empty line before code context + result.extend(context_lines) + except Exception: + pass # Ignore errors getting code context + + return "\n".join(result) + + @classmethod + def _get_code_context(cls, filename: str, line_number: int, context: int = 8) -> list: + """Get code context around a specific line""" + try: + start_line = max(1, line_number - context) + end_line = line_number + context + + # Use subprocess to get lines (similar to PHP version) + cmd = ['sed', '-n', f'{start_line},{end_line}p', filename] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=5) + + if result.returncode == 0 and result.stdout: + lines = result.stdout.strip().split('\n') + formatted_lines = [] + for i, line in enumerate(lines): + line_num = start_line + i + formatted_lines.append(f"{line_num:6d}: {line}") + return formatted_lines + + except Exception: + pass + + return [] + + +def install_exception_handler(): + """Convenience function to install the exception handler""" + ZabbixPythonExceptionHandler.init() + + +# Auto-initialize when imported (similar to PHP version) +install_exception_handler() \ No newline at end of file diff --git a/src/usr/lib/python3.12/zabbix_monitor/zabbix_monitor.py b/src/usr/lib/python3.12/zabbix_monitor/zabbix_monitor.py new file mode 100644 index 0000000..9b645a6 --- /dev/null +++ b/src/usr/lib/python3.12/zabbix_monitor/zabbix_monitor.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +""" +Simple Zabbix Exception Monitor + +This is a simpler entry point for Zabbix exception monitoring. +Just import this module to enable exception logging. + +Usage: + import sys + sys.path.insert(0, '/usr/lib/python3.12') + import zabbix_monitor + + # Your application code here... +""" + +# Import and auto-initialize exception handler +from .zabbix_exception_handler import ZabbixPythonExceptionHandler + +# Initialize immediately +ZabbixPythonExceptionHandler.init() + +# Export main class for manual control if needed +__all__ = ['ZabbixPythonExceptionHandler'] \ No newline at end of file diff --git a/src/usr/share/doc/zabbix-python-monitoring/zabbix_template_python_exceptions.yaml b/src/usr/share/doc/zabbix-python-monitoring/zabbix_template_python_exceptions.yaml new file mode 100644 index 0000000..3dc6df9 --- /dev/null +++ b/src/usr/share/doc/zabbix-python-monitoring/zabbix_template_python_exceptions.yaml @@ -0,0 +1,133 @@ +zabbix_export: + version: '6.4' + template_groups: + - uuid: 7df96b18c230490a9a0a9e2307226338 + name: 'Templates/Applications' + templates: + - uuid: c770cff7c4bf49e6b2b0e7a5f3d2c4a2 + template: 'Python Exceptions Monitoring' + name: 'Python Exceptions Monitoring' + description: 'Template for monitoring Python exceptions with deduplication via UserParameter' + groups: + - name: 'Templates/Applications' + macros: + - macro: '{$PYTHON.LOG.FILE}' + value: '/var/log/zabbix/python.exceptions.log' + description: 'Path to Python exception log file' + items: + - uuid: 8df96b18c230490a9a0a9e2307226342 + name: 'Python Exception Monitor' + type: ZABBIX_PASSIVE + key: 'python.exception.latest' + delay: 30s + history: 7d + value_type: TEXT + description: 'Monitor Python exceptions from log file via UserParameter' + preprocessing: + - type: JAVASCRIPT + parameters: + - | + // Parse JSON from UserParameter (already last line) + try { + // Debug: Return original value if empty + if (!value || value === '' || value.trim() === '') { + return 'No exceptions logged yet'; + } + + // Try to parse JSON (UserParameter gives us just the last line) + var entry = JSON.parse(value); + + // Format stacktrace with line numbers + var formattedStack = 'No stacktrace available'; + if (entry.stacktrace && entry.stacktrace.length > 0) { + formattedStack = entry.stacktrace.split('\n').map(function(line, index) { + return line.trim(); + }).join('\n'); + } + + // Return nicely formatted text (not JSON) + return '=== PYTHON EXCEPTION ===\n' + + 'Message: ' + (entry.message || 'No message') + '\n' + + 'File: ' + (entry.file || 'unknown') + '\n' + + 'Line: ' + (entry.line || 0) + '\n' + + 'Host: ' + (entry.host || 'unknown') + '\n' + + 'Time: ' + (entry.timestamp || 'unknown') + '\n' + + 'Exception Type: ' + (entry.exception_class || 'unknown') + '\n' + + 'Script: ' + (entry.script_name || 'unknown') + '\n' + + 'Python Version: ' + (entry.python_version || 'unknown') + '\n' + + 'Working Dir: ' + (entry.working_directory || 'unknown') + '\n' + + 'ID: ' + (entry.unique_id || 'unknown') + '\n\n' + + '=== STACKTRACE ===\n' + + formattedStack; + + } catch (e) { + // Return error info for debugging + return 'JSON Parse Error: ' + e.message + '\n\nOriginal value:\n' + value; + } + triggers: + - uuid: 9df96b18c230490a9a0a9e2307226352 + expression: 'length(last(/Python Exceptions Monitoring/python.exception.latest)) > 15' + name: 'Python Exception Detected' + event_name: 'New Python Exception Logged' + priority: HIGH + description: | + 🐍 PYTHON EXCEPTION DETECTED 🐍 + ═══════════════════════════════════════════════════════ + + 📊 FORMATTED EXCEPTION DATA: + {ITEM.LASTVALUE} + + ═══════════════════════════════════════════════════════ + 🔍 QUICK ACCESS LINKS: + + • Latest Data: Monitoring → Latest Data → {HOST.NAME} + • Full History: Monitoring → Latest Data → "Python Exception Monitor" + • Host Overview: Monitoring → Hosts → {HOST.NAME} + + ═══════════════════════════════════════════════════════ + ℹ️ INFORMATION: + + This alert contains the complete Python exception details including: + ✓ Exception message and exact location + ✓ Full stacktrace with numbered lines + ✓ Script context (name, working directory, Python version) + ✓ Request context (host, URI, headers) + ✓ Unique identifier for correlation + ✓ Exact timestamp of occurrence + + ⚠️ ACTION REQUIRED: This alert requires manual acknowledgment. + Use "Actions" → "Close" to dismiss after reviewing. + + ═══════════════════════════════════════════════════════ + Host: {HOST.NAME} | Time: {EVENT.TIME} | Severity: {TRIGGER.SEVERITY} + manual_close: 'YES' + tags: + - tag: scope + value: application + - tag: component + value: python + - uuid: 8df96b18c230490a9a0a9e2307226343 + name: 'Python Exception Log Size' + type: ZABBIX_PASSIVE + key: 'python.exception.log.size' + delay: 30s + history: 7d + description: 'Size of Python exception log file (bytes) - for monitoring purposes' + - uuid: 8df96b18c230490a9a0a9e2307226344 + name: 'Python Exception Log Accessible' + type: ZABBIX_PASSIVE + key: 'python.exception.log.accessible' + delay: 300s + history: 7d + description: 'Check if Python exception log is accessible (1=yes, 0=no)' + triggers: + - uuid: 9df96b18c230490a9a0a9e2307226345 + expression: 'last(/Python Exceptions Monitoring/python.exception.log.accessible) = 0' + name: 'Python Exception Log Not Accessible' + priority: HIGH + description: 'Python exception log file is not accessible' + tags: + - tag: scope + value: availability + - tag: component + value: python \ No newline at end of file