This commit is contained in:
Holger Sielaff
2025-09-29 18:26:33 +02:00
commit 35b6ac3fbc
6 changed files with 474 additions and 0 deletions

62
example_usage.py Normal file
View File

@@ -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...")

View File

@@ -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"

View File

@@ -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()

View File

@@ -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']

View File

@@ -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