initial
This commit is contained in:
62
example_usage.py
Normal file
62
example_usage.py
Normal 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...")
|
||||
59
src/etc/zabbix/zabbix_agentd.d/python-exceptions.conf
Normal file
59
src/etc/zabbix/zabbix_agentd.d/python-exceptions.conf
Normal 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"
|
||||
0
src/usr/lib/python3.12/zabbix_monitor/__init__.py
Normal file
0
src/usr/lib/python3.12/zabbix_monitor/__init__.py
Normal 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()
|
||||
23
src/usr/lib/python3.12/zabbix_monitor/zabbix_monitor.py
Normal file
23
src/usr/lib/python3.12/zabbix_monitor/zabbix_monitor.py
Normal 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']
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user