Логирование в Go: slog и production-практики
Зачем знать: логи — основной инструмент диагностики в production. Middle 1 Go-разработчик в 2026 должен уметь писать структурированные логи, понимать различия между
slog/zap/zerolog, корректно прокидывать контекст (trace ID, request ID) и не сливать в логи sensitive-данные. Без этого debugging в кластере превращается в гадание.
Содержание
Заголовок раздела «Содержание»- Базовая концепция
- Как в Go (с примерами)
- Gotchas
- Best practices в production
- Вопросы на собесе
- Practice
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»Зачем структурированное логирование
Заголовок раздела «Зачем структурированное логирование»Текстовые логи (log.Printf("user %s logged in", id)) — это плоские строки, которые сложно парсить, искать и агрегировать. В production у вас тысячи сервисов и миллионы событий — без структуры (key-value) вы утонете.
Структурированный лог — это запись с явными полями:
{"time":"2026-05-21T10:00:00Z","level":"INFO","msg":"user logged in","user_id":"u-123","ip":"10.0.0.1","trace_id":"abc-def"}Такие записи легко индексировать (ELK, Loki, CloudWatch), искать (level=ERROR AND service=payments) и агрегировать (count by user_id).
Эволюция логгеров в Go
Заголовок раздела «Эволюция логгеров в Go»log(stdlib, исторический) — только plain text, без уровней, без структуры. Для middle 1 в 2026 — не использовать в новых проектах.logrus(sirupsen) — первый популярный structured logger. Сейчас в maintenance mode, низкая производительность. Не использовать в новых проектах.zap(uber-go) — высокопроизводительный, типизированные поля, две версии API (sugared/structured).zerolog(rs/zerolog) — zero-allocation, chain API, JSON only.slog(Go 1.21+, stdlib) — официальный stdlib structured logger. Дефолт в 2026 для нового кода, если нет требований к экстремальной производительности.
Что такое slog
Заголовок раздела «Что такое slog»log/slog появился в Go 1.21 (август 2023). Это официальный structured logger в стандартной библиотеке. Ключевые компоненты:
- Logger — точка входа (
slog.Info,slog.Errorи т.д.). - Handler — отвечает за форматирование и вывод (TextHandler, JSONHandler, custom).
- Record — одна запись (time, level, message, attrs).
- Attr — пара key-value (
slog.String("user_id", id)).
Уровни логирования
Заголовок раздела «Уровни логирования»В slog четыре основных уровня:
Debug— детальная отладка (отключается в prod).Info— нормальные события (запросы, события бизнес-логики).Warn— нештатные ситуации, recoverable.Error— ошибки, требующие внимания.
Можно определить кастомные (slog.Level(-2) для Trace). Дефолтный уровень — Info.
2. Как в Go (с примерами)
Заголовок раздела «2. Как в Go (с примерами)»2.1 Базовый slog
Заголовок раздела «2.1 Базовый slog»package main
import ( "log/slog" "os")
func main() { // Простейший вариант — использовать дефолтный logger slog.Info("server started", "port", 8080, "env", "production") slog.Error("failed to connect db", "err", "timeout", "host", "db-1")
// Вывод в формате текста по умолчанию: // time=... level=INFO msg="server started" port=8080 env=production}2.2 JSON handler — стандарт production
Заголовок раздела «2.2 JSON handler — стандарт production»package main
import ( "log/slog" "os")
func main() { handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, }) logger := slog.New(handler) slog.SetDefault(logger) // делаем дефолтным
slog.Info("request handled", "method", "GET", "path", "/api/users", "status", 200, "duration_ms", 42)}Вывод (одна строка):
{"time":"2026-05-21T10:00:00Z","level":"INFO","msg":"request handled","method":"GET","path":"/api/users","status":200,"duration_ms":42}2.3 Типизированные Attr — быстрее и безопаснее
Заголовок раздела «2.3 Типизированные Attr — быстрее и безопаснее»slog.Info("payment processed", slog.String("user_id", "u-123"), slog.Int64("amount_cents", 9999), slog.Duration("duration", 245*time.Millisecond), slog.Bool("retry", false),)Типизированные Attr избегают reflection и аллокаций по сравнению с any-аргументами.
2.4 logger.With — контекстные поля
Заголовок раздела «2.4 logger.With — контекстные поля»// Логгер с префикс-полями для одного запросаreqLogger := slog.With( slog.String("request_id", "req-abc"), slog.String("user_id", "u-123"),)reqLogger.Info("start handling")reqLogger.Info("db query", "table", "users")reqLogger.Info("done", "status", 200)Все три записи получат request_id и user_id автоматически.
2.5 Groups — вложенная структура
Заголовок раздела «2.5 Groups — вложенная структура»slog.Info("http request", slog.Group("request", slog.String("method", "POST"), slog.String("path", "/login"), ), slog.Group("response", slog.Int("status", 401), slog.String("error", "invalid credentials"), ),)В JSON получится:
{ "msg": "http request", "request": {"method":"POST","path":"/login"}, "response": {"status":401,"error":"invalid credentials"}}2.6 Context propagation
Заголовок раздела «2.6 Context propagation»Чтобы прокидывать request_id/trace_id через context, есть slog.InfoContext:
package main
import ( "context" "log/slog" "os")
type ctxKey string
const requestIDKey ctxKey = "request_id"
// ContextHandler оборачивает базовый handler и подмешивает поля из ctxtype ContextHandler struct { slog.Handler}
func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error { if v, ok := ctx.Value(requestIDKey).(string); ok { r.AddAttrs(slog.String("request_id", v)) } return h.Handler.Handle(ctx, r)}
func main() { base := slog.NewJSONHandler(os.Stdout, nil) logger := slog.New(&ContextHandler{Handler: base}) slog.SetDefault(logger)
ctx := context.WithValue(context.Background(), requestIDKey, "req-42") slog.InfoContext(ctx, "processing", "step", "validate")}Теперь любой вызов slog.InfoContext(ctx, ...) автоматически содержит request_id из контекста.
2.7 Логирование ошибок
Заголовок раздела «2.7 Логирование ошибок»err := doSomething()if err != nil { // Просто как поле slog.Error("operation failed", "err", err)
// Лучше — wrapped error с контекстом wrapped := fmt.Errorf("user=%s: %w", userID, err) slog.Error("operation failed", slog.String("user_id", userID), slog.Any("err", wrapped), )}В JSON-выводе err сериализуется через Error() метод.
Stack trace
Заголовок раздела «Stack trace»slog сам не пишет stack trace. Если нужен — добавляйте вручную:
import "runtime/debug"
slog.Error("panic recovered", slog.Any("err", r), slog.String("stack", string(debug.Stack())),)2.8 Custom Handler — кастомный формат
Заголовок раздела «2.8 Custom Handler — кастомный формат»type PrettyHandler struct { w io.Writer level slog.Level attrs []slog.Attr}
func (h *PrettyHandler) Enabled(_ context.Context, l slog.Level) bool { return l >= h.level}
func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error { buf := fmt.Sprintf("[%s] %s %s", r.Time.Format(time.RFC3339), r.Level, r.Message) r.Attrs(func(a slog.Attr) bool { buf += fmt.Sprintf(" %s=%v", a.Key, a.Value) return true }) _, err := fmt.Fprintln(h.w, buf) return err}
func (h *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return &PrettyHandler{w: h.w, level: h.level, attrs: append(h.attrs, attrs...)}}
func (h *PrettyHandler) WithGroup(name string) slog.Handler { return h }2.9 Source location
Заголовок раздела «2.9 Source location»slog.HandlerOptions{AddSource: true} добавит source поле с файлом/строкой вызова. Полезно для debug, но дороже по производительности.
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ AddSource: true, Level: slog.LevelInfo,})2.10 zap — для тех, кому нужна максимальная производительность
Заголовок раздела «2.10 zap — для тех, кому нужна максимальная производительность»import "go.uber.org/zap"
logger, _ := zap.NewProduction() // structured JSONdefer logger.Sync()
logger.Info("request handled", zap.String("method", "GET"), zap.Int("status", 200), zap.Duration("duration", 42*time.Millisecond),)zap.NewProduction() — JSON, INFO-level, sampling on. zap.NewDevelopment() — pretty text, DEBUG-level.
2.11 zerolog — zero allocation
Заголовок раздела «2.11 zerolog — zero allocation»import "github.com/rs/zerolog"import "github.com/rs/zerolog/log"
func main() { zerolog.TimeFieldFormat = zerolog.TimeFormatUnix log.Info(). Str("user_id", "u-123"). Int("status", 200). Msg("request handled")}zerolog строит JSON напрямую в []byte без interface{}, поэтому почти не аллоцирует.
2.12 Бенчмарк (приблизительные цифры, 2026)
Заголовок раздела «2.12 Бенчмарк (приблизительные цифры, 2026)»| Логгер | ns/op | allocs/op |
|---|---|---|
| zerolog | ~50 | 0 |
| zap (structured) | ~100 | 0-1 |
| slog (JSON) | ~150-200 | 0-2 |
| zap (sugared) | ~300 | 2-3 |
| logrus | ~3000 | 20+ |
Цифры зависят от количества полей и handler. slog ловит большую часть оптимизаций zap, но всё ещё медленнее на ~30-50%.
2.13 Slogger через slog.LogValuer
Заголовок раздела «2.13 Slogger через slog.LogValuer»Можно настроить, как ваш тип будет логироваться:
type User struct { ID string Email string Pass string // sensitive!}
func (u User) LogValue() slog.Value { return slog.GroupValue( slog.String("id", u.ID), slog.String("email", maskEmail(u.Email)), // Pass — никогда! )}
slog.Info("user logged in", "user", user) // Pass не утечёт3. Gotchas
Заголовок раздела «3. Gotchas»3.1 slog.Default() в init() — race
Заголовок раздела «3.1 slog.Default() в init() — race»Если в init() вы делаете slog.SetDefault(...), а другая горутина уже логирует через slog.Default() — будет race. Это редко, но: установите дефолтный logger в main() до запуска любых горутин.
3.2 Нечётное количество ключ-значений
Заголовок раздела «3.2 Нечётное количество ключ-значений»slog.Info("oops", "key1", "value1", "key2") // "key2" без значения!slog распознает это как !BADKEY или повесит ключу значение nil. Поведение зависит от handler. Используйте slog.String(...) если боитесь ошибиться.
3.3 Defer + слишком ранний log.Fatal
Заголовок раздела «3.3 Defer + слишком ранний log.Fatal»defer logger.Sync() // zaplog.Fatalf("boom") // os.Exit(1), defer не вызовется!Используйте os.Exit только когда логи уже flush’нуты или используйте handler с синхронной записью.
3.4 Лог в hot path
Заголовок раздела «3.4 Лог в hot path»Логирование в горячей петле (миллионы вызовов в секунду) даже у zerolog всё ещё аллоцирует кое-что. Если профайлер показывает логи — добавьте sampling:
sampler := zerolog.BasicSampler{N: 100} // 1 из 100logger := log.Sample(&sampler)В slog встроенного sampling нет — пишите свой handler.
3.5 Логирование sensitive-данных
Заголовок раздела «3.5 Логирование sensitive-данных»Никогда не логируйте:
- Пароли, API keys, токены, secrets.
- Полные номера карт (PAN), CVV.
- PII (email, телефон) — маскируйте.
- JWT целиком (там может быть PII).
slog.Info("login attempt", "user", user.Email) // BAD: email — PIIslog.Info("login attempt", "user_id", user.ID) // OK3.6 Логирование больших структур
Заголовок раздела «3.6 Логирование больших структур»slog.Info("got response", "body", string(body)) // body — 10 МБ JSON, ойЛоги попадут в Loki/ELK и съедят квоту. Логируйте размер, sample, hash — но не всё тело.
3.7 slog.Any vs типизированные функции
Заголовок раздела «3.7 slog.Any vs типизированные функции»slog.Info("a", "x", 42) // any → reflectionslog.Info("a", slog.Int("x", 42)) // typed, быстрееРазница ~2-3x. В hot path используйте типизированные Attr.
3.8 Изменение уровня в runtime
Заголовок раздела «3.8 Изменение уровня в runtime»slog не имеет встроенного механизма для динамического изменения уровня. Решение — slog.LevelVar:
var levelVar slog.LevelVarlevelVar.Set(slog.LevelInfo)
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &levelVar})logger := slog.New(handler)
// В рантайме (например, через HTTP endpoint):levelVar.Set(slog.LevelDebug) // включили debug на лету3.9 JSON-handler и time format
Заголовок раздела «3.9 JSON-handler и time format»По умолчанию slog использует time.RFC3339Nano. Если ваш сборщик логов ждёт другой формат — настройте HandlerOptions.ReplaceAttr:
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { if a.Key == slog.TimeKey { return slog.Int64("ts", a.Value.Time().Unix()) } return a },})3.10 stderr vs stdout
Заголовок раздела «3.10 stderr vs stdout»В Kubernetes сборщик логов читает оба, но конвенция:
stdout— нормальные логи.stderr— критические ошибки, паники, fatal.
Если смешать в одном JSONHandler — всё уйдёт в один поток. Это нормально, главное не писать в файл.
4. Best practices в production
Заголовок раздела «4. Best practices в production»4.1 Один логгер, JSON, stdout
Заголовок раздела «4.1 Один логгер, JSON, stdout»func newLogger(env string) *slog.Logger { level := slog.LevelInfo if env == "dev" { level = slog.LevelDebug } handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: level, AddSource: env == "dev", }) return slog.New(handler).With( slog.String("service", "payments"), slog.String("version", buildVersion), slog.String("env", env), )}4.2 Не используйте файлы — пишите в stdout
Заголовок раздела «4.2 Не используйте файлы — пишите в stdout»В Kubernetes (и Docker в целом) контейнер пишет в stdout/stderr, сборщик (fluent-bit, vector) подбирает. Никогда не пишите логи в файл в контейнере — будет fight с rotation, full disk и т.д.
Если запускаете не в контейнере — используйте systemd journal или lumberjack (но это уже в 2026 редкость).
4.3 Уровень: INFO в prod, DEBUG в dev
Заголовок раздела «4.3 Уровень: INFO в prod, DEBUG в dev»if os.Getenv("LOG_LEVEL") == "debug" { levelVar.Set(slog.LevelDebug)}DEBUG в prod = квота кончится за час и счёт за хранилище взлетит.
4.4 Стандартные поля в каждой записи
Заголовок раздела «4.4 Стандартные поля в каждой записи»Обязательно:
service— имя сервиса.version— версия билда.env— окружение (prod/staging/dev).trace_id— OTel trace ID (для корреляции).request_id— идентификатор запроса.
Опционально (по контексту):
user_id,tenant_id,account_id.method,path,status,duration_msдля HTTP.
4.5 Middleware для HTTP
Заголовок раздела «4.5 Middleware для HTTP»func LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() reqID := r.Header.Get("X-Request-ID") if reqID == "" { reqID = uuid.NewString() }
ctx := context.WithValue(r.Context(), requestIDKey, reqID) rw := &statusRecorder{ResponseWriter: w, status: 200}
next.ServeHTTP(rw, r.WithContext(ctx))
slog.InfoContext(ctx, "http request", slog.String("method", r.Method), slog.String("path", r.URL.Path), slog.Int("status", rw.status), slog.Duration("duration", time.Since(start)), slog.String("ip", r.RemoteAddr), ) })}
type statusRecorder struct { http.ResponseWriter status int}
func (sr *statusRecorder) WriteHeader(code int) { sr.status = code sr.ResponseWriter.WriteHeader(code)}4.6 Sampling для high-volume логов
Заголовок раздела «4.6 Sampling для high-volume логов»// Простой sampler — каждую N-ую записьtype SamplingHandler struct { slog.Handler rate int64 cnt atomic.Int64}
func (h *SamplingHandler) Handle(ctx context.Context, r slog.Record) error { if r.Level >= slog.LevelError { return h.Handler.Handle(ctx, r) // ошибки всегда } if h.cnt.Add(1)%h.rate == 0 { return h.Handler.Handle(ctx, r) } return nil}4.7 Корреляция с traces
Заголовок раздела «4.7 Корреляция с traces»При использовании OpenTelemetry:
func traceCtxAttrs(ctx context.Context) []slog.Attr { span := trace.SpanFromContext(ctx) sc := span.SpanContext() if !sc.IsValid() { return nil } return []slog.Attr{ slog.String("trace_id", sc.TraceID().String()), slog.String("span_id", sc.SpanID().String()), }}Подключите через custom Handler — и каждая запись будет иметь trace_id для корреляции в Grafana/Tempo.
4.8 Не логируйте каждый шаг
Заголовок раздела «4.8 Не логируйте каждый шаг»// BADslog.Debug("entering function")slog.Debug("step 1")slog.Debug("step 2")slog.Debug("exiting function")
// GOODctx, span := tracer.Start(ctx, "doWork")defer span.End()// логи — только для важных событийТрассы лучше логов для пошагового профилирования. Логи — для событий.
4.9 Sentry / error tracking
Заголовок раздела «4.9 Sentry / error tracking»Для критичных ошибок дополнительно отправляйте в Sentry/Honeycomb/Datadog:
if err != nil { slog.ErrorContext(ctx, "payment failed", "err", err) sentry.CaptureException(err) // отправит stack + breadcrumbs}4.10 PII masking
Заголовок раздела «4.10 PII masking»func maskEmail(s string) string { at := strings.Index(s, "@") if at <= 1 { return "***" } return s[:1] + "***" + s[at:]}Заверните в slog.LogValuer (см. п. 2.13), чтобы PII никогда не сливалась случайно.
4.11 Структурированные ошибки
Заголовок раздела «4.11 Структурированные ошибки»type AppError struct { Code string Msg string Fields map[string]any}
func (e *AppError) Error() string { return e.Msg }func (e *AppError) LogValue() slog.Value { attrs := []slog.Attr{ slog.String("code", e.Code), slog.String("msg", e.Msg), } for k, v := range e.Fields { attrs = append(attrs, slog.Any(k, v)) } return slog.GroupValue(attrs...)}4.12 Аудит-логи отдельно
Заголовок раздела «4.12 Аудит-логи отдельно»Финансовые/правовые события (списания, согласия, выдачи) — это аудит. Логи могут потеряться (sampling, retention), а аудит должен жить вечно. Делайте отдельный канал: запись в БД, отдельный Kafka-топик, иммутабельное хранилище.
5. Вопросы на собесе
Заголовок раздела «5. Вопросы на собесе»-
Почему
logиз stdlib больше не используется в production? Нет уровней, нет структуры, plain text, сложно парсить, нет context propagation. -
Чем
slogотличается отzapиzerolog?slog— stdlib, кросс-вендорный handler API.zapиzerolog— быстрее (на 30-100%), но сторонние библиотеки. В 2026 для большинства проектовslogдостаточен. -
Что такое handler в
slog? Компонент, который форматирует и пишетRecord. ВстроеныTextHandlerиJSONHandler. Можно писать кастомные (sampling, context-aware, multi-target). -
Как добавить trace_id во все логи запроса? Custom Handler, который читает
trace.SpanFromContext(ctx)и подмешиваетtrace_id/span_idв Record. -
Почему
JSONHandlerпредпочтителен в production? Парсится сборщиками (Loki, ELK, CloudWatch), индексируется по полям, можно искать по конкретным значениям. -
Что такое
slog.LogValuerи зачем он? Интерфейс для кастомной сериализации типа вslog.Value. Применение: маскирование PII, скрытие паролей, форматирование сложных структур. -
Как менять уровень логирования в рантайме? Через
slog.LevelVar— указатель на уровень, который можно атомарно изменить (например, через HTTP endpoint/debug/loglevel). -
Что такое sampling в логах? Зачем? Запись только N% событий, чтобы не утопить систему сбора при peak load. Реализуется кастомным handler-ом или встроено в
zap/zerolog. -
Куда писать логи в Docker/K8s контейнере? stdout/stderr — runtime/сборщик подберёт сам. Не писать в файл внутри контейнера.
-
Чем опасно логирование внутри hot path? Аллокации, синхронизация (lock в handler), I/O. Может прибить latency. Решение: sampling, типизированные
Attr, отдельный async-handler. -
Что такое cardinality в контексте логов? Сколько уникальных значений у поля. Высокая cardinality (user_id, request_id) — нормально для логов, но для метрик — плохо (см. файл про Prometheus).
-
Как не залогировать пароль? Не передавать в
Info/Error. ИспользоватьLogValuerна типе. Code review. Pre-commit hooks с regex. -
Логи vs метрики vs трассы — когда что? Логи — что произошло (события). Метрики — сколько раз (агрегация). Трассы — где и сколько заняло (путь запроса).
-
Что такое correlation ID? Уникальный ID запроса, который пробрасывается через все сервисы (HTTP header
X-Request-IDилиtraceparentдля OTel) — чтобы связать логи разных сервисов. -
Как
slogсериализует ошибки? ЧерезError()метод (по умолчаниюAny.Resolve()распознаётerror). Stack trace надо добавлять вручную (runtime/debug.Stack()). -
Что такое handler chain? Несколько handler-ов, обёрнутых друг в друга: например, SamplingHandler → ContextHandler → JSONHandler. Каждый делает свою работу.
-
Чем
slog.Withотличается отslog.Group?Withсоздаёт новый logger с предустановленными полями (для serie логов).Group— добавляет вложенный объект к одной записи. -
Можно ли использовать
log/slogсzapпод капотом? Да, черезslog.NewHandleradapter (go.uber.org/zap/exp/zapslog). Получаете APIslog, perfzap. -
Что такое log shipping pipeline? Контейнер → stdout → node agent (fluent-bit, vector) → centralized store (Loki, ELK) → UI (Grafana, Kibana).
-
Чем
logrusплох в 2026? Очень много аллокаций (reflection), maintenance mode, синхронные хуки. Для новых проектов —slogилиzap. -
Зачем
AddSource: true? Добавляет file:line в каждый лог. Полезно в dev, но дорого в prod (runtime caller information). -
Что такое structured panic? Recover + лог с полной структурой: error, stack, request_id. Без этого panic ничего не даст в Kibana.
-
Как тестировать логирование? Пишите в
bytes.Bufferчерез JSONHandler, парсите JSON, проверяйте поля.testing/slogtestпакет помогает проверить корректность кастомных handler-ов. -
Что такое log levels в Kubernetes? Конвенция: DEBUG/INFO/WARN/ERROR/FATAL. K8s сам не парсит уровень — это делает сборщик (например, Loki может фильтровать по
levelJSON-поле). -
Чем отличаются
slog.Infoиslog.InfoContext?InfoContextпринимаетcontext.Contextи пробрасывает его в handler — handler может прочитать trace_id, deadline, request_id из ctx.
6. Practice
Заголовок раздела «6. Practice»Упражнение 1: Базовый JSON-logger
Заголовок раздела «Упражнение 1: Базовый JSON-logger»Напишите функцию NewLogger(env string) *slog.Logger, которая:
- В
prod— JSONHandler, level=Info, AddSource=false. - В
dev— TextHandler, level=Debug, AddSource=true. - Всегда добавляет
serviceиversionкак базовые поля.
Упражнение 2: Context-aware handler
Заголовок раздела «Упражнение 2: Context-aware handler»Напишите handler, который из context.Context извлекает request_id и trace_id и добавляет их в каждую запись.
Упражнение 3: PII masking
Заголовок раздела «Упражнение 3: PII masking»Создайте тип User с полями ID, Email, Phone. Реализуйте LogValuer, чтобы email маскировался (a***@example.com), а phone — последние 4 цифры (***1234).
Упражнение 4: HTTP middleware
Заголовок раздела «Упражнение 4: HTTP middleware»Напишите middleware LoggingMiddleware, который логирует метод, путь, статус, длительность каждого запроса.
Упражнение 5: Sampling handler
Заголовок раздела «Упражнение 5: Sampling handler»Реализуйте handler, который пропускает Info/Debug каждые 1 из 100, а Warn/Error — всегда.
Упражнение 6: Динамический уровень
Заголовок раздела «Упражнение 6: Динамический уровень»Сделайте HTTP endpoint /debug/loglevel, который через GET возвращает текущий уровень, через POST устанавливает новый (debug/info/warn/error).
Упражнение 7: Тесты на логирование
Заголовок раздела «Упражнение 7: Тесты на логирование»Напишите тест, который проверяет, что при вызове handlePayment(...) с ошибкой логируется запись с level=ERROR и полем err.
7. Источники
Заголовок раздела «7. Источники»- Официальная документация
log/slog— https://pkg.go.dev/log/slog (2026, актуальная версия). - Go blog: “Structured Logging with slog” — https://go.dev/blog/slog (Jonathan Amsterdam, автор пакета).
- Uber zap — https://github.com/uber-go/zap (если нужна максимальная производительность).
- zerolog — https://github.com/rs/zerolog (zero-allocation logging).
- The Twelve-Factor App: Logs — https://12factor.net/logs (stdout, log streams).
- Google SRE Book: Monitoring Distributed Systems — https://sre.google/sre-book/monitoring-distributed-systems/ (логи vs метрики vs трассы).
- OpenTelemetry Logs spec — https://opentelemetry.io/docs/specs/otel/logs/ (как корректно коррелировать с trace_id).
- Loki best practices — https://grafana.com/docs/loki/latest/best-practices/ (cardinality, structured logging).
- Datadog blog: “Best Practices for Logging in Go” — поиск по году, 2024-2026.
- Kubernetes logging architecture — https://kubernetes.io/docs/concepts/cluster-administration/logging/.