survey_custom_certificate_t.../services/certificate_logger.py
2025-11-29 08:46:04 +07:00

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
)