Данный текст описывает единые стандарты логирования для всех компонентов проекта. Соблюдение этих правил обеспечивает консистентность логов, упрощает отладку и мониторинг.
1. Стандарт уровней логирования
Используйте уровни логирования в соответствии с их назначением:
| Уровень | Когда использовать | Пример |
|---|---|---|
logger.debug() |
Отладочная информация, промежуточные вычисления | Генерация токенов, SQL-запросы в dev-режиме |
logger.info() |
Успешные бизнес-события | user_created, payment_confirmed, domain_verified |
logger.warning() |
Ожидаемые проблемы, ошибки валидации | validation_failed, retry_attempt, token_expired |
logger.error() |
Неожиданные ошибки, но обработанные | Сбой внешнего сервиса с fallback-логикой |
logger.exception() |
Только в except-блоках с stacktrace |
Любое исключение, требующее анализа |
Правильно
logger.debug("login_link_generated", uid_prefix=uid[:8], token_prefix=token[:4])
logger.info("user_created", email=email)
logger.warning("domain_validation_failed", reason="empty_domain")
logger.error("email_backend_rejected")
logger.exception("email_send_failed") # внутри except
Неправильно
logger.info("evm_address_validation_failed") # ← Должен быть warning!
logger.exception("error", error=str(e)) # ← вне except-блока
2. Единый формат имён событий
Имена событий должны быть в snake_case с глаголом в прошедшем времени.
Правильно
"user_created"
"payment_processed"
"domain_verified"
"validation_failed"
"link_generated"
"token_expired"
Неправильно
"userCreated" # camelCase
"create_user" # глагол в настоящем времени
"USER_CREATED" # CONSTANT_CASE
"user-create" # kebab-case
3. Стандартизация контекстных полей
Используйте единые имена для одинаковых сущностей во всём проекте.
Стандартные имена полей
| Поле | Тип | Описание |
|---|---|---|
user_id |
UUID | ID пользователя (всегда user_id, не user_pk) |
account_id |
UUID | ID аккаунта |
domain |
str | Домен (без префиксов http/https) |
amount |
str | Сумма (Decimal → str для JSON) |
reason |
str | Причина ошибки |
error |
str | Текст ошибки (только в logger.exception()) |
Правильно
logger.info(
"payment_processed",
user_id=user.id,
account_id=account.id,
amount=str(amount),
reason="invalid_format",
)
Неправильно
logger.info("error", user=user, pk=123, data=some_dict) # ← слишком общо
logger.info("payment", user_pk=pk, sum=amount) # ← неправильные имена
4. Обработка исключений с контекстом
Общие правила
logger.exception()использует только внутриexcept-блоков- Не дублируйте
errorиerror_type— structlog добавит stacktrace автоматически - Не логируйте чувствительные данные (полные токены, пароли, email без маскировки)
Правильно
# Поиск пользователя с обработкой ошибок
try:
user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist) as e:
logger.warning("invalid_uid", uidb64_prefix=uidb64[:8]) # ← только префикс
msg = "Invalid user identification"
raise UserCreationError(msg) from e
# Отправка email с exception()
try:
sent = self._do_send_email(email, login_url)
except Exception as e:
logger.exception("email_send_failed") # ← email уже в contextvars
raise UserCreationError("Failed to send login email") from e
Неправильно
# Избыточные поля в exception()
logger.exception(
"safe_browsing_request_failed",
domain=domain, # ← уже в contextvars
error_type=type(e).__name__, # ← избыточно
error_message=str(e), # ← избыточно
)
# Логирование вне except
logger.exception("some_error", error=str(e)) # ← нет stacktrace!
5. Безопасность: маскировка чувствительных данных
Автоматическая маскировка
В проекте настроен mask_email_processor, который автоматически маскирует email:
logger.info("user_found", email="john.doe@example.com")
# В логе: "email": "j***@example.com"
Ручная маскировка токенов и идентификаторов
Никогда не логируйте полные токены, UID или секреты!
Правильно
logger.debug(
"login_link_generated",
uid_prefix=uid[:8], # ← только префикс
token_prefix=token[:4], # ← только префикс
)
logger.warning("invalid_uid", uidb64_prefix=uidb64[:8])
Неправильно
logger.debug("login_link", link=login_url) # ← содержит полный токен!
logger.info("user_uid", uid=uid) # ← полный UID
logger.debug("token", token=token) # ← полный токен
6. Контекстные переменные (structlog.contextvars)
Используйте structlog.contextvars.bind_contextvars() для автоматического добавления
контекста ко всем последующим логам в рамках запроса/операции.
Правильно
# В начале обработки запроса
structlog.contextvars.bind_contextvars(email=email)
# ... далее все логи автоматически получат email
logger.info("auth_link_requested") # ← будет с email в контексте
logger.info("auth_link_sent") # ← тоже с email
# В сервисе верификации
structlog.contextvars.bind_contextvars(domain=domain, verification_uuid_prefix=verification_uuid[:8])
logger.info("txt_record_found") # ← с domain и verification_uuid_prefix
logger.warning("verification_mismatch") # ← тоже с контекстом
Где добавлять contextvars
| Место | Какие переменные добавлять |
|---|---|
AuthorizationView.form_valid() |
email |
DomainVerificationService.verify_domain() |
raw_domain, clean_domain |
DomainVerificationService.check_txt_record() |
domain, verification_uuid_prefix |
validate_safe_domain() |
domain |
AccountMetricsView.get() |
account_id, account_domain |
7. Кастомные исключения для сервисного слоя
Создавайте иерархию кастомных исключений для каждого сервиса.
Пример структуры
# landing/services.py
class UserCreationError(Exception):
"""Базовое исключение для ошибок создания и управления пользователями."""
pass
# dashboard/services.py
class DomainVerificationError(Exception):
"""Базовое исключение для ошибок верификации домена."""
pass
# payment/services.py (требует создания)
class PaymentProcessingError(Exception):
"""Базовое исключение для ошибок обработки платежей."""
pass
Использование
# Правильно
try:
result = DomainVerificationService.verify_domain(raw_domain, verification_uuid)
except DomainVerificationError as e:
logger.warning("domain_verification_failed", reason=str(e))
messages.error(request, "Domain verification failed")
# Неправильно
try:
result = DomainVerificationService.verify_domain(raw_domain, verification_uuid)
except Exception as e:
logger.exception("error") # ← слишком общо
raise
Шаблон для нового кода
import structlog
logger = structlog.get_logger(__name__)
def process_payment(payment_id: str, amount: Decimal) -> bool:
"""Обработка платежа.
Args:
payment_id: ID платежа.
amount: Сумма платежа.
Returns:
True если платеж успешен.
Raises:
PaymentError: При ошибке обработки.
"""
# 1. Добавляем контекст в начале
structlog.contextvars.bind_contextvars(payment_id=payment_id)
logger.info("payment_processing_started", amount=str(amount))
# 2. Проверяем существование
try:
payment = Payment.objects.get(id=payment_id)
except Payment.DoesNotExist:
logger.warning("payment_not_found")
raise PaymentError("Payment not found")
# 3. Валидация бизнес-логики
if amount <= 0:
logger.warning("payment_invalid_amount", amount=str(amount))
raise PaymentError("Invalid amount")
# 4. Обработка с exception() только в except
try:
payment.process()
except Exception as e:
logger.exception("payment_processing_failed")
raise PaymentError("Processing error") from e
# 5. Успешное завершение
logger.info("payment_processed_successfully")
return True
Чек-лист для код-ревью
Перед мержем проверьте:
- Используется правильный уровень логирования (warning для ошибок валидации)
- Имя события в
snake_caseс глаголом в прошедшем времени - Контекстные переменные добавлены через
bind_contextvars() - В
logger.exception()нет дублирующих полей (error,error_type) - Чувствительные данные замаскированы (email, токены, UID)
- Кастомные исключения используются вместо общего
Exception - Нет полных токенов/секретов в логах (только префиксы)
Инструменты
Проверка через ruff
# Проверка логов на наличие чувствительных данных
ruff check --select LOG .
Локальное тестирование
В DEBUG-режиме логи выводятся в консоль с цветовой разметкой:
docker compose up # логи в stdout
В production логи выводятся в JSON-формате для сбора ELK/Graylog:
# core/settings/logging.py
if DEBUG:
structlog_processors.append(structlog.dev.ConsoleRenderer())
else:
structlog_processors.append(structlog.processors.JSONRenderer())