analytical_force / src /utils /logger.py
ViniciusKhan's picture
Analytical-Force: API + Docker
8445ed4
Raw
History Blame Contribute Delete
3.53 kB
"""Configuração centralizada de logging do Analytical-Force.
Fornece um logger com saída para console e arquivo (``logs/analytical_force.log``).
Inclui um filtro simples que evita o vazamento acidental de segredos no log.
"""
from __future__ import annotations
import logging
import re
from logging.handlers import RotatingFileHandler
from pathlib import Path
# Diretório de logs na raiz do projeto.
_RAIZ_PROJETO = Path(__file__).resolve().parents[2]
_DIR_LOGS = _RAIZ_PROJETO / "logs"
_ARQUIVO_LOG = _DIR_LOGS / "analytical_force.log"
# Nome do logger raiz do projeto.
_NOME_LOGGER = "analytical_force"
# Padrões que NÃO devem aparecer no log (mascarados como ***).
# Cobre chaves comuns e valores tipo token/secret.
_PADROES_SENSIVEIS = [
re.compile(r"(?i)(password\s*[=:]\s*)(\S+)"),
re.compile(r"(?i)(token\s*[=:]\s*)(\S+)"),
re.compile(r"(?i)(api[_-]?key\s*[=:]\s*)(\S+)"),
re.compile(r"(?i)(secret\s*[=:]\s*)(\S+)"),
re.compile(r"(?i)(authorization:\s*bearer\s+)(\S+)"),
]
class _FiltroSegredos(logging.Filter):
"""Filtro que mascara valores sensíveis nas mensagens de log."""
def filter(self, record: logging.LogRecord) -> bool: # noqa: A003
try:
mensagem = record.getMessage()
except Exception:
return True
original = mensagem
for padrao in _PADROES_SENSIVEIS:
mensagem = padrao.sub(r"\1***", mensagem)
if mensagem != original:
# Substitui args já formatados pela versão mascarada.
record.msg = mensagem
record.args = None
return True
def configurar_logger(nivel: int = logging.INFO) -> logging.Logger:
"""Configura e retorna o logger raiz do projeto.
É idempotente: chamadas repetidas não duplicam handlers.
Args:
nivel: Nível de log (ex.: ``logging.INFO``, ``logging.DEBUG``).
Returns:
Logger configurado.
"""
logger = logging.getLogger(_NOME_LOGGER)
logger.setLevel(nivel)
# Evita adicionar handlers duplicados em re-execuções/imports.
if logger.handlers:
return logger
formato = logging.Formatter(
fmt="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
filtro = _FiltroSegredos()
# Handler de console.
console = logging.StreamHandler()
console.setFormatter(formato)
console.addFilter(filtro)
logger.addHandler(console)
# Handler de arquivo com rotação (best-effort: não quebra se faltar permissão).
try:
_DIR_LOGS.mkdir(parents=True, exist_ok=True)
arquivo = RotatingFileHandler(
_ARQUIVO_LOG, maxBytes=2_000_000, backupCount=5, encoding="utf-8"
)
arquivo.setFormatter(formato)
arquivo.addFilter(filtro)
logger.addHandler(arquivo)
except OSError:
logger.warning("Não foi possível criar o arquivo de log em %s", _ARQUIVO_LOG)
# Não propaga para o root logger para evitar logs duplicados.
logger.propagate = False
return logger
def get_logger(nome: str | None = None) -> logging.Logger:
"""Retorna um logger filho do logger do projeto.
Args:
nome: Sufixo do logger (ex.: ``"salesforce"``). Se None, retorna o raiz.
Returns:
Logger pronto para uso.
"""
# Garante que o logger raiz esteja configurado ao menos uma vez.
configurar_logger()
if nome:
return logging.getLogger(f"{_NOME_LOGGER}.{nome}")
return logging.getLogger(_NOME_LOGGER)