← Назад к вопросам

Как внедрить логгер в приложение?

2.0 Middle🔥 202 комментариев
#Observability#Микросервисы и архитектура

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Внедрение логгера в Go-приложение

Внедрение логгера в Go-приложение — это критически важная задача для обеспечения наблюдаемости, отладки и мониторинга. Вот профессиональный подход, который я использую в продакшен-приложениях.

Выбор архитектурного подхода

Существует три основных подхода к внедрению логгера:

  1. Глобальный логгер — простой, но проблематичный для тестирования
  2. Внедрение через параметры функций — чисто, но создаёт загромождение
  3. Внедрение через структуры (Dependency Injection) — наиболее рекомендуемый

Структура с внедрённым логгером

package main

import (
    "context"
    "github.com/sirupsen/logrus"
    "go.uber.org/zap"
)

// Определяем интерфейс логгера
type Logger interface {
    Info(ctx context.Context, msg string, fields ...interface{})
    Error(ctx context.Context, msg string, fields ...interface{})
    Debug(ctx context.Context, msg string, fields ...interface{})
    WithFields(fields map[string]interface{}) Logger
}

// Реализация сервиса с внедрённым логгером
type UserService struct {
    logger Logger
    repo   UserRepository
}

func NewUserService(logger Logger, repo UserRepository) *UserService {
    return &UserService{
        logger: logger,
        repo:   repo,
    }
}

func (s *UserService) CreateUser(ctx context.Context, user *User) error {
    // Логируем начало операции с контекстными полями
    s.logger.Info(ctx, "Creating user", 
        "user_id", user.ID,
        "email", user.Email,
    )
    
    // Бизнес-логика
    err := s.repo.Save(ctx, user)
    if err != nil {
        // Логируем ошибку с дополнительной информацией
        s.logger.Error(ctx, "Failed to create user",
            "error", err,
            "user_id", user.ID,
        )
        return err
    }
    
    s.logger.Info(ctx, "User created successfully")
    return nil
}

Фабрика и конфигурация логгера

package logger

import (
    "os"
    "github.com/sirupsen/logrus"
)

// Конфигурация логгера
type Config struct {
    Level      string `json:"level"`      // debug, info, warn, error
    Format     string `json:"format"`     // json, text
    Output     string `json:"output"`     // stdout, file path
    WithCaller bool   `json:"with_caller"` // включать информацию о вызове
}

// Реализация логгера на основе logrus
type LogrusLogger struct {
    logger *logrus.Entry
}

func NewLogger(cfg Config) (Logger, error) {
    log := logrus.New()
    
    // Устанавливаем уровень логирования
    level, err := logrus.ParseLevel(cfg.Level)
    if err != nil {
        level = logrus.InfoLevel
    }
    log.SetLevel(level)
    
    // Настраиваем формат
    if cfg.Format == "json" {
        log.SetFormatter(&logrus.JSONFormatter{
            DisableTimestamp: false,
            CallerPrettyfier: func(f *runtime.Frame) (string, string) {
                return "", fmt.Sprintf("%s:%d", f.File, f.Line)
            },
        })
    }
    
    // Настраиваем вывод
    if cfg.Output != "stdout" {
        file, err := os.OpenFile(cfg.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
        if err == nil {
            log.SetOutput(file)
        }
    }
    
    // Добавляем caller если нужно
    if cfg.WithCaller {
        log.SetReportCaller(true)
    }
    
    return &LogrusLogger{
        logger: logrus.NewEntry(log),
    }, nil
}

func (l *LogrusLogger) Info(ctx context.Context, msg string, fields ...interface{}) {
    entry := l.withContext(ctx).WithFields(l.fieldsToMap(fields))
    entry.Info(msg)
}

Интеграция с контекстом

// Ключи для контекста
type contextKey string

const (
    loggerKey contextKey = "logger"
    requestIDKey contextKey = "request_id"
)

// Middleware для HTTP-обработчиков
func LoggingMiddleware(logger Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Создаём request ID
            requestID := uuid.New().String()
            
            // Создаём логгер с request ID
            requestLogger := logger.WithFields(map[string]interface{}{
                "request_id": requestID,
                "method":     r.Method,
                "path":       r.URL.Path,
                "ip":         r.RemoteAddr,
            })
            
            // Добавляем в контекст
            ctx := context.WithValue(r.Context(), loggerKey, requestLogger)
            ctx = context.WithValue(ctx, requestIDKey, requestID)
            
            // Продолжаем цепочку
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

// Получение логгера из контекста
func FromContext(ctx context.Context) Logger {
    if logger, ok := ctx.Value(loggerKey).(Logger); ok {
        return logger
    }
    return defaultLogger // возвращаем fallback логгер
}

Практические рекомендации

Структурированное логирование

  • Всегда используйте структурированное логирование (JSON или key-value пары)
  • Добавляйте контекстные поля: request_id, user_id, correlation_id
  • Избегайте чувствительных данных: никогда не логируйте пароли, токены, персональные данные

Уровни логирования

// Правильное использование уровней
logger.Debug(ctx, "Detailed internal state", "state", state)    // Для разработки
logger.Info(ctx, "Business operation", "user_id", userID)       // Бизнес-события
logger.Warn(ctx, "Unexpected situation", "attempt", attempt)    // Нестандартные ситуации
logger.Error(ctx, "Operation failed", "error", err)             // Ошибки, требующие внимания

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

// Mock логгер для тестов
type MockLogger struct {
    Messages []string
}

func (m *MockLogger) Info(ctx context.Context, msg string, fields ...interface{}) {
    m.Messages = append(m.Messages, msg)
}

func TestUserService(t *testing.T) {
    mockLogger := &MockLogger{}
    service := NewUserService(mockLogger, mockRepo)
    
    // Тестируем...
    assert.Contains(t, mockLogger.Messages, "Creating user")
}

Продвинутые техники

  1. Сэмплинг для высоконагруженных эндпоинтов
  2. Асинхронное логирование через буферизованный канал
  3. Интеграция с трейсингом (OpenTelemetry, Jaeger)
  4. Динамическое изменение уровня логирования без перезапуска
  5. Логирование в метрики для алертинга

Распространённые ошибки

  • Игнорирование контекста — теряется связь между логами
  • Слишком много/мало информации — находите баланс
  • Блокирующие операции в основном потоке выполнения
  • Отсутствие ротации логов — риск заполнения диска

Заключение

Правильное внедрение логгера — это не просто техническая задача, а важная часть архитектуры приложения. Используйте интерфейсы, внедрение зависимостей и структурированное логирование. Интегрируйте логгер с контекстом и трейсингом для полной наблюдаемости. Выбирайте проверенные решения как zap для производительности или logrus для удобства, но всегда абстрагируйтесь через собственный интерфейс для гибкости и тестируемости.

Как внедрить логгер в приложение? | PrepBro