Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-05 17:51:16 +02:00
Commit 0d7d888502
1594 geänderte Dateien mit 122839 neuen und 0 gelöschten Zeilen

Datei anzeigen

Datei anzeigen

@ -0,0 +1,273 @@
import logging
import traceback
from functools import wraps
from typing import Optional, Dict, Any, Callable, Union
from flask import (
Flask, request, jsonify, render_template, flash, redirect,
url_for, current_app, g
)
from werkzeug.exceptions import HTTPException
import psycopg2
from .exceptions import (
BaseApplicationException, DatabaseException, ValidationException,
AuthenticationException, ResourceException, QueryError,
ConnectionError, TransactionError
)
logger = logging.getLogger(__name__)
def init_error_handlers(app: Flask) -> None:
@app.before_request
def before_request():
g.request_id = request.headers.get('X-Request-ID',
BaseApplicationException('', '', 0).request_id)
@app.errorhandler(BaseApplicationException)
def handle_application_error(error: BaseApplicationException):
return _handle_error(error)
@app.errorhandler(HTTPException)
def handle_http_error(error: HTTPException):
return _handle_error(error)
@app.errorhandler(psycopg2.Error)
def handle_database_error(error: psycopg2.Error):
db_exception = _convert_psycopg2_error(error)
return _handle_error(db_exception)
@app.errorhandler(Exception)
def handle_unexpected_error(error: Exception):
logger.error(
f"Unexpected error: {str(error)}",
exc_info=True,
extra={'request_id': getattr(g, 'request_id', 'unknown')}
)
if current_app.debug:
raise
generic_error = BaseApplicationException(
message="An unexpected error occurred",
code="INTERNAL_ERROR",
status_code=500,
user_message="Ein unerwarteter Fehler ist aufgetreten"
)
return _handle_error(generic_error)
def _handle_error(error: Union[BaseApplicationException, HTTPException, Exception]) -> tuple:
if isinstance(error, HTTPException):
status_code = error.code
error_dict = {
'error': {
'code': error.name.upper().replace(' ', '_'),
'message': error.description or str(error),
'request_id': getattr(g, 'request_id', 'unknown')
}
}
user_message = error.description or str(error)
elif isinstance(error, BaseApplicationException):
status_code = error.status_code
error_dict = error.to_dict(include_details=current_app.debug)
error_dict['error']['request_id'] = getattr(g, 'request_id', error.request_id)
user_message = error.user_message
logger.error(
f"{error.__class__.__name__}: {error.message}",
extra={
'error_code': error.code,
'details': error.details,
'request_id': error_dict['error']['request_id']
}
)
else:
status_code = 500
error_dict = {
'error': {
'code': 'INTERNAL_ERROR',
'message': 'An internal error occurred',
'request_id': getattr(g, 'request_id', 'unknown')
}
}
user_message = "Ein interner Fehler ist aufgetreten"
if _is_json_request():
return jsonify(error_dict), status_code
else:
if status_code == 404:
return render_template('404.html'), 404
elif status_code >= 500:
return render_template('500.html', error=user_message), status_code
else:
flash(user_message, 'error')
return render_template('error.html',
error=user_message,
error_code=error_dict['error']['code'],
request_id=error_dict['error']['request_id']), status_code
def _convert_psycopg2_error(error: psycopg2.Error) -> DatabaseException:
error_code = getattr(error, 'pgcode', None)
error_message = str(error).split('\n')[0]
if isinstance(error, psycopg2.OperationalError):
return ConnectionError(
message=f"Database connection failed: {error_message}",
host=None
)
elif isinstance(error, psycopg2.IntegrityError):
if error_code == '23505':
return ValidationException(
message="Duplicate entry violation",
details={'constraint': error_message},
user_message="Dieser Eintrag existiert bereits"
)
elif error_code == '23503':
return ValidationException(
message="Foreign key violation",
details={'constraint': error_message},
user_message="Referenzierte Daten existieren nicht"
)
else:
return ValidationException(
message="Data integrity violation",
details={'error_code': error_code},
user_message="Datenintegritätsfehler"
)
elif isinstance(error, psycopg2.DataError):
return ValidationException(
message="Invalid data format",
details={'error': error_message},
user_message="Ungültiges Datenformat"
)
else:
return QueryError(
message=error_message,
query="[query hidden for security]",
error_code=error_code
)
def _is_json_request() -> bool:
return (request.is_json or
request.path.startswith('/api/') or
request.accept_mimetypes.best == 'application/json')
def handle_errors(
catch: tuple = (Exception,),
message: str = "Operation failed",
user_message: Optional[str] = None,
redirect_to: Optional[str] = None
) -> Callable:
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except catch as e:
if isinstance(e, BaseApplicationException):
raise
logger.error(
f"Error in {func.__name__}: {str(e)}",
exc_info=True,
extra={'request_id': getattr(g, 'request_id', 'unknown')}
)
if _is_json_request():
return jsonify({
'error': {
'code': 'OPERATION_FAILED',
'message': user_message or message,
'request_id': getattr(g, 'request_id', 'unknown')
}
}), 500
else:
flash(user_message or message, 'error')
if redirect_to:
return redirect(url_for(redirect_to))
return redirect(request.referrer or url_for('admin.dashboard'))
return wrapper
return decorator
def validate_request(
required_fields: Optional[Dict[str, type]] = None,
optional_fields: Optional[Dict[str, type]] = None
) -> Callable:
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
data = request.get_json() if request.is_json else request.form
if required_fields:
for field, expected_type in required_fields.items():
if field not in data:
raise ValidationException(
message=f"Missing required field: {field}",
field=field,
user_message=f"Pflichtfeld fehlt: {field}"
)
try:
if expected_type != str:
if expected_type == int:
int(data[field])
elif expected_type == float:
float(data[field])
elif expected_type == bool:
if isinstance(data[field], str):
if data[field].lower() not in ['true', 'false', '1', '0']:
raise ValueError
except (ValueError, TypeError):
raise ValidationException(
message=f"Invalid type for field {field}",
field=field,
value=data[field],
details={'expected_type': expected_type.__name__},
user_message=f"Ungültiger Typ für Feld {field}"
)
return func(*args, **kwargs)
return wrapper
return decorator
class ErrorContext:
def __init__(
self,
operation: str,
resource_type: Optional[str] = None,
resource_id: Optional[Any] = None
):
self.operation = operation
self.resource_type = resource_type
self.resource_id = resource_id
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_val is None:
return False
if isinstance(exc_val, BaseApplicationException):
return False
logger.error(
f"Error during {self.operation}",
exc_info=True,
extra={
'operation': self.operation,
'resource_type': self.resource_type,
'resource_id': self.resource_id,
'request_id': getattr(g, 'request_id', 'unknown')
}
)
return False

Datei anzeigen

@ -0,0 +1,356 @@
import uuid
from typing import Optional, Dict, Any
from datetime import datetime
class BaseApplicationException(Exception):
def __init__(
self,
message: str,
code: str,
status_code: int = 500,
details: Optional[Dict[str, Any]] = None,
user_message: Optional[str] = None
):
super().__init__(message)
self.message = message
self.code = code
self.status_code = status_code
self.details = details or {}
self.user_message = user_message or message
self.timestamp = datetime.utcnow()
self.request_id = str(uuid.uuid4())
def to_dict(self, include_details: bool = False) -> Dict[str, Any]:
result = {
'error': {
'code': self.code,
'message': self.user_message,
'timestamp': self.timestamp.isoformat(),
'request_id': self.request_id
}
}
if include_details and self.details:
result['error']['details'] = self.details
return result
class ValidationException(BaseApplicationException):
def __init__(
self,
message: str,
field: Optional[str] = None,
value: Any = None,
details: Optional[Dict[str, Any]] = None,
user_message: Optional[str] = None
):
details = details or {}
if field:
details['field'] = field
if value is not None:
details['value'] = str(value)
super().__init__(
message=message,
code='VALIDATION_ERROR',
status_code=400,
details=details,
user_message=user_message or "Ungültige Eingabe"
)
class InputValidationError(ValidationException):
def __init__(
self,
field: str,
message: str,
value: Any = None,
expected_type: Optional[str] = None
):
details = {'expected_type': expected_type} if expected_type else None
super().__init__(
message=f"Invalid input for field '{field}': {message}",
field=field,
value=value,
details=details,
user_message=f"Ungültiger Wert für Feld '{field}'"
)
class BusinessRuleViolation(ValidationException):
def __init__(
self,
rule: str,
message: str,
context: Optional[Dict[str, Any]] = None
):
super().__init__(
message=message,
details={'rule': rule, 'context': context or {}},
user_message="Geschäftsregel verletzt"
)
class DataIntegrityError(ValidationException):
def __init__(
self,
entity: str,
constraint: str,
message: str,
details: Optional[Dict[str, Any]] = None
):
details = details or {}
details.update({'entity': entity, 'constraint': constraint})
super().__init__(
message=message,
details=details,
user_message="Datenintegritätsfehler"
)
class AuthenticationException(BaseApplicationException):
def __init__(
self,
message: str,
details: Optional[Dict[str, Any]] = None,
user_message: Optional[str] = None
):
super().__init__(
message=message,
code='AUTHENTICATION_ERROR',
status_code=401,
details=details,
user_message=user_message or "Authentifizierung fehlgeschlagen"
)
class InvalidCredentialsError(AuthenticationException):
def __init__(self, username: Optional[str] = None):
details = {'username': username} if username else None
super().__init__(
message="Invalid username or password",
details=details,
user_message="Ungültiger Benutzername oder Passwort"
)
class SessionExpiredError(AuthenticationException):
def __init__(self, session_id: Optional[str] = None):
details = {'session_id': session_id} if session_id else None
super().__init__(
message="Session has expired",
details=details,
user_message="Ihre Sitzung ist abgelaufen"
)
class InsufficientPermissionsError(AuthenticationException):
def __init__(
self,
required_permission: str,
user_permissions: Optional[list] = None
):
super().__init__(
message=f"User lacks required permission: {required_permission}",
details={
'required': required_permission,
'user_permissions': user_permissions or []
},
user_message="Unzureichende Berechtigungen für diese Aktion"
)
self.status_code = 403
class DatabaseException(BaseApplicationException):
def __init__(
self,
message: str,
query: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
user_message: Optional[str] = None
):
details = details or {}
if query:
details['query_hash'] = str(hash(query))
super().__init__(
message=message,
code='DATABASE_ERROR',
status_code=500,
details=details,
user_message=user_message or "Datenbankfehler aufgetreten"
)
class ConnectionError(DatabaseException):
def __init__(self, message: str, host: Optional[str] = None):
details = {'host': host} if host else None
super().__init__(
message=message,
details=details,
user_message="Datenbankverbindung fehlgeschlagen"
)
class QueryError(DatabaseException):
def __init__(
self,
message: str,
query: str,
error_code: Optional[str] = None
):
super().__init__(
message=message,
query=query,
details={'error_code': error_code} if error_code else None,
user_message="Datenbankabfrage fehlgeschlagen"
)
class TransactionError(DatabaseException):
def __init__(self, message: str, operation: str):
super().__init__(
message=message,
details={'operation': operation},
user_message="Transaktion fehlgeschlagen"
)
class ExternalServiceException(BaseApplicationException):
def __init__(
self,
service_name: str,
message: str,
details: Optional[Dict[str, Any]] = None,
user_message: Optional[str] = None
):
details = details or {}
details['service'] = service_name
super().__init__(
message=message,
code='EXTERNAL_SERVICE_ERROR',
status_code=502,
details=details,
user_message=user_message or f"Fehler beim Zugriff auf {service_name}"
)
class APIError(ExternalServiceException):
def __init__(
self,
service_name: str,
endpoint: str,
status_code: int,
message: str
):
super().__init__(
service_name=service_name,
message=message,
details={
'endpoint': endpoint,
'response_status': status_code
},
user_message=f"API-Fehler bei {service_name}"
)
class TimeoutError(ExternalServiceException):
def __init__(
self,
service_name: str,
timeout_seconds: int,
operation: str
):
super().__init__(
service_name=service_name,
message=f"Timeout after {timeout_seconds}s while {operation}",
details={
'timeout_seconds': timeout_seconds,
'operation': operation
},
user_message=f"Zeitüberschreitung bei {service_name}"
)
class ResourceException(BaseApplicationException):
def __init__(
self,
message: str,
resource_type: str,
resource_id: Any = None,
details: Optional[Dict[str, Any]] = None,
user_message: Optional[str] = None
):
details = details or {}
details.update({
'resource_type': resource_type,
'resource_id': str(resource_id) if resource_id else None
})
super().__init__(
message=message,
code='RESOURCE_ERROR',
status_code=404,
details=details,
user_message=user_message or "Ressourcenfehler"
)
class ResourceNotFoundError(ResourceException):
def __init__(
self,
resource_type: str,
resource_id: Any = None,
search_criteria: Optional[Dict[str, Any]] = None
):
details = {'search_criteria': search_criteria} if search_criteria else None
super().__init__(
message=f"{resource_type} not found",
resource_type=resource_type,
resource_id=resource_id,
details=details,
user_message=f"{resource_type} nicht gefunden"
)
self.status_code = 404
class ResourceConflictError(ResourceException):
def __init__(
self,
resource_type: str,
resource_id: Any,
conflict_reason: str
):
super().__init__(
message=f"Conflict with {resource_type}: {conflict_reason}",
resource_type=resource_type,
resource_id=resource_id,
details={'conflict_reason': conflict_reason},
user_message=f"Konflikt mit {resource_type}"
)
self.status_code = 409
class ResourceLimitExceeded(ResourceException):
def __init__(
self,
resource_type: str,
limit: int,
current: int,
requested: Optional[int] = None
):
details = {
'limit': limit,
'current': current,
'requested': requested
}
super().__init__(
message=f"{resource_type} limit exceeded: {current}/{limit}",
resource_type=resource_type,
details=details,
user_message=f"Limit für {resource_type} überschritten"
)
self.status_code = 429

Datei anzeigen

@ -0,0 +1,190 @@
import logging
import logging.handlers
import json
import sys
import os
from datetime import datetime
from typing import Dict, Any
from flask import g, request, has_request_context
class StructuredFormatter(logging.Formatter):
def format(self, record):
log_data = {
'timestamp': datetime.utcnow().isoformat(),
'level': record.levelname,
'logger': record.name,
'message': record.getMessage(),
'module': record.module,
'function': record.funcName,
'line': record.lineno
}
if has_request_context():
log_data['request'] = {
'method': request.method,
'path': request.path,
'remote_addr': request.remote_addr,
'user_agent': request.user_agent.string,
'request_id': getattr(g, 'request_id', 'unknown')
}
if hasattr(record, 'request_id'):
log_data['request_id'] = record.request_id
if hasattr(record, 'error_code'):
log_data['error_code'] = record.error_code
if hasattr(record, 'details') and record.details:
log_data['details'] = self._sanitize_details(record.details)
if record.exc_info:
log_data['exception'] = {
'type': record.exc_info[0].__name__,
'message': str(record.exc_info[1]),
'traceback': self.formatException(record.exc_info)
}
return json.dumps(log_data, ensure_ascii=False)
def _sanitize_details(self, details: Dict[str, Any]) -> Dict[str, Any]:
sensitive_fields = {
'password', 'secret', 'token', 'api_key', 'authorization',
'credit_card', 'ssn', 'pin'
}
sanitized = {}
for key, value in details.items():
if any(field in key.lower() for field in sensitive_fields):
sanitized[key] = '[REDACTED]'
elif isinstance(value, dict):
sanitized[key] = self._sanitize_details(value)
else:
sanitized[key] = value
return sanitized
class ErrorLevelFilter(logging.Filter):
def __init__(self, min_level=logging.ERROR):
self.min_level = min_level
def filter(self, record):
return record.levelno >= self.min_level
def setup_logging(app):
log_level = os.getenv('LOG_LEVEL', 'INFO').upper()
log_dir = os.getenv('LOG_DIR', 'logs')
os.makedirs(log_dir, exist_ok=True)
root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, log_level))
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
if app.debug:
console_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
else:
console_formatter = StructuredFormatter()
console_handler.setFormatter(console_formatter)
root_logger.addHandler(console_handler)
app_log_handler = logging.handlers.RotatingFileHandler(
os.path.join(log_dir, 'app.log'),
maxBytes=10 * 1024 * 1024,
backupCount=10
)
app_log_handler.setLevel(logging.DEBUG)
app_log_handler.setFormatter(StructuredFormatter())
root_logger.addHandler(app_log_handler)
error_log_handler = logging.handlers.RotatingFileHandler(
os.path.join(log_dir, 'errors.log'),
maxBytes=10 * 1024 * 1024,
backupCount=10
)
error_log_handler.setLevel(logging.ERROR)
error_log_handler.setFormatter(StructuredFormatter())
error_log_handler.addFilter(ErrorLevelFilter())
root_logger.addHandler(error_log_handler)
security_logger = logging.getLogger('security')
security_handler = logging.handlers.RotatingFileHandler(
os.path.join(log_dir, 'security.log'),
maxBytes=10 * 1024 * 1024,
backupCount=20
)
security_handler.setFormatter(StructuredFormatter())
security_logger.addHandler(security_handler)
security_logger.setLevel(logging.INFO)
werkzeug_logger = logging.getLogger('werkzeug')
werkzeug_logger.setLevel(logging.WARNING)
@app.before_request
def log_request_info():
logger = logging.getLogger('request')
logger.info(
'Request started',
extra={
'request_id': getattr(g, 'request_id', 'unknown'),
'details': {
'method': request.method,
'path': request.path,
'query_params': dict(request.args),
'content_length': request.content_length
}
}
)
@app.after_request
def log_response_info(response):
logger = logging.getLogger('request')
logger.info(
'Request completed',
extra={
'request_id': getattr(g, 'request_id', 'unknown'),
'details': {
'status_code': response.status_code,
'content_length': response.content_length or 0
}
}
)
return response
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)
def log_error(logger: logging.Logger, message: str, error: Exception = None, **kwargs):
extra = kwargs.copy()
if error:
extra['error_type'] = type(error).__name__
extra['error_message'] = str(error)
if hasattr(error, 'code'):
extra['error_code'] = error.code
if hasattr(error, 'details'):
extra['details'] = error.details
logger.error(message, exc_info=error, extra=extra)
def log_security_event(event_type: str, message: str, **details):
logger = logging.getLogger('security')
logger.warning(
f"Security Event: {event_type} - {message}",
extra={
'security_event': event_type,
'details': details,
'request_id': getattr(g, 'request_id', 'unknown') if has_request_context() else None
}
)

Datei anzeigen

@ -0,0 +1,246 @@
import time
import functools
from typing import Dict, Any, Optional, List
from collections import defaultdict, deque
from datetime import datetime, timedelta
from threading import Lock
import logging
from prometheus_client import Counter, Histogram, Gauge, generate_latest
from flask import g, request, Response
from .exceptions import BaseApplicationException
from .logging_config import log_security_event
logger = logging.getLogger(__name__)
class ErrorMetrics:
def __init__(self):
self.error_counter = Counter(
'app_errors_total',
'Total number of errors',
['error_code', 'status_code', 'endpoint']
)
self.error_rate = Gauge(
'app_error_rate',
'Error rate per minute',
['error_code']
)
self.request_duration = Histogram(
'app_request_duration_seconds',
'Request duration in seconds',
['method', 'endpoint', 'status_code']
)
self.validation_errors = Counter(
'app_validation_errors_total',
'Total validation errors',
['field', 'endpoint']
)
self.auth_failures = Counter(
'app_auth_failures_total',
'Total authentication failures',
['reason', 'endpoint']
)
self.db_errors = Counter(
'app_database_errors_total',
'Total database errors',
['error_type', 'operation']
)
self._error_history = defaultdict(lambda: deque(maxlen=60))
self._lock = Lock()
def record_error(self, error: BaseApplicationException, endpoint: str = None):
endpoint = endpoint or request.endpoint or 'unknown'
self.error_counter.labels(
error_code=error.code,
status_code=error.status_code,
endpoint=endpoint
).inc()
with self._lock:
self._error_history[error.code].append(datetime.utcnow())
self._update_error_rates()
if error.code == 'VALIDATION_ERROR' and 'field' in error.details:
self.validation_errors.labels(
field=error.details['field'],
endpoint=endpoint
).inc()
elif error.code == 'AUTHENTICATION_ERROR':
reason = error.__class__.__name__
self.auth_failures.labels(
reason=reason,
endpoint=endpoint
).inc()
elif error.code == 'DATABASE_ERROR':
error_type = error.__class__.__name__
operation = error.details.get('operation', 'unknown')
self.db_errors.labels(
error_type=error_type,
operation=operation
).inc()
def _update_error_rates(self):
now = datetime.utcnow()
one_minute_ago = now - timedelta(minutes=1)
for error_code, timestamps in self._error_history.items():
recent_count = sum(1 for ts in timestamps if ts >= one_minute_ago)
self.error_rate.labels(error_code=error_code).set(recent_count)
class AlertManager:
def __init__(self):
self.alerts = []
self.alert_thresholds = {
'error_rate': 10,
'auth_failure_rate': 5,
'db_error_rate': 3,
'response_time_95th': 2.0
}
self._lock = Lock()
def check_alerts(self, metrics: ErrorMetrics):
new_alerts = []
for error_code, rate in self._get_current_error_rates(metrics).items():
if rate > self.alert_thresholds['error_rate']:
new_alerts.append({
'type': 'high_error_rate',
'severity': 'critical',
'error_code': error_code,
'rate': rate,
'threshold': self.alert_thresholds['error_rate'],
'message': f'High error rate for {error_code}: {rate}/min',
'timestamp': datetime.utcnow()
})
auth_failure_rate = self._get_auth_failure_rate(metrics)
if auth_failure_rate > self.alert_thresholds['auth_failure_rate']:
new_alerts.append({
'type': 'auth_failures',
'severity': 'warning',
'rate': auth_failure_rate,
'threshold': self.alert_thresholds['auth_failure_rate'],
'message': f'High authentication failure rate: {auth_failure_rate}/min',
'timestamp': datetime.utcnow()
})
log_security_event(
'HIGH_AUTH_FAILURE_RATE',
f'Authentication failure rate exceeded threshold',
rate=auth_failure_rate,
threshold=self.alert_thresholds['auth_failure_rate']
)
with self._lock:
self.alerts.extend(new_alerts)
self.alerts = [a for a in self.alerts
if a['timestamp'] > datetime.utcnow() - timedelta(hours=24)]
return new_alerts
def _get_current_error_rates(self, metrics: ErrorMetrics) -> Dict[str, float]:
rates = {}
with metrics._lock:
now = datetime.utcnow()
one_minute_ago = now - timedelta(minutes=1)
for error_code, timestamps in metrics._error_history.items():
rates[error_code] = sum(1 for ts in timestamps if ts >= one_minute_ago)
return rates
def _get_auth_failure_rate(self, metrics: ErrorMetrics) -> float:
return sum(
sample.value
for sample in metrics.auth_failures._child_samples()
) / 60.0
def get_active_alerts(self) -> List[Dict[str, Any]]:
with self._lock:
return list(self.alerts)
error_metrics = ErrorMetrics()
alert_manager = AlertManager()
def init_monitoring(app):
@app.before_request
def before_request():
g.start_time = time.time()
@app.after_request
def after_request(response):
if hasattr(g, 'start_time'):
duration = time.time() - g.start_time
error_metrics.request_duration.labels(
method=request.method,
endpoint=request.endpoint or 'unknown',
status_code=response.status_code
).observe(duration)
return response
@app.route('/metrics')
def metrics():
alert_manager.check_alerts(error_metrics)
return Response(generate_latest(), mimetype='text/plain')
@app.route('/api/alerts')
def get_alerts():
alerts = alert_manager.get_active_alerts()
return {
'alerts': alerts,
'total': len(alerts),
'critical': len([a for a in alerts if a['severity'] == 'critical']),
'warning': len([a for a in alerts if a['severity'] == 'warning'])
}
def monitor_performance(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
try:
result = func(*args, **kwargs)
return result
finally:
duration = time.time() - start_time
if duration > 1.0:
logger.warning(
f"Slow function execution: {func.__name__}",
extra={
'function': func.__name__,
'duration': duration,
'request_id': getattr(g, 'request_id', 'unknown')
}
)
return wrapper
def track_error(error: BaseApplicationException):
error_metrics.record_error(error)
if error.status_code >= 500:
logger.error(
f"Critical error occurred: {error.code}",
extra={
'error_code': error.code,
'message': error.message,
'details': error.details,
'request_id': error.request_id
}
)

Datei anzeigen

@ -0,0 +1,435 @@
import re
from typing import Any, Optional, List, Dict, Callable, Union
from datetime import datetime, date
from functools import wraps
import ipaddress
from flask import request
from .exceptions import InputValidationError, ValidationException
class ValidationRules:
EMAIL_PATTERN = re.compile(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
)
PHONE_PATTERN = re.compile(
r'^[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,10}$'
)
LICENSE_KEY_PATTERN = re.compile(
r'^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
)
SAFE_STRING_PATTERN = re.compile(
r'^[a-zA-Z0-9\s\-\_\.\,\!\?\@\#\$\%\&\*\(\)\[\]\{\}\:\;\'\"\+\=\/\\]+$'
)
USERNAME_PATTERN = re.compile(
r'^[a-zA-Z0-9_\-\.]{3,50}$'
)
PASSWORD_MIN_LENGTH = 8
PASSWORD_REQUIRE_UPPER = True
PASSWORD_REQUIRE_LOWER = True
PASSWORD_REQUIRE_DIGIT = True
PASSWORD_REQUIRE_SPECIAL = True
class Validators:
@staticmethod
def required(value: Any, field_name: str = "field") -> Any:
if value is None or (isinstance(value, str) and not value.strip()):
raise InputValidationError(
field=field_name,
message="This field is required",
value=value
)
return value
@staticmethod
def email(value: str, field_name: str = "email") -> str:
value = Validators.required(value, field_name).strip()
if not ValidationRules.EMAIL_PATTERN.match(value):
raise InputValidationError(
field=field_name,
message="Invalid email format",
value=value,
expected_type="email"
)
return value.lower()
@staticmethod
def phone(value: str, field_name: str = "phone") -> str:
value = Validators.required(value, field_name).strip()
cleaned = re.sub(r'[\s\-\(\)]', '', value)
if not ValidationRules.PHONE_PATTERN.match(value):
raise InputValidationError(
field=field_name,
message="Invalid phone number format",
value=value,
expected_type="phone"
)
return cleaned
@staticmethod
def license_key(value: str, field_name: str = "license_key") -> str:
value = Validators.required(value, field_name).strip().upper()
if not ValidationRules.LICENSE_KEY_PATTERN.match(value):
raise InputValidationError(
field=field_name,
message="Invalid license key format (expected: XXXX-XXXX-XXXX-XXXX)",
value=value,
expected_type="license_key"
)
return value
@staticmethod
def integer(
value: Union[str, int],
field_name: str = "field",
min_value: Optional[int] = None,
max_value: Optional[int] = None
) -> int:
try:
int_value = int(value)
except (ValueError, TypeError):
raise InputValidationError(
field=field_name,
message="Must be a valid integer",
value=value,
expected_type="integer"
)
if min_value is not None and int_value < min_value:
raise InputValidationError(
field=field_name,
message=f"Must be at least {min_value}",
value=int_value
)
if max_value is not None and int_value > max_value:
raise InputValidationError(
field=field_name,
message=f"Must be at most {max_value}",
value=int_value
)
return int_value
@staticmethod
def float_number(
value: Union[str, float],
field_name: str = "field",
min_value: Optional[float] = None,
max_value: Optional[float] = None
) -> float:
try:
float_value = float(value)
except (ValueError, TypeError):
raise InputValidationError(
field=field_name,
message="Must be a valid number",
value=value,
expected_type="float"
)
if min_value is not None and float_value < min_value:
raise InputValidationError(
field=field_name,
message=f"Must be at least {min_value}",
value=float_value
)
if max_value is not None and float_value > max_value:
raise InputValidationError(
field=field_name,
message=f"Must be at most {max_value}",
value=float_value
)
return float_value
@staticmethod
def boolean(value: Union[str, bool], field_name: str = "field") -> bool:
if isinstance(value, bool):
return value
if isinstance(value, str):
value_lower = value.lower()
if value_lower in ['true', '1', 'yes', 'on']:
return True
elif value_lower in ['false', '0', 'no', 'off']:
return False
raise InputValidationError(
field=field_name,
message="Must be a valid boolean",
value=value,
expected_type="boolean"
)
@staticmethod
def string(
value: str,
field_name: str = "field",
min_length: Optional[int] = None,
max_length: Optional[int] = None,
pattern: Optional[re.Pattern] = None,
safe_only: bool = False
) -> str:
value = Validators.required(value, field_name).strip()
if min_length is not None and len(value) < min_length:
raise InputValidationError(
field=field_name,
message=f"Must be at least {min_length} characters",
value=value
)
if max_length is not None and len(value) > max_length:
raise InputValidationError(
field=field_name,
message=f"Must be at most {max_length} characters",
value=value
)
if safe_only and not ValidationRules.SAFE_STRING_PATTERN.match(value):
raise InputValidationError(
field=field_name,
message="Contains invalid characters",
value=value
)
if pattern and not pattern.match(value):
raise InputValidationError(
field=field_name,
message="Does not match required format",
value=value
)
return value
@staticmethod
def username(value: str, field_name: str = "username") -> str:
value = Validators.required(value, field_name).strip()
if not ValidationRules.USERNAME_PATTERN.match(value):
raise InputValidationError(
field=field_name,
message="Username must be 3-50 characters and contain only letters, numbers, _, -, or .",
value=value,
expected_type="username"
)
return value
@staticmethod
def password(value: str, field_name: str = "password") -> str:
value = Validators.required(value, field_name)
errors = []
if len(value) < ValidationRules.PASSWORD_MIN_LENGTH:
errors.append(f"at least {ValidationRules.PASSWORD_MIN_LENGTH} characters")
if ValidationRules.PASSWORD_REQUIRE_UPPER and not re.search(r'[A-Z]', value):
errors.append("at least one uppercase letter")
if ValidationRules.PASSWORD_REQUIRE_LOWER and not re.search(r'[a-z]', value):
errors.append("at least one lowercase letter")
if ValidationRules.PASSWORD_REQUIRE_DIGIT and not re.search(r'\d', value):
errors.append("at least one digit")
if ValidationRules.PASSWORD_REQUIRE_SPECIAL and not re.search(r'[!@#$%^&*(),.?":{}|<>]', value):
errors.append("at least one special character")
if errors:
raise InputValidationError(
field=field_name,
message=f"Password must contain {', '.join(errors)}",
value="[hidden]"
)
return value
@staticmethod
def date_string(
value: str,
field_name: str = "date",
format: str = "%Y-%m-%d",
min_date: Optional[date] = None,
max_date: Optional[date] = None
) -> date:
value = Validators.required(value, field_name).strip()
try:
date_value = datetime.strptime(value, format).date()
except ValueError:
raise InputValidationError(
field=field_name,
message=f"Invalid date format (expected: {format})",
value=value,
expected_type="date"
)
if min_date and date_value < min_date:
raise InputValidationError(
field=field_name,
message=f"Date must be after {min_date}",
value=value
)
if max_date and date_value > max_date:
raise InputValidationError(
field=field_name,
message=f"Date must be before {max_date}",
value=value
)
return date_value
@staticmethod
def ip_address(
value: str,
field_name: str = "ip_address",
version: Optional[int] = None
) -> str:
value = Validators.required(value, field_name).strip()
try:
ip = ipaddress.ip_address(value)
if version and ip.version != version:
raise ValueError
except ValueError:
version_str = f"IPv{version}" if version else "IP"
raise InputValidationError(
field=field_name,
message=f"Invalid {version_str} address",
value=value,
expected_type="ip_address"
)
return str(ip)
@staticmethod
def url(
value: str,
field_name: str = "url",
require_https: bool = False
) -> str:
value = Validators.required(value, field_name).strip()
url_pattern = re.compile(
r'^https?://'
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|'
r'localhost|'
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
r'(?::\d+)?'
r'(?:/?|[/?]\S+)$', re.IGNORECASE
)
if not url_pattern.match(value):
raise InputValidationError(
field=field_name,
message="Invalid URL format",
value=value,
expected_type="url"
)
if require_https and not value.startswith('https://'):
raise InputValidationError(
field=field_name,
message="URL must use HTTPS",
value=value
)
return value
@staticmethod
def enum(
value: Any,
field_name: str,
allowed_values: List[Any]
) -> Any:
if value not in allowed_values:
raise InputValidationError(
field=field_name,
message=f"Must be one of: {', '.join(map(str, allowed_values))}",
value=value
)
return value
def validate(rules: Dict[str, Dict[str, Any]]) -> Callable:
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
data = request.get_json() if request.is_json else request.form
validated_data = {}
for field_name, field_rules in rules.items():
value = data.get(field_name)
if 'required' in field_rules and field_rules['required']:
value = Validators.required(value, field_name)
elif value is None or value == '':
if 'default' in field_rules:
validated_data[field_name] = field_rules['default']
continue
validator_name = field_rules.get('type', 'string')
validator_func = getattr(Validators, validator_name, None)
if not validator_func:
raise ValueError(f"Unknown validator type: {validator_name}")
validator_params = {
k: v for k, v in field_rules.items()
if k not in ['type', 'required', 'default']
}
validator_params['field_name'] = field_name
validated_data[field_name] = validator_func(value, **validator_params)
request.validated_data = validated_data
return func(*args, **kwargs)
return wrapper
return decorator
def sanitize_html(value: str) -> str:
dangerous_tags = re.compile(
r'<(script|iframe|object|embed|form|input|button|textarea|select|link|meta|style).*?>.*?</\1>',
re.IGNORECASE | re.DOTALL
)
dangerous_attrs = re.compile(
r'\s*(on\w+|style|javascript:)[\s]*=[\s]*["\']?[^"\'>\s]+',
re.IGNORECASE
)
value = dangerous_tags.sub('', value)
value = dangerous_attrs.sub('', value)
return value
def sanitize_sql_identifier(value: str) -> str:
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value):
raise ValidationException(
message="Invalid SQL identifier",
details={'value': value},
user_message="Ungültiger Bezeichner"
)
return value