533 lines
15 KiB
Python
533 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
Certificate Logger Service
|
|
|
|
This module provides centralized logging functionality for the Survey Custom
|
|
Certificate Template module. It ensures consistent logging across all components
|
|
and provides utilities for structured logging of certificate generation events.
|
|
"""
|
|
|
|
import logging
|
|
import traceback
|
|
from datetime import datetime
|
|
from typing import Optional, Dict, Any
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CertificateLogger:
|
|
"""
|
|
Centralized logging service for certificate operations.
|
|
|
|
This class provides structured logging methods for all certificate-related
|
|
operations, ensuring consistent log formatting and comprehensive error tracking.
|
|
"""
|
|
|
|
# Log level constants
|
|
DEBUG = logging.DEBUG
|
|
INFO = logging.INFO
|
|
WARNING = logging.WARNING
|
|
ERROR = logging.ERROR
|
|
CRITICAL = logging.CRITICAL
|
|
|
|
@staticmethod
|
|
def _format_context(context: Optional[Dict[str, Any]] = None) -> str:
|
|
"""
|
|
Format context dictionary into a readable string for logging.
|
|
|
|
Args:
|
|
context: Dictionary containing contextual information
|
|
|
|
Returns:
|
|
str: Formatted context string
|
|
"""
|
|
if not context:
|
|
return ""
|
|
|
|
try:
|
|
parts = []
|
|
for key, value in context.items():
|
|
if value is not None:
|
|
parts.append(f"{key}={value}")
|
|
return " | ".join(parts) if parts else ""
|
|
except Exception as e:
|
|
return f"[Error formatting context: {str(e)}]"
|
|
|
|
@classmethod
|
|
def log_certificate_generation_start(
|
|
cls,
|
|
survey_id: int,
|
|
survey_title: str,
|
|
user_input_id: int,
|
|
partner_name: Optional[str] = None
|
|
):
|
|
"""
|
|
Log the start of certificate generation.
|
|
|
|
Args:
|
|
survey_id: ID of the survey
|
|
survey_title: Title of the survey
|
|
user_input_id: ID of the user input record
|
|
partner_name: Name of the participant (optional)
|
|
"""
|
|
context = cls._format_context({
|
|
'survey_id': survey_id,
|
|
'survey_title': survey_title,
|
|
'user_input_id': user_input_id,
|
|
'partner_name': partner_name,
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
_logger.info(
|
|
'=== CERTIFICATE GENERATION START === | %s',
|
|
context
|
|
)
|
|
|
|
@classmethod
|
|
def log_certificate_generation_success(
|
|
cls,
|
|
survey_id: int,
|
|
user_input_id: int,
|
|
pdf_size: int,
|
|
duration_ms: Optional[float] = None
|
|
):
|
|
"""
|
|
Log successful certificate generation.
|
|
|
|
Args:
|
|
survey_id: ID of the survey
|
|
user_input_id: ID of the user input record
|
|
pdf_size: Size of generated PDF in bytes
|
|
duration_ms: Generation duration in milliseconds (optional)
|
|
"""
|
|
context = cls._format_context({
|
|
'survey_id': survey_id,
|
|
'user_input_id': user_input_id,
|
|
'pdf_size_bytes': pdf_size,
|
|
'pdf_size_kb': round(pdf_size / 1024, 2),
|
|
'duration_ms': duration_ms,
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
_logger.info(
|
|
'=== CERTIFICATE GENERATION SUCCESS === | %s',
|
|
context
|
|
)
|
|
|
|
@classmethod
|
|
def log_certificate_generation_failure(
|
|
cls,
|
|
survey_id: int,
|
|
user_input_id: int,
|
|
error: Exception,
|
|
error_type: str = 'unknown',
|
|
context_data: Optional[Dict[str, Any]] = None
|
|
):
|
|
"""
|
|
Log certificate generation failure with full error context.
|
|
|
|
Args:
|
|
survey_id: ID of the survey
|
|
user_input_id: ID of the user input record
|
|
error: The exception that occurred
|
|
error_type: Type of error (e.g., 'validation', 'conversion', 'libreoffice')
|
|
context_data: Additional context information
|
|
"""
|
|
base_context = {
|
|
'survey_id': survey_id,
|
|
'user_input_id': user_input_id,
|
|
'error_type': error_type,
|
|
'error_class': error.__class__.__name__,
|
|
'error_message': str(error),
|
|
'timestamp': datetime.now().isoformat()
|
|
}
|
|
|
|
if context_data:
|
|
base_context.update(context_data)
|
|
|
|
context = cls._format_context(base_context)
|
|
|
|
_logger.error(
|
|
'=== CERTIFICATE GENERATION FAILURE === | %s',
|
|
context,
|
|
exc_info=True
|
|
)
|
|
|
|
# Log the full stack trace for debugging
|
|
_logger.debug(
|
|
'Full stack trace for certificate generation failure:\n%s',
|
|
traceback.format_exc()
|
|
)
|
|
|
|
@classmethod
|
|
def log_libreoffice_call_start(
|
|
cls,
|
|
docx_path: str,
|
|
attempt: int = 1,
|
|
max_attempts: int = 1
|
|
):
|
|
"""
|
|
Log the start of a LibreOffice conversion call.
|
|
|
|
Args:
|
|
docx_path: Path to the DOCX file being converted
|
|
attempt: Current attempt number
|
|
max_attempts: Maximum number of attempts
|
|
"""
|
|
context = cls._format_context({
|
|
'docx_path': docx_path,
|
|
'attempt': attempt,
|
|
'max_attempts': max_attempts,
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
_logger.info(
|
|
'>>> LibreOffice conversion START | %s',
|
|
context
|
|
)
|
|
|
|
@classmethod
|
|
def log_libreoffice_call_success(
|
|
cls,
|
|
docx_path: str,
|
|
pdf_size: int,
|
|
attempt: int = 1,
|
|
duration_ms: Optional[float] = None
|
|
):
|
|
"""
|
|
Log successful LibreOffice conversion.
|
|
|
|
Args:
|
|
docx_path: Path to the DOCX file that was converted
|
|
pdf_size: Size of generated PDF in bytes
|
|
attempt: Attempt number that succeeded
|
|
duration_ms: Conversion duration in milliseconds (optional)
|
|
"""
|
|
context = cls._format_context({
|
|
'docx_path': docx_path,
|
|
'pdf_size_bytes': pdf_size,
|
|
'pdf_size_kb': round(pdf_size / 1024, 2),
|
|
'attempt': attempt,
|
|
'duration_ms': duration_ms,
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
_logger.info(
|
|
'>>> LibreOffice conversion SUCCESS | %s',
|
|
context
|
|
)
|
|
|
|
@classmethod
|
|
def log_libreoffice_call_failure(
|
|
cls,
|
|
docx_path: str,
|
|
error: Exception,
|
|
attempt: int = 1,
|
|
max_attempts: int = 1,
|
|
stdout: Optional[str] = None,
|
|
stderr: Optional[str] = None,
|
|
exit_code: Optional[int] = None
|
|
):
|
|
"""
|
|
Log LibreOffice conversion failure with full subprocess context.
|
|
|
|
Args:
|
|
docx_path: Path to the DOCX file
|
|
error: The exception that occurred
|
|
attempt: Current attempt number
|
|
max_attempts: Maximum number of attempts
|
|
stdout: Standard output from LibreOffice
|
|
stderr: Standard error from LibreOffice
|
|
exit_code: Exit code from LibreOffice process
|
|
"""
|
|
context = cls._format_context({
|
|
'docx_path': docx_path,
|
|
'attempt': attempt,
|
|
'max_attempts': max_attempts,
|
|
'error_class': error.__class__.__name__,
|
|
'error_message': str(error),
|
|
'exit_code': exit_code,
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
_logger.error(
|
|
'>>> LibreOffice conversion FAILURE | %s',
|
|
context
|
|
)
|
|
|
|
if stdout:
|
|
_logger.debug('LibreOffice stdout:\n%s', stdout)
|
|
|
|
if stderr:
|
|
_logger.error('LibreOffice stderr:\n%s', stderr)
|
|
|
|
if exit_code is not None:
|
|
_logger.error('LibreOffice exit code: %d', exit_code)
|
|
|
|
@classmethod
|
|
def log_libreoffice_unavailable(
|
|
cls,
|
|
error_message: str,
|
|
context_data: Optional[Dict[str, Any]] = None
|
|
):
|
|
"""
|
|
Log LibreOffice unavailability.
|
|
|
|
This is a critical error that should trigger administrator notifications.
|
|
|
|
Args:
|
|
error_message: Description of the unavailability issue
|
|
context_data: Additional context information
|
|
"""
|
|
base_context = {
|
|
'error_message': error_message,
|
|
'timestamp': datetime.now().isoformat()
|
|
}
|
|
|
|
if context_data:
|
|
base_context.update(context_data)
|
|
|
|
context = cls._format_context(base_context)
|
|
|
|
_logger.critical(
|
|
'!!! LIBREOFFICE UNAVAILABLE !!! | %s',
|
|
context
|
|
)
|
|
|
|
@classmethod
|
|
def log_template_upload(
|
|
cls,
|
|
survey_id: int,
|
|
filename: str,
|
|
file_size: int,
|
|
is_update: bool = False
|
|
):
|
|
"""
|
|
Log template file upload.
|
|
|
|
Args:
|
|
survey_id: ID of the survey
|
|
filename: Name of the uploaded file
|
|
file_size: Size of the file in bytes
|
|
is_update: Whether this is an update to existing template
|
|
"""
|
|
context = cls._format_context({
|
|
'survey_id': survey_id,
|
|
'filename': filename,
|
|
'file_size_bytes': file_size,
|
|
'file_size_mb': round(file_size / (1024 * 1024), 2),
|
|
'operation': 'update' if is_update else 'create',
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
_logger.info(
|
|
'Template upload | %s',
|
|
context
|
|
)
|
|
|
|
@classmethod
|
|
def log_template_parsing(
|
|
cls,
|
|
survey_id: int,
|
|
placeholder_count: int,
|
|
preserved_mappings: int = 0
|
|
):
|
|
"""
|
|
Log template parsing results.
|
|
|
|
Args:
|
|
survey_id: ID of the survey
|
|
placeholder_count: Number of placeholders found
|
|
preserved_mappings: Number of preserved mappings (for updates)
|
|
"""
|
|
context = cls._format_context({
|
|
'survey_id': survey_id,
|
|
'placeholder_count': placeholder_count,
|
|
'preserved_mappings': preserved_mappings,
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
_logger.info(
|
|
'Template parsing complete | %s',
|
|
context
|
|
)
|
|
|
|
@classmethod
|
|
def log_template_save(
|
|
cls,
|
|
survey_id: int,
|
|
placeholder_count: int,
|
|
is_update: bool = False
|
|
):
|
|
"""
|
|
Log template configuration save.
|
|
|
|
Args:
|
|
survey_id: ID of the survey
|
|
placeholder_count: Number of placeholder mappings
|
|
is_update: Whether this is an update to existing template
|
|
"""
|
|
context = cls._format_context({
|
|
'survey_id': survey_id,
|
|
'placeholder_count': placeholder_count,
|
|
'operation': 'update' if is_update else 'create',
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
_logger.info(
|
|
'Template configuration saved | %s',
|
|
context
|
|
)
|
|
|
|
@classmethod
|
|
def log_template_deletion(
|
|
cls,
|
|
survey_id: int,
|
|
survey_title: str
|
|
):
|
|
"""
|
|
Log template deletion.
|
|
|
|
Args:
|
|
survey_id: ID of the survey
|
|
survey_title: Title of the survey
|
|
"""
|
|
context = cls._format_context({
|
|
'survey_id': survey_id,
|
|
'survey_title': survey_title,
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
_logger.info(
|
|
'Template deleted | %s',
|
|
context
|
|
)
|
|
|
|
@classmethod
|
|
def log_preview_generation(
|
|
cls,
|
|
survey_id: int,
|
|
placeholder_count: int,
|
|
success: bool = True,
|
|
error: Optional[Exception] = None
|
|
):
|
|
"""
|
|
Log preview certificate generation.
|
|
|
|
Args:
|
|
survey_id: ID of the survey
|
|
placeholder_count: Number of placeholders
|
|
success: Whether generation was successful
|
|
error: Exception if generation failed
|
|
"""
|
|
context = cls._format_context({
|
|
'survey_id': survey_id,
|
|
'placeholder_count': placeholder_count,
|
|
'success': success,
|
|
'error': str(error) if error else None,
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
if success:
|
|
_logger.info(
|
|
'Preview generation SUCCESS | %s',
|
|
context
|
|
)
|
|
else:
|
|
_logger.error(
|
|
'Preview generation FAILURE | %s',
|
|
context,
|
|
exc_info=error is not None
|
|
)
|
|
|
|
@classmethod
|
|
def log_validation_error(
|
|
cls,
|
|
operation: str,
|
|
error_message: str,
|
|
context_data: Optional[Dict[str, Any]] = None
|
|
):
|
|
"""
|
|
Log validation errors.
|
|
|
|
Args:
|
|
operation: The operation that failed validation
|
|
error_message: Description of the validation error
|
|
context_data: Additional context information
|
|
"""
|
|
base_context = {
|
|
'operation': operation,
|
|
'error_message': error_message,
|
|
'timestamp': datetime.now().isoformat()
|
|
}
|
|
|
|
if context_data:
|
|
base_context.update(context_data)
|
|
|
|
context = cls._format_context(base_context)
|
|
|
|
_logger.warning(
|
|
'Validation error | %s',
|
|
context
|
|
)
|
|
|
|
@classmethod
|
|
def log_attachment_creation(
|
|
cls,
|
|
user_input_id: int,
|
|
attachment_id: int,
|
|
filename: str,
|
|
file_size: int
|
|
):
|
|
"""
|
|
Log certificate attachment creation.
|
|
|
|
Args:
|
|
user_input_id: ID of the user input record
|
|
attachment_id: ID of the created attachment
|
|
filename: Name of the attachment file
|
|
file_size: Size of the file in bytes
|
|
"""
|
|
context = cls._format_context({
|
|
'user_input_id': user_input_id,
|
|
'attachment_id': attachment_id,
|
|
'filename': filename,
|
|
'file_size_bytes': file_size,
|
|
'file_size_kb': round(file_size / 1024, 2),
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
_logger.info(
|
|
'Certificate attachment created | %s',
|
|
context
|
|
)
|
|
|
|
@classmethod
|
|
def log_data_retrieval(
|
|
cls,
|
|
user_input_id: int,
|
|
fields_populated: int,
|
|
total_fields: int
|
|
):
|
|
"""
|
|
Log certificate data retrieval.
|
|
|
|
Args:
|
|
user_input_id: ID of the user input record
|
|
fields_populated: Number of fields with data
|
|
total_fields: Total number of fields
|
|
"""
|
|
context = cls._format_context({
|
|
'user_input_id': user_input_id,
|
|
'fields_populated': fields_populated,
|
|
'total_fields': total_fields,
|
|
'completion_rate': f"{round((fields_populated / total_fields) * 100, 1)}%",
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
_logger.debug(
|
|
'Certificate data retrieved | %s',
|
|
context
|
|
)
|