PARA.AI / api /utils /logger.py
Carlex22's picture
Revert "ParaAIV3.1"
1f24745
"""
Sistema de logging customizado para para.AI API v3.0
Configura loggers com rotação de arquivos e formatação padronizada
"""
import logging
import sys
from pathlib import Path
from logging.handlers import RotatingFileHandler
from datetime import datetime
from typing import Optional
# ============================================================================
# FORMATTERS
# ============================================================================
class ColoredFormatter(logging.Formatter):
"""
Formatter com cores para output no console.
"""
# Códigos de cores ANSI
COLORS = {
'DEBUG': '\033[36m', # Cyan
'INFO': '\033[32m', # Green
'WARNING': '\033[33m', # Yellow
'ERROR': '\033[31m', # Red
'CRITICAL': '\033[35m', # Magenta
}
RESET = '\033[0m'
def format(self, record):
"""Formata log com cores."""
# Adicionar cor ao level name
levelname = record.levelname
if levelname in self.COLORS:
record.levelname = f"{self.COLORS[levelname]}{levelname}{self.RESET}"
# Formatar
result = super().format(record)
# Resetar levelname original (para não afetar outros handlers)
record.levelname = levelname
return result
class JSONFormatter(logging.Formatter):
"""
Formatter que gera logs em formato JSON.
Útil para parsing automatizado e ferramentas de log aggregation.
"""
def format(self, record):
"""Formata log como JSON."""
import json
log_data = {
"timestamp": datetime.fromtimestamp(record.created).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
"line": record.lineno,
}
# Adicionar exception info se houver
if record.exc_info:
log_data["exception"] = self.formatException(record.exc_info)
# Adicionar campos extras
if hasattr(record, "extra"):
log_data.update(record.extra)
return json.dumps(log_data)
# ============================================================================
# CONFIGURAÇÃO DE LOGGER
# ============================================================================
def setup_logger(
name: str,
level: Optional[str] = None,
log_file: Optional[str] = None,
enable_console: bool = True,
enable_file: bool = True,
use_json: bool = False,
max_bytes: int = 10485760, # 10MB
backup_count: int = 5
) -> logging.Logger:
"""
Configura e retorna um logger customizado.
Args:
name: Nome do logger (geralmente __name__ do módulo)
level: Nível de log (DEBUG, INFO, WARNING, ERROR, CRITICAL)
log_file: Caminho do arquivo de log (se None, usa padrão)
enable_console: Habilitar output no console
enable_file: Habilitar output em arquivo
use_json: Usar formato JSON nos logs
max_bytes: Tamanho máximo do arquivo de log
backup_count: Número de arquivos de backup a manter
Returns:
Logger configurado
"""
# Criar logger
logger = logging.getLogger(name)
# Evitar duplicação de handlers
if logger.handlers:
return logger
# Determinar nível de log
if level is None:
try:
from api.config import get_settings
settings = get_settings()
level = settings.LOG_LEVEL
except:
level = "DEBUG"
logger.setLevel(getattr(logging, level.upper()))
# ========================================================================
# CONSOLE HANDLER
# ========================================================================
if enable_console:
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.DEBUG)
# Formato para console
if use_json:
console_formatter = JSONFormatter()
else:
console_format = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
console_formatter = ColoredFormatter(
console_format,
datefmt="%Y-%m-%d %H:%M:%S"
)
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)
# ========================================================================
# FILE HANDLER
# ========================================================================
if enable_file:
# Determinar caminho do arquivo
if log_file is None:
try:
from api.config import get_settings
settings = get_settings()
log_dir = Path(settings.LOG_FILE_PATH)
except:
log_dir = Path("./logs")
log_dir.mkdir(parents=True, exist_ok=True)
# Nome do arquivo com data
today = datetime.now().strftime("%Y%m%d")
log_file = log_dir / f"para_ai_{today}.log"
else:
log_file = Path(log_file)
log_file.parent.mkdir(parents=True, exist_ok=True)
# Rotating file handler (rotaciona quando atinge max_bytes)
file_handler = RotatingFileHandler(
log_file,
maxBytes=max_bytes,
backupCount=backup_count,
encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG)
# Formato para arquivo
if use_json:
file_formatter = JSONFormatter()
else:
file_format = "%(asctime)s | %(levelname)-8s | %(name)s | %(funcName)s:%(lineno)d | %(message)s"
file_formatter = logging.Formatter(
file_format,
datefmt="%Y-%m-%d %H:%M:%S"
)
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
# Prevenir propagação para logger pai (evita duplicação)
logger.propagate = False
return logger
# ============================================================================
# LOGGER GLOBAL DA API
# ============================================================================
# Logger principal da API (usado em api/main.py)
api_logger = setup_logger("api", enable_console=True, enable_file=True)
# ============================================================================
# FUNÇÕES AUXILIARES
# ============================================================================
def log_request(method: str, path: str, status_code: int, duration_ms: float):
"""
Loga uma requisição HTTP de forma padronizada.
Args:
method: Método HTTP (GET, POST, etc)
path: Caminho da URL
status_code: Código de status HTTP
duration_ms: Duração da requisição em milissegundos
"""
emoji = "✅" if status_code < 400 else "❌"
api_logger.info(
f"{emoji} {method} {path} - Status: {status_code} - Duration: {duration_ms:.2f}ms"
)
def log_llm_call(
provider: str,
model: str,
tokens: int,
duration_ms: float,
success: bool = True
):
"""
Loga uma chamada ao LLM de forma padronizada.
Args:
provider: Nome do provedor (groq, openai, etc)
model: Nome do modelo usado
tokens: Número de tokens consumidos
duration_ms: Duração da chamada em ms
success: Se a chamada foi bem sucedida
"""
emoji = "🤖" if success else "⚠️"
status = "OK" if success else "ERRO"
api_logger.info(
f"{emoji} LLM Call [{provider}/{model}] - Tokens: {tokens} - "
f"Duration: {duration_ms:.2f}ms - Status: {status}"
)
def log_processing(
processor_name: str,
acordao_id: str,
duration_ms: float,
success: bool = True,
error: Optional[str] = None
):
"""
Loga processamento de acórdão.
Args:
processor_name: Nome do processador
acordao_id: ID do acórdão processado
duration_ms: Duração em ms
success: Se processamento foi bem sucedido
error: Mensagem de erro se houver
"""
emoji = "✅" if success else "❌"
status = "OK" if success else f"ERRO: {error}"
api_logger.info(
f"{emoji} Processor [{processor_name}] - Acórdão: {acordao_id} - "
f"Duration: {duration_ms:.2f}ms - {status}"
)
def log_database_operation(
operation: str,
table: str,
records: int,
duration_ms: float,
success: bool = True
):
"""
Loga operação de banco de dados.
Args:
operation: Tipo de operação (INSERT, UPDATE, SELECT, etc)
table: Nome da tabela
records: Número de registros afetados
duration_ms: Duração em ms
success: Se operação foi bem sucedida
"""
emoji = "💾" if success else "⚠️"
status = "OK" if success else "ERRO"
api_logger.info(
f"{emoji} DB [{operation}] {table} - Records: {records} - "
f"Duration: {duration_ms:.2f}ms - {status}"
)
# ============================================================================
# CONTEXT MANAGER PARA LOGGING DE PERFORMANCE
# ============================================================================
class LogPerformance:
"""
Context manager para logar performance de operações.
Exemplo:
with LogPerformance("processar_acordao", logger):
# código aqui
pass
"""
def __init__(self, operation_name: str, logger: Optional[logging.Logger] = None):
"""
Args:
operation_name: Nome da operação
logger: Logger a usar (se None, usa api_logger)
"""
self.operation_name = operation_name
self.logger = logger or api_logger
self.start_time = None
def __enter__(self):
"""Inicia timer."""
self.start_time = datetime.now()
self.logger.debug(f"🏁 Iniciando: {self.operation_name}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Finaliza timer e loga resultado."""
duration = (datetime.now() - self.start_time).total_seconds() * 1000
if exc_type is None:
self.logger.info(f"✅ Concluído: {self.operation_name} - {duration:.2f}ms")
else:
self.logger.error(
f"❌ Erro em: {self.operation_name} - {duration:.2f}ms - {exc_val}"
)
return False # Não suprime exceção
# ============================================================================
# EXEMPLOS DE USO
# ============================================================================
if __name__ == "__main__":
# Criar logger
logger = setup_logger("test_logger")
# Testar diferentes níveis
logger.debug("Mensagem de DEBUG")
logger.info("Mensagem de INFO")
logger.warning("Mensagem de WARNING")
logger.error("Mensagem de ERROR")
logger.critical("Mensagem de CRITICAL")
# Testar funções auxiliares
log_request("GET", "/api/v1/health", 200, 15.5)
log_llm_call("groq", "llama-3.1-70b", 1500, 850.3)
log_processing("ProcessorMetadados", "acordao_001", 1200.5, True)
log_database_operation("INSERT", "acordaos", 10, 45.2)
# Testar context manager
with LogPerformance("operacao_teste", logger):
import time
time.sleep(0.1) # Simular trabalho
print("\n✅ Logs escritos em ./logs/")