# -*- 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 )