Modern QA2026Лучшие практики структурированного логирования
Join

Course06 Observability-Driven Testing

Cutting-edge · Chapter 06

Лучшие практики структурированного логирования

Updated Jul 2026

Почему структурированное логирование важно для QA

Неструктурированные логи читаемы для человека, но враждебны для машин. Когда в 3 часа ночи происходит инцидент в продакшене, вам нужно запрашивать логи программно — фильтровать по сервису, временному диапазону, типу ошибки и ID пользователя. Структурированное логирование выводит события в парсируемом формате (JSON), обеспечивая автоматизированный анализ, фильтрацию и корреляцию, невозможные с логами в свободном тексте.

Неструктурированное vs. структурированное логирование

# BAD: Unstructured -- human-readable, machine-hostile
2026-01-15 14:23:45 ERROR Failed to process order for user john@example.com: timeout after 30s

# GOOD: Structured JSON -- machine-parseable, human-readable with tooling
{
  "timestamp": "2026-01-15T14:23:45.123Z",
  "level": "error",
  "service": "order-service",
  "event": "order_processing_failed",
  "user_id": "usr_42",
  "order_id": "ord_789",
  "error_type": "timeout",
  "timeout_seconds": 30,
  "downstream_service": "payment-service",
  "trace_id": "abc123def456",
  "span_id": "span_789",
  "environment": "production",
  "version": "2.4.1"
}

Структурированная версия позволяет делать запросы вида:

  • «Показать все ошибки таймаута за последний час для зависимости payment-service»
  • «Подсчитать ошибки по типам для пользователя usr_42»
  • «Коррелировать эту ошибку с трассировкой abc123def456, чтобы увидеть полный поток запроса»

Реализация структурированного логирования (Python)

# structured_logging_setup.py
import structlog
import logging

# Configure structlog for JSON output
structlog.configure(
    processors=[
        structlog.contextvars.merge_contextvars,      # thread-safe context
        structlog.processors.add_log_level,           # add "level" field
        structlog.processors.StackInfoRenderer(),     # include stack traces
        structlog.dev.set_exc_info,                   # attach exception info
        structlog.processors.TimeStamper(fmt="iso"),  # ISO 8601 timestamps
        structlog.processors.JSONRenderer(),          # output as JSON
    ],
    wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
    context_class=dict,
    logger_factory=structlog.PrintLoggerFactory(),
)

log = structlog.get_logger()

def process_order(user_id: str, order_id: str, items: list):
    # Bind context that will appear in ALL subsequent log entries
    log_ctx = log.bind(user_id=user_id, order_id=order_id, item_count=len(items))

    log_ctx.info("order_processing_started")

    try:
        total = calculate_total(items)
        log_ctx.info("order_total_calculated", total_cents=total)

        payment_result = charge_payment(user_id, total)
        log_ctx.info("payment_processed",
                     payment_id=payment_result.id,
                     payment_method=payment_result.method)

        reserve_inventory(items)
        log_ctx.info("inventory_reserved")

        log_ctx.info("order_processing_completed", duration_ms=elapsed())

    except PaymentTimeoutError as e:
        log_ctx.error("payment_timeout",
                      downstream_service="payment-service",
                      timeout_seconds=e.timeout,
                      retry_count=e.retries)
        raise

    except InsufficientInventoryError as e:
        log_ctx.warning("inventory_insufficient",
                        missing_items=e.missing_items,
                        available=e.available)
        raise

Пример на Node.js/TypeScript

// structured-logger.ts
import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label }),
  },
  timestamp: pino.stdTimeFunctions.isoTime,
  base: {
    service: 'order-service',
    version: process.env.APP_VERSION,
    environment: process.env.NODE_ENV,
  },
});

export function processOrder(userId: string, orderId: string, items: Item[]) {
  const orderLog = logger.child({ userId, orderId, itemCount: items.length });

  orderLog.info('order_processing_started');

  try {
    const total = calculateTotal(items);
    orderLog.info({ totalCents: total }, 'order_total_calculated');

    const payment = chargePayment(userId, total);
    orderLog.info({ paymentId: payment.id, method: payment.method }, 'payment_processed');

    orderLog.info('order_processing_completed');
  } catch (err) {
    orderLog.error({ err, downstreamService: 'payment-service' }, 'order_processing_failed');
    throw err;
  }
}

Обязательные поля для каждой записи лога

Поле Обязательно? Назначение
timestamp Да Когда произошло событие (ISO 8601)
level Да Серьёзность (debug, info, warn, error)
service Да Какой сервис сгенерировал лог
event Да Машиночитаемое имя события (snake_case)
trace_id Да Корреляция с распределёнными трассировками
span_id Рекомендуется Корреляция с конкретным спаном трассировки
environment Рекомендуется production, staging, development
version Рекомендуется Версия приложения для корреляции с деплоями
user_id Когда доступно Атрибуция пользователя (хешировано для соответствия PII)
request_id Рекомендуется Корреляция в рамках одного запроса

Правила и запреты логирования для тестируемости

Делайте Не делайте
Логируйте на каждом значимом переходе состояния Логируйте сырые тела запросов/ответов (риск PII)
Включайте идентификаторы корреляции (trace_id, request_id) Используйте строковую интерполяцию для сообщений лога
Используйте единообразные имена событий между сервисами Логируйте на уровне DEBUG в продакшене
Включайте данные о времени для операций Логируйте секреты, токены или пароли
Разделяйте бизнес-события от технических Создавайте логи, полезные только при разработке
Используйте структурированные поля для каждого переменного значения Встраивайте значения в строку сообщения

Антипаттерн: значения в строках сообщений

# BAD -- values embedded in string, impossible to query
log.info(f"User {user_id} placed order {order_id} for ${total}")

# GOOD -- values as structured fields, queryable
log.info("order_placed", user_id=user_id, order_id=order_id, total_cents=total)

Тестирование на основе логов

Структурированные логи позволяют использовать мощный паттерн тестирования: утверждения на основе вывода логов для проверки поведения системы:

# test_order_logging.py
import json

def test_order_flow_produces_expected_logs(capsys, order_service):
    """Verify the order flow logs all expected events."""
    order_service.process_order("user_1", "order_1", [{"sku": "A", "qty": 1}])

    captured = capsys.readouterr()
    log_lines = [json.loads(line) for line in captured.out.strip().split('\n')]

    # Verify expected events are logged in order
    events = [log["event"] for log in log_lines]
    assert "order_processing_started" in events
    assert "order_total_calculated" in events
    assert "payment_processed" in events
    assert "inventory_reserved" in events
    assert "order_processing_completed" in events

    # Verify context is propagated
    for log in log_lines:
        assert log["user_id"] == "user_1"
        assert log["order_id"] == "order_1"

    # Verify timing data is present
    completed = next(l for l in log_lines if l["event"] == "order_processing_completed")
    assert "duration_ms" in completed
    assert completed["duration_ms"] > 0

Архитектура агрегации логов

[Service A]     [Service B]     [Service C]
  stdout          stdout          stdout
    |               |               |
    v               v               v
[Log Collector: Fluentd / Fluent Bit / Vector]
    |
    v
[Log Storage: Elasticsearch / Loki / CloudWatch Logs]
    |
    v
[Query UI: Kibana / Grafana / CloudWatch Insights]
    |
    v
[Alerting: based on log patterns and frequency]

Рекомендации по инструментам

Компонент Рекомендуется Альтернатива
Сбор Fluent Bit (низкое потребление ресурсов) Fluentd, Vector
Хранение Grafana Loki (экономичный) Elasticsearch, CloudWatch
Запросы Grafana (LogQL) Kibana (KQL), CloudWatch Insights
Алертинг Grafana Alerts ElastAlert, CloudWatch Alarms

PII в логах: проблема тестирования

QA-архитекторы должны проверять, что продакшен-логи не содержат персонально идентифицируемую информацию (PII). Добавьте автоматическое обнаружение PII в ваш пайплайн логов:

# pii_log_scanner.py
import re

PII_PATTERNS = {
    "email": re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"),
    "phone": re.compile(r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b"),
    "ssn": re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),
    "credit_card": re.compile(r"\b(?:\d{4}[-\s]?){3}\d{4}\b"),
}

def scan_log_entry(log_json: dict) -> list:
    """Scan a structured log entry for PII."""
    findings = []
    text = json.dumps(log_json)

    for pii_type, pattern in PII_PATTERNS.items():
        matches = pattern.findall(text)
        real_matches = [m for m in matches if "example" not in m and "test" not in m]
        if real_matches:
            findings.append({"type": pii_type, "count": len(real_matches)})

    return findings

Структурированное логирование — это не просто удобство для разработчиков, а предпосылка для каждой другой практики наблюдаемости в этой главе. Инвестируйте в него на ранних этапах.