Pull to refresh

Условная буферизация логов в Go

Level of difficultyEasy
Reading time3 min
Views1.4K

В .NET 9 появилась интересная функциональность — Log Buffering, которая позволяет буферизовать логи в памяти и выводить их только при определенных условиях. Меня заинтересовала эта идея, что я решил реализовать аналогичный механизм для Go. Так появился EmitLog — пакет для условной буферизации логов.

Проблема традиционного логирования

Представьте типичный веб-сервис с детальным логированием:

func ProcessPayment(ctx context.Context, paymentID string) error {
    log.Debug("Starting payment processing")
    log.Debug("Validating payment data")
    log.Debug("Checking user balance")
    log.Debug("Connecting to payment gateway")
    log.Info("Payment processed successfully")
    return nil
}

При высокой нагрузке такой сервис генерирует огромное количество логов:

  • 99% успешных транзакций = миллионы ненужных debug-логов

  • Высокие расходы на хранение в системах типа ELK, Datadog

  • Замедление поиска важной информации в море рутинных записей

Но если убрать debug-логи совсем, то при ошибке мы потеряем контекст происходящего.

Решение: условная буферизация

EmitLog решает эту дилемму:

  1. Буферизует все логи запроса в памяти

  2. Анализирует результат выполнения

  3. Решает — сохранить логи или отбросить

// При ошибке — видим полный контекст
[ERROR] Payment failed: insufficient funds
[DEBUG] Starting payment processing
[DEBUG] Validating payment data  
[DEBUG] Checking user balance
[INFO] User balance: $50, required: $100
// При успехе — логи отбрасываются (или сохраняются с вероятностью 5%)

Цепочка контекста логирования

Вот как выглядит полный flow:

HTTP Request → Middleware создает BufferingWriter
                    ↓
              Создает logger с контекстом запроса
                    ↓
              Logger помещается в context.Context
                    ↓
              Handler извлекает logger: GetLoggerFromContext(ctx)
                    ↓
              Все логи пишутся в буфер в памяти
                    ↓
              [Возникла ошибка?]
                 ↙        ↘
              Да           Нет
               ↓            ↓
         Flush всех    [Random < SaveRate?]
           логов          ↙        ↘
                        Да          Нет
                         ↓           ↓
                   Сохранить    Отбросить

Практическое использование

Базовая настройка

func main() {
    // Настраиваем zerolog
    log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
    
    // Конфигурация EmitLog
    config := emitlog.Config{
        SaveRate:         10.0,      // 10% успешных запросов
        BufferingEnabled: true,      
        FlushOnError:     true,      // Всегда при ошибках
        FlushOnWarn:      false,     // Игнорируем warnings
        BufferSize:       64 * 1024, // 64KB на запрос
    }
    
    // Применяем middleware
    handler := emitlog.Middleware(config, os.Stderr)(mux)
    http.ListenAndServe(":8080", handler)
}

Использование в хендлерах

func ProcessOrderHandler(w http.ResponseWriter, r *http.Request) {
    // Извлекаем настроенный logger из контекста
    logger := emitlog.GetLoggerFromContext(r.Context())
    
    logger.Debug().Msg("Parsing order request")
    
    var order Order
    if err := json.NewDecoder(r.Body).Decode(&order); err != nil {
        logger.Error().Err(err).Msg("Failed to parse order")
        http.Error(w, "Bad request", 400)
        return // Все debug-логи будут выведены!
    }
    
    logger.Debug().
        Str("order_id", order.ID).
        Float64("amount", order.Amount).
        Msg("Processing order")
    
    if err := processPayment(order); err != nil {
        logger.Error().Err(err).Msg("Payment failed")
        http.Error(w, "Payment failed", 500)
        return // Видим полный контекст ошибки
    }
    
    logger.Info().Msg("Order processed successfully")
    w.WriteHeader(200)
    // При успехе логи сохранятся только с вероятностью SaveRate
}

Преимущества подхода

1. Снижение объема логов

Традиционное логирование:

  • 1M запросов/день × 10 логов/запрос = 10M записей

  • При SaveRate = 5%: 50K записей + логи всех ошибок

  • Экономия: 95%+ на хранении

2. Полный контекст при ошибках

Когда что-то идет не так, вы видите всю историю запроса:

  • Все debug-сообщения

  • Промежуточные состояния

  • Точную последовательность операций

3. Производительность

  • Снижение I/O: меньше записей на диск/сеть

  • Батчинг: при сбросе логи пишутся одним блоком

4. Гибкая конфигурация

// Development
config.BufferingEnabled = false  // Видим все сразу
config.SaveRate = 100.0          // Сохраняем все

// Staging  
config.SaveRate = 50.0           // Половину для анализа
config.FlushOnWarn = true        // Warnings тоже важны

// Production
config.SaveRate = 5.0            // Только 5% успешных
config.BufferSize = 128 * 1024   // Больше буфер

Когда использовать EmitLog

Идеально подходит для:

  • Высоконагруженных API с детальным логированием

  • Микросервисов с дорогой инфраструктурой логирования

  • Систем, где ошибки требуют полного контекста для отладки

Не подходит для:

  • Критичных аудит-логов (compliance)

  • Реального времени мониторинга

  • Маленьких приложений с небольшим объемом логов

Заключение

Код проекта доступен на GitHub

Буду рад фидбеку и предложениям по развитию проекта!

мой телерамм для связи https://t.me/atsaregorodtsev

Tags:
Hubs:
+10
Comments19

Articles