---
title: "Логирование"
description: "Логирование go-kit: kitlog и пары ключ-значение, NewJSONLogger и LogfmtLogger, log.With и Valuer, уровни level, ServerFinalizer и endpoint-middleware логирования."
course: "go-kit: микросервисный тулкит"
url: "https://golangmaster.ru/courses/go-kit/lessons/06-logirovanie/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Логирование

*Курс «go-kit: микросервисный тулкит», урок 6 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/go-kit/lessons/06-logirovanie/*

Сервис отвечает и возвращает честные статусы — теперь ему нужны глаза. В go-kit логирование — не printf, а структурные пары ключ-значение из пакета kitlog: один интерфейс Logger, слоёные обёртки log.With, Valuer'ы для времени и идентификаторов запросов, уровни отдельным пакетом level. Ты соберёшь базовый логгер main, напишешь endpoint-middleware логирования — обёртку func(Endpoint) Endpoint из урока 02, которая пишет method, err и время каждого вызова, — и встретишь ServerFinalizer, честное место для лога HTTP-запроса. Ключи «go kit логирование» и «kitlog» станут рабочим инструментом, а не терминами.

### Зачем go-kit начал с логгера

Из всех сквозных функций логирование появилось в курсе первым не случайно: пока у сервиса нет метрик и трейсинга, лог — единственный свидетель о жизни. Но пакет kitlog решает более глубокую задачу, чем вывод строк: он делает событие структурой. Вызов logger.Log("method", "uppercase", "err", err) — это не строка, а набор пар ключ-значение, который форматтер рендерит на выходе как угодно: logfmt для консоли разработчика, JSON для сборщика логов платформы. Причина проста: в микросервисах логи читает не человек, а конвейер — grep по ключу, группировка, алерт; строка «failed to process» для него мусор, а пара "method", "uppercase" — индексируемое поле. Поэтому go kit логирование начинается с вопроса «какие ключи будут у события», а не «что напечатать». Ты уже видел kitlog в уроке 04: kitlog.NewJSONLogger(os.Stdout) и logger.Log("msg", "listening", "addr", ":8080") — сегодня разберём каждую букву этого заклинания и соберём из него настоящий наблюдательный слой.

### Интерфейс Logger: одна строка на весь контракт

Откроем исходники пакета github.com/go-kit/log — весь интерфейс умещается в одну сигнатуру: type Logger interface { Log(keyvals ...interface{}) error }. Это осознанный минимализм в духе endpoint: никаких уровней, имён полей, форматтеров в контракте — только пары. Три следствия, которые надо понять сразу. Первое: пары идут чётно — ключ, значение, ключ, значение; если передать нечётное количество, логгер не упадёт, а подставит в хвост значение ErrMissingValue — константу с текстом (MISSING), и в логе появится громкий маркер ошибки программиста. Второе: Log возвращает error, и по общему соглашению курса его игнорируют: в defer-обёртках пишут _ = logger.Log(...). Падать из-за невозможности записать лог — путь к каскадной аварии. Третье: реализация обязана быть безопасной для конкурентного доступа и не менять переданные keyvals — контракт записан в доке пакета, и на него опирается вся композиция. Один интерфейс, а в результате любой компонент — middleware, транспорт, клиент — принимает log.Logger и не знает, куда именно едет вывод: stdout, файл, syslog или сетевой коллектор.

### Базовый логгер: JSON, logfmt, Nop

```go
import (
        "log"
        "net/http"
        "os"

        "github.com/go-kit/log"
        "github.com/go-kit/log/level"
)

func main() {
        // Форматтер №1: JSON для прод-коллектора.
        var logger log.Logger
        logger = log.NewJSONLogger(os.Stdout)
        // Форматтер №2: logfmt для локальной разработки.
        // logger = log.NewLogfmtLogger(os.Stderr)

        logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller)

        logger.Log("msg", "listening", "addr", ":8080")
        log.Fatal(http.ListenAndServe(":8080", mux))
}
```

Два штатных форматтера kitlog: NewJSONLogger пишет каждое событие объектом, NewLogfmtLogger — пары ключ=значение через пробел, читаемые глазами. Оба НЕ безопасны для конкурентного использования без обёртки log.NewSyncLogger — оберни их, если логгер разделён между горутинами. log.With добавляет ко всем событиям пары ts и caller: DefaultTimestampUTC — это Valuer, который на каждый вызов Log вычисляет свежий RFC3339Nano в UTC, а DefaultCaller — файл и строку вызова.

### log.With и Valuer: слоёные значения, вычисляемые на каждом событии

Главный инструмент kitlog — функция log.With(logger, keyvals...), возвращающая новый Logger с добавленными парами. Слои складываются: базовый With в main задаёт ts и caller, middleware поверх добавляет request_id, и каждая строка лога собирает их все — без передачи логгера по аргументам через весь стек. Пара слов о направлении: With добавляет пары в конец (ближе к твоим данным), WithPrefix — в начало (ближе к ts), и именно WithPrefix используют хелперы пакета level. Значения в With могут быть не константами, а Valuer — функциями func() interface{}; они перевычисляются на каждый вызов Log. Из коробки есть log.Timestamp(t func() time.Time), готовые DefaultTimestamp и DefaultTimestampUTC (обе — RFC3339Nano, вторая в UTC) и DefaultCaller. Свой Valuer пишется за минуту — например, генератор request id, если тот не пришёл в заголовке. Важная граница: слой With — место для контекста вызова (какой сервис, какой компонент пишет), а не для данных конкретного запроса; тело запроса в слой With не кладут никогда, для него есть поля события.

### Endpoint-middleware логирования

```go
func loggingMiddleware(logger log.Logger) endpoint.Middleware {
        return func(next endpoint.Endpoint) endpoint.Endpoint {
                return func(ctx context.Context, request interface{}) (response interface{}, err error) {
                        defer func(begin time.Time) {
                                _ = logger.Log(
                                        "method", "uppercase",
                                        "err", err,
                                        "took", time.Since(begin),
                                )
                        }(time.Now())

                        response, err = next(ctx, request)
                        return response, err
                }
        }
}

// Сборка в main: middleware надевается один раз.
upper := makeUppercaseEndpoint(svc)
upper = loggingMiddleware(logger)(upper)
```

Канон go-kit логирования: лог — это обёртка endpoint'а, а не строка в хендлере. Замыкание помнит logger и next; именованные результаты response и err видны defer'у, который фиксирует время от входа до выхода и пишет событие с err и took. Проверка type assertion на response даст доступ к Err-полю Failer, если хочешь логировать бизнес-ошибки отдельно от err. Middleware надевается в main один раз — на горячем пути запроса только вызов готовой функции.

### Почему логи живут в middleware, а не в сервисе

Вопрос, который тебе зададут на ревью: почему не написать logger.Log прямо в методе сервиса? Ответ — тот же принцип, что и весь курс. Сервис из урока 03 не знает ни о транспорте, ни о форматтерах: суй ему logger — и появится зависимость от инфраструктуры в самом чистом слое, а тесты сервиса потащат моки логгера. Endpoint-middleware — слой между транспортом и endpoint'ом: он видит и request, и response, и err, но остаётся транспорт-агностичным. Повесь один и тот же loggingMiddleware на HTTP-endpoint сегодня и на gRPC-endpoint из урока 13 завтра — логи поедут в обоих случаях, сервис не узнает об этом. Сравни уровни, куда можно вставить лог: декодер/энкодер знают только транспорт, endpoint-middleware знает операцию целиком, service middleware из урока 15 — даже бизнес-контекст, но требует интерфейса сервиса. Правило выбора: лог операции с временем и err — endpoint-middleware; лог бизнес-события («заказ создан») — service middleware; лог транспортных деталей — ServerFinalizer, о котором ниже.

### X-Request-ID и ServerFinalizer: лог HTTP-запроса

В уроке 04 мы обещали: идентификатор запроса появится в уроке про логи. Схема из двух частей. Часть первая — ServerBefore: RequestFunc читает заголовок X-Request-ID (или генерирует новый) и кладёт его в контекст с приватным ключом; теперь значение едет через endpoint в сервис и доступно любому слою. Часть вторая — ServerFinalizer: опция NewServer принимает ServerFinalizerFunc func(ctx context.Context, code int, r *http.Request) и вызывается после того, как ответ уже записан клиенту; это официальное место для лога запроса: method, uri, статус код, длительность, размер ответа. Именно поэтому finalizer'а есть доступ к response headers и размеру: когда опция установлена, Server оборачивает ResponseWriter в interceptingWriter и складывает их в контекст под ключами ContextKeyResponseHeaders и ContextKeyResponseSize. Корреляция собирается в цепочку: клиент шлёт X-Request-ID → ServerBefore кладёт в ctx → endpoint-middleware логирует операцию → finalizer логирует запрос; в платформе логов по одному request_id разворачивается весь путь вызова через все сервисы.

### ServerBefore и ServerFinalizer рядом

```go
type requestIDKey struct{}

func toContext() kithttp.RequestFunc {
        return func(ctx context.Context, r *http.Request) context.Context {
                id := r.Header.Get("X-Request-ID")
                if id == "" {
                        id = fmt.Sprint(rand.Int63()) // учебный генератор
                }
                return context.WithValue(ctx, requestIDKey{}, id)
        }
}

mux.Handle("/uppercase", kithttp.NewServer(
        upper,
        decodeUppercaseRequest,
        encodeResponse,
        kithttp.ServerBefore(toContext()),
        kithttp.ServerFinalizer(func(ctx context.Context, code int, r *http.Request) {
                id, _ := ctx.Value(requestIDKey{}).(string)
                _ = logger.Log("method", r.Method, "uri", r.RequestURI,
                        "status", code, "request_id", id)
        }),
))
```

Слева — RequestFunc из урока 04: условия вызова, а не данные запроса, кладутся в ctx с приватным структурным ключом. Справа — finalizer: он выполнится после записи ответа, поэтому знает финальный статус код. Приватный ключ-структура вместо строки — канон работы с context.Value из урока 02: типобезопасно и без коллизий.

### Уровни: пакет level поверх kitlog

В интерфейсе Logger нет уровней — и это не упущение, а точка расширения. Уровень в go-kit логировании — просто ещё одна пара в событии, по умолчанию ключ level со значениями debug, info, warn, error. Пакет github.com/go-kit/log/level даёт удобные хелперы: level.Error(logger) возвращает логгер с префиксом level=error, и далее level.Info, level.Warn, level.Debug — все они построены на log.WithPrefix над твоим логгером. Зачем нужен ключ, если можно писать "level", "error" руками: чтобы работала вторая половина пакета — фильтр. level.NewFilter(logger, level.AllowInfo()) оборачивает базовый логгер и пропускает только события с уровнем info и выше: debug-записи пишутся в коде всегда, но молчат в проде, где фильтр AllowInfo, и шумят локально, где AllowDebug. Важные дефолты фильтра: без опций он давит ВСЕ leveled-события (собери фильтр с Allow... — иначе ничего не увидишь), а события без уровня проходят мимо фильтра, пока ты не включил опцию SquelchNoLevel. Уровень — не замена тексту, а измеримая размерность: алерт платформы смотрит на rate error-событий, а не парсит слова.

### Уровни в сборке main

```go
var logger log.Logger
logger = log.NewLogfmtLogger(os.Stderr)
logger = level.NewFilter(logger, level.AllowInfo())
logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller)

// Ошибка инфраструктуры:
level.Error(logger).Log("msg", "decode failed", "err", err)
// Обычное событие:
level.Info(logger).Log("msg", "listening", "addr", ":8080")

// Debug-записи пишутся в коде сразу, но молчат под AllowInfo:
level.Debug(logger).Log("msg", "request decoded", "req", req)
```

Порядок слоёв важен: фильтр ставится над форматтером, а With — над фильтром, чтобы ts и caller не вычислялись зря на отфильтрованных событиях. level.Error добавляет pair level=error через WithPrefix, дальше событие идёт сквозь фильтр (error пропущен) и форматтер. Уровень придёт в JSON/logfmt полем — платформа группирует по нему без регулярок.

### Какой лог — из какого места

| Тип события | Где пишется | Что содержит | Уровень |
| --- | --- | --- | --- |
| Жизненный цикл процесса | main до ListenAndServe | msg, addr, версия сборки | info |
| HTTP-запрос целиком | ServerFinalizer | method, uri, status, request_id | info / error по коду |
| Операция сервиса | endpoint-middleware | method, err, took | warn при err != nil |
| Бизнес-событие | service middleware (урок 15) | заказ создан, id, сумма | info |
| Инфраструктурный сбой | ServerErrorHandler / код | err, transport | error |
| Шум отладки | level.Debug где угодно | req, response | debug — фильтруется |

### Чего в логи не класть

Логи — данные, которые ты отправляешь неизвестно куда на годы вперёд, поэтому гигиена важнее многословия. Пароли, токены, заголовок Authorization — никогда, ни в debug, ни при ошибке; запрос с JWT в логе — это слитый доступ. Персональные данные — с осторожностью: имя и почта в логе делают его субъектом GDPR и удорожают хранение; часто достаточно хешированного идентификатора. Тела запросов целиком — только в debug и только на низовых окружениях: строка с телом в error-логе на проде быстро превращается в хранилище личных переписок. Про объём тоже думай: лог на каждый запрос — норма, лог на каждый внутренний цикл — инцидент бюджета; при всплесках помогает семплирование. И последнее — длина ключей: короткие имена (ts, err, took) — конвенция kitlog, они экономят и место, и глаза; главное — договориться об одном словаре ключей на все сервисы платформы, иначе фильтры коллектора будут враждовать с твоими названиями.

> **Нечётный Log и глушение ошибки.** Два грабли на первой неделе с kitlog. Первая: logger.Log("method") — нечётное число аргументов; логгер не упадёт, а подставит (MISSING), и ты найдёшь эту строку в проде через месяц. Вторая: Log возвращает error, и в defer его нельзя оставить без _ = — компилятор потребует объяснений, а необработанный error внутри defer паникой не станет, но линтер и ревью будут недовольны. Пиши _ = logger.Log(...) как идиому курса.

> **Честно о запуске кода.** Урок требует установленного Go и модуля github.com/go-kit/kit — в песочнице курса сторонние модули не исполняются, поэтому интерактивы построены на чтении кода. Локально собери stringsvc, замени логгер на LogfmtLogger с фильтром AllowInfo и уровневыми вызовами, дёрни сервис curl'ом и найди в выводе пары level=, ts= и caller= — три маркера того, что слои собраны правильно.

**Вопрос:** logger.Log("method", "uppercase", "err") — что окажется в логе?

- Ошибка компиляции: Log принимает только чётное число аргументов
- ✅ Пара err=(MISSING) — ErrMissingValue подставлен за отсутствующее значение
- Паника в рантайме на нечётной длине keyvals
- Пара err= — пустая строка вместо значения

Сигнатура Log(keyvals ...interface{}) компилируется с любым числом аргументов, но нечётная длина ловится в рантайме: kitlog добавляет ErrMissingValue — константу с текстом (MISSING). Это осознанный выбор: лог должен выйти даже из кривого вызова, но с громким маркером, который легко найти поиском.

**Вопрос:** Почему loggingMiddleware — это endpoint-middleware, а не строки в методе сервиса?

- Логгер нельзя передать в сервис — компилятор запретит
- ✅ Так лог работает на любом транспорте и не тащит зависимость от инфраструктуры в чистый слой
- В сервисе недоступно время вызова time.Now
- Endpoint-middleware выполняется в отдельной горутине — быстрее

Endpoint транспорт-агностичен: одна обёртка логирует и HTTP, и gRPC вызовы, а сервис остаётся без импортов kitlog и без моков логгера в тестах. Время доступно в любом месте — дело в границах слоёв, не в возможностях. Горутин никаких нет: middleware — обычная обёртка функции.

**Вопрос:** logger = level.NewFilter(log.NewLogfmtLogger(os.Stderr), level.AllowInfo()). Что попадёт в вывод?

- Только level.Info — filter пропускает ровно указанный уровень
- ✅ Info, warn, error и события без уровня; debug — молчит
- Ничего: фильтр без опции SquelchNoLevel давит всё
- Только события без уровня — leveled-события давятся по умолчанию

AllowInfo() разрешает info, warn и error — уровни выше или равные указанному; debug отфильтрован. События без пары level проходят, потому что SquelchNoLevel по умолчанию выключен. Полная тишина — это фильтр БЕЗ опций Allow: он давит все leveled-события, и это частая ошибка новичка.

### Порядок пар в слоёном логгере

```go
logger := log.NewJSONLogger(os.Stdout)
logger = log.With(logger, "a", 1)
logger = log.With(logger, "b", 2)
_ = logger.Log("c", 3)
```

**Что выведет программа?**

- ✅ {"a":1,"b":2,"c":3} — слои With накапливаются по порядку
- {"b":2,"a":1,"c":3} — второй With перезаписал первый
- {"c":3,"a":1,"b":2} — новые пары всегда спереди
- {"c":3} — предыдущие With не переживают переприсваивание

Каждый With возвращает новый логгер с накопленными парами: первый With добавил a=1, второй — b=2, и вызов Log дописывает c=3 в конец. Порядок в JSON-выходе именно такой, как накопились слои. Если нужна пара в самом начале — рядом есть WithPrefix; хелперы уровня level.Error/Info построены именно на нём, чтобы level стоял первым в событии.

### Valuer: сколько раз вычислится ts?

```go
logger := log.NewJSONLogger(os.Stdout)
logger = log.With(logger, "ts", log.DefaultTimestampUTC)

_ = logger.Log("msg", "first")
time.Sleep(time.Second)
_ = logger.Log("msg", "second")
```

**Что выведет программа?**

- Оба события с одинаковым ts — время зафиксировано в With
- ✅ Два разных ts с разницей в секунду — Valuer вычисляется на каждый Log
- Один ts и одна пустота — DefaultTimestampUTC одноразовый
- Паника на втором вызове — Valuer нельзя переиспользовать

log.With заменяет Valuer-значения на результаты их вызова при каждом событии — таково поведение bindValues внутри kitlog. DefaultTimestampUTC — это Valuer, возвращающий time.Now().UTC() в формате RFC3339Nano, поэтому события разнесены на секунду. Дефолты процесса фиксируются в With один раз на старте, а время всегда едет через Valuer.

### Собери базовый логгер stringsvc

Восстанови сборку логгера в main. Пропуски — по порядку: форматтер kitlog, слоёная функция, Valuer времени UTC.

```go
var logger log.Logger
logger = log.___(os.Stderr)
logger = log.___(logger, "ts", log.___, "caller", log.DefaultCaller)

_ = logger.Log("msg", "listening", "addr", ":8080")
```

Подсказка: Первый пропуск — конструктор logfmt-форматтера над os.Stderr; второй — функция добавления пар; третий — готовый Valuer с RFC3339Nano в UTC.

Правильные ответы в пропусках: `NewLogfmtLogger`, `With`, `DefaultTimestampUTC`

### Путь события лога одного запроса

Расставь этапы генерации логов для POST /uppercase в правильном порядке: от прихода запроса до последней записи.

Правильный порядок строк:

1. `ListenAndServe принял запрос, mux нашёл Server`
2. `ServerBefore прочитал X-Request-ID и положил его в ctx`
3. `loggingMiddleware зафиксировал begin := time.Now`
4. `endpoint вызвал сервис, ответ вернулся наружу`
5. `defer middleware записал method, err и took`
6. `энкодер записал тело ответа клиенту`
7. `ServerFinalizer записал method, uri, status и request_id`
8. `платформа логов свела обе записи по request_id`

### Термины урока

- **log.Logger** — Интерфейс kitlog: Log(keyvals ...interface{}) error — пары ключ-значение; нечётная длина лечится ErrMissingValue (MISSING).
- **log.With / WithPrefix** — Слоёные обёртки: With добавляет пары в конец события, WithPrefix — в начало; оба возвращают новый Logger, вычисляя Valuer на каждом Log.
- **Valuer** — func() interface{} в позиции значения: перевычисляется на каждое событие — так работают log.Timestamp, DefaultTimestampUTC и DefaultCaller.
- **level.NewFilter** — Обёртка фильтрации уровней: AllowInfo/AllowDebug/... пропускает уровни и выше; без опций давит все leveled-события; SquelchNoLevel управляет безуровневыми.
- **ServerFinalizer** — ServerOption с func(ctx, code int, r): вызывается после записи ответа — официальное место лога запроса; знает статус, заголовки и размер ответа.
- **loggingMiddleware** — Endpoint-middleware с defer и time.Since(begin): пишет method, err, took для каждого вызова endpoint'а — на любом транспорте.

### Что дальше

Сервис обрёл голос: структура события вместо строк, слои With вместо передачи логгера по стеку, уровни через фильтр, request id через ServerBefore и финальный лог запроса через ServerFinalizer. Вернуть контекст обёрток можно в [уроке 02-endpoint](/courses/go-kit/lessons/02-endpoint/), к опциям транспорта — в [уроке 04-http-transport](/courses/go-kit/lessons/04-http-transport/), к кодеку ошибок — в [уроке 05-codeki](/courses/go-kit/lessons/05-codeki/); полная программа — на [странице курса](/courses/go-kit/). Логирование — первый наблюдательный слой; второй — [урок 07-metriki](/courses/go-kit/lessons/07-metriki/): Counter, Gauge и Histogram пакета metrics, prometheus-реализации, латентность в гистограмме и instrumenting middleware рядом с тем loggingMiddleware, который ты только что написал. Третье измерение — трейсинг — ждёт в уроке 08: там запросы получат сквозные спаны, и request id из этого урока окажется их предком.

## Коротко: что запомнить

- kitlog — структурный лог: Logger.Log(keyvals ...interface{}) error; чётные пары, нечётные добираются ErrMissingValue.
- Два штатных форматтера: NewJSONLogger для коллектора и NewLogfmtLogger для консоли; NewNopLogger — тихий заглушка.
- log.With наслаивает пары, Valuer вычисляется на каждый Log: ts и caller — конвенция log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller).
- Лог операции — endpoint-middleware: defer + time.Since пишет method/err/took, работает на любом транспорте, сервис не знает про логгер.
- X-Request-ID кладёт ServerBefore в ctx, финальный лог запроса пишет ServerFinalizer со статусом и request_id.
- Уровни — пара level=key из пакета level: level.Error(logger) для события, level.NewFilter(logger, level.AllowInfo()) для прод-фильтра.
- В логи не едут секреты и персональные данные; словарь ключей — единый на платформу.

## Домашнее задание

Возьми stringsvc из урока 05 и надень на него полный лог-контур. Первое: собери базовый логгер — LogfmtLogger для локали, фильтр level.AllowInfo, слой With с ts и caller, и убедись, что каждое событие несёт level, ts и caller. Второе: напиши loggingMiddleware(logger) endpoint.Middleware и повесь его на оба endpoint'а; в логе должны появиться method, err и took — проверь на успешном запросе и на ErrEmpty. Третье: добавь ServerBefore с X-Request-ID (чужой заголовок или генерация) и ServerFinalizer с method, uri, status, request_id; двумя curl-вызовами с одним заголовком добейся, чтобы обе записи — middleware и finalizer — срослись по одному id. Четвёртое: уровняй события — ошибки декодера как level.Error, слушание порта как level.Info, декодирование тела как level.Debug; переключи фильтр между AllowInfo и AllowDebug и опиши разницу одной фразой. Пятое: почисти вывод — ни одного секрета, ни одного тела запроса в error-логе; для этого перепиши место, где err выводится целиком, на err.Error().

## Вопросы и ответы по уроку

### JSON или logfmt — что выбрать для продакшена?

Смотри на потребителя. Коллекторы вроде Elasticsearch и Loki любят JSON: поля парсятся без правил. logfmt удобнее человеку в консоли и дешевле на диске; платформы вроде Heroku проглотят и его. В kitlog смена формата — одна строка в main: NewJSONLogger вместо NewLogfmtLogger, весь остальной код не меняется.

### Почему Log возвращает error, если его принято игнорировать?

Контракт честный: запись может не удаться — диск полон, коллектор недоступен. kitlog не решает за тебя, что делать с этим сбоем, и по умолчанию курс рекомендует _ = logger.Log(...): падать из-за невозможности записать лог хуже, чем остаться без записи. Если важность выше — оберни логгер своим middleware с политикой ретраев или метрикой потерь.

### Логировать на уровне endpoint или сервиса?

Endpoint-middleware — для операций: время, err, request id; он транспорт-агностичен и не требует менять интерфейс сервиса. Service middleware (урок 15) — для бизнес-событий с доменным контекстом. Строки лога в самом сервисе — только когда без них никак; тогда логгер передаётся через конструктор вместе с зависимостями, а не создается на месте.
