Содержание урока · 10 разделов
Медленный запрос живёт в проде, а не на ноутбуке: локально база маленькая, планировщик всегда прав, и всё летает. Первое, что нужно от драйвера, — честный журнал: какой SQL исполнялся, сколько он занял и чем закончился. В pgx v4 для этого существовал встроенный Logger с уровнями, в v5 его убрали и заменили трейсерами — интерфейсом, который приложение реализует само. Так драйвер перестал тащить зависимость от конкретной библиотеки логов: хотите slog, zap, zerolog или спаны OpenTelemetry — реализуете два метода, и события потекут туда, куда скажете. Идея простая до обидного: перед каждым запросом драйвер зовёт хук начала и отдаёт SQL с аргументами, после — хук конца с ошибкой или тегом команды. Между ними живёт весь ваш контроль: замерить время, отфильтровать шум, отрезать персональные данные. Дальше — как этим пользоваться так, чтобы логи помогали, а не превращались в пожар.
QueryTracer: два хука на каждый запрос#
Интерфейс из tracer.go выглядит так: TraceQueryStart(ctx context.Context, conn pgx.Conn, data pgx.TraceQueryStartData) context.Context и TraceQueryEnd(ctx context.Context, conn pgx.Conn, data pgx.TraceQueryEndData). В данных старта — SQL и слайс аргументов; в данных конца — CommandTag с числом затронутых строк и Err. Хук начала возвращает контекст, и именно он приезжает в хук конца: это единственный канал передачи данных между хуками, и через него несут, например, время старта. Дизайн важный: в TraceQueryEndData нет SQL — если хотите логировать запрос целиком в конце, положите его в контекст в начале. Один QueryTracer покрывает Exec, Query и QueryRow: драйвер не различает их, для него это один путь. Трейсер получает conn, и через conn.PgConn().PID() можно достать идентификатор серверного процесса — полезно, когда запрос висит в pg_stat_activity и его нужно срастить с логом.
Подключение делается через конфигурацию, а не через живой пул. Канонический путь: cfg, err := pgxpool.ParseConfig(dsn); затем cfg.ConnConfig.Tracer = myTracer; и только после этого pgxpool.NewWithConfig(ctx, cfg). Причина жёсткая: трейсер снимается с конфига в момент установления каждого соединения и запоминается в нём до конца жизни — соединение, созданное без трейсера, трейсер не получит уже никогда. Поэтому установка после создания пула молча не работает: пул живёт, запросы ходят, логов нет. Запасной путь — колбэк BeforeConnect из четвёртого урока: он получает тот самый конфиг, который сейчас пойдёт в подключение, и может вписать трейсер на лету. А вот AfterConnect поздно: соединение уже установлено, поезд ушёл. pgx.ConnectConfig принимает ConnConfig с тем же полем Tracer, так что одиночное соединение подключается идентично.
Slow query log своими руками
GOtype SlowQueryTracer struct {
Threshold time.Duration
Logger *slog.Logger
}
type queryStart struct {
start time.Time
sql string
}
type queryStartKey struct{}
func (t *SlowQueryTracer) TraceQueryStart(
ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData,
) context.Context {
return context.WithValue(ctx, queryStartKey{},
queryStart{start: time.Now(), sql: data.SQL})
}
func (t *SlowQueryTracer) TraceQueryEnd(
ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData,
) {
d, ok := ctx.Value(queryStartKey{}).(queryStart)
if !ok {
return
}
elapsed := time.Since(d.start)
if data.Err != nil {
t.Logger.Error("query failed",
"sql", d.sql, "time", elapsed, "err", data.Err)
return
}
if elapsed < t.Threshold {
return
}
t.Logger.Warn("slow query",
"sql", d.sql, "time", elapsed,
"rows", data.CommandTag.RowsAffected())
}
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, err
}
cfg.ConnConfig.Tracer = &SlowQueryTracer{
Threshold: 200 * time.Millisecond,
Logger: slog.Default(),
}
pool, err := pgxpool.NewWithConfig(ctx, cfg)Хук старта кладёт в контекст время и SQL — в данных конца SQL нет, поэтому он едет через контекст. Хук конца первым делом разбирает ошибку: она логируется всегда и на уровне Error, независимо от порога. Быстрые успешные запросы молчат, медленные попадают в Warn с длительностью и числом строк. Трейсер вписан в конфиг до создания пула, иначе коннекты родятся без него.
Что писать в лог и что прятать#
Рабочий набор полей короткий: обрезанный SQL, длительность, число затронутых строк, ошибка и идентификатор процесса — PID, полученный через conn.PgConn().PID(), позволяет найти зависший запрос в pg_stat_activity по логу. Аргументы — самая спорная часть. С одной стороны, без args медленный INSERT не воспроизвести; с другой — в аргументах живут персональные данные, токены и пароли, и лог-система с удержанием на месяц превращает отладочное удобство в утечку. Компромисс стандартный: в разработке логируем всё, в проде — SQL без аргументов, либо обрезаем и маскируем значения. Готовые решения экономят: пакет tracelog перед печатью режет строки и байты длиннее шестидесяти четырёх символов с пометкой о размере — история не пухнет на блобах, хотя о чувствительности это не спасает. Вторая ось — объём: на тысяче запросов в секунду лог каждого запроса рождает гигабайты и душит сборщик логов, поэтому в проде пишут только ошибки и slow query log, а полный поток оставляют dev-окружению и временным включениям по флагу.
Трейсер с полным логированием args вкатили на прод и забыли: через месяц аудит обнаружил, что в логах лежат почты, телефоны и refresh-токены из INSERT-ов. Если логи пишутся всегда и целиком, утечка — вопрос времени, когда инфраструктура логов ещё и доступна шире, чем база. Если аргументы нужны для отладки, включайте их флагом на ограниченное время, маскируйте чувствительные поля и не выводите их выше уровня Debug.
Проверка: канал между хуками
Трейсеру нужно показать в лог конца запроса SQL и время старта. Как это делается?
Prepare, batch и несколько трейсеров#
QueryTracer — не единственный интерфейс. В том же tracer.go живут PrepareTracer с методами TracePrepareStart и TracePrepareEnd (данные: имя стейтмента, SQL, флаг AlreadyPrepared), BatchTracer для SendBatch, CopyFromTracer для COPY и ConnectTracer для события установления соединения. Механика единая: драйвер проверяет ваш трейсер на соответствие каждому интерфейсу и зовёт те хуки, которые он реализует. Prepare-события приходят и при явном conn.Prepare, и при неявной подготовке в режимах с кэшем стейтментов: AlreadyPrepared равно true, когда стейтмент взят из кэша — по этим записям видно, как греется кэш. Чтобы повесить несколько трейсеров сразу, поле Tracer одно, но в свежих выпусках v5 есть пакет multitracer: multitracer.New(a, b) собирает из двух трейсеров один, раскладывая их по интерфейсам автоматически. Без него роль свёртки выполняет собственный враппер в десяток строк — писать его или тянуть multitracer, дело вкуса, но два хука slow-трейсера и полный tracelog совместимы без проблем.
Что окажется в логах?
Прочитайте код и выберите, что он выведет:
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
return err
}
cfg := pool.Config().ConnConfig
cfg.Tracer = &SlowQueryTracer{
Threshold: 200 * time.Millisecond,
Logger: slog.Default(),
}
pool.QueryRow(ctx, "SELECT 1").Scan(&v)tracelog: готовый трейсер-логгер#
Писать трейсер с нуля необязательно: в поставке pgx есть пакет github.com/jackc/pgx/v5/tracelog со структурой TraceLog — она реализует сразу QueryTracer, BatchTracer, CopyFromTracer, ConnectTracer и даже трейсеры Acquire и Release пула, так что одним значением закрывается весь цикл жизни соединения. Настройка — два поля: Logger и LogLevel. Уровни свои, от LogLevelTrace и LogLevelDebug через Info и Warn до LogLevelError и LogLevelNone. Успешный запрос логируется на Info с полями sql, args, time и commandTag, ошибка — на Error с полем err, Prepare — отдельным сообщением с alreadyPrepared, acquire и release пула — на Debug. Интерфейс Logger из tracelog описывает один метод Log(ctx, level, msg, data map[string]any), и адаптер на slog — это switch по уровню плюс разворачивание карты в пары ключ-значение. Уровень LogLevelInfo покажет каждый запрос; LogLevelError превратит tracelog в тихого сторожа только для сбоев.
tracelog с адаптером на slog
GOtype SlogAdapter struct{ Logger *slog.Logger }
func (a SlogAdapter) Log(ctx context.Context,
level tracelog.LogLevel, msg string, data map[string]any,
) {
var lvl slog.Level
switch level {
case tracelog.LogLevelTrace, tracelog.LogLevelDebug:
lvl = slog.LevelDebug
case tracelog.LogLevelInfo:
lvl = slog.LevelInfo
case tracelog.LogLevelWarn:
lvl = slog.LevelWarn
default:
lvl = slog.LevelError
}
args := make([]any, 0, len(data)*2)
for k, v := range data {
args = append(args, k, v)
}
a.Logger.Log(ctx, lvl, msg, args...)
}
func newPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, err
}
cfg.ConnConfig.Tracer = &tracelog.TraceLog{
Logger: SlogAdapter{Logger: slog.Default()},
LogLevel: tracelog.LogLevelInfo,
}
return pgxpool.NewWithConfig(ctx, cfg)
}Адаптер транслирует уровни tracelog в уровни slog и разворачивает карту данных в пары для структурированного вывода. TraceLog назначен в конфиг до NewWithConfig — теперь на уровне Info в лог капает каждый запрос со временем и тегом команды, а ошибки уходят на Error автоматически. Порог verbosity настраивается одним полем LogLevel, без правки кода трейсера.
Проверка: уровни tracelog
tracelog.TraceLog подключён с LogLevelInfo. Какие события попадут в лог?
Трейсер вписали в колбэк AfterConnect — и неделю ждали логов, которых не было: соединения уже созданы, у каждого свой snapshot конфига. Если трейсер не появился в логах, первым делом проверьте, что он назначен до pgxpool.NewWithConfig, а не после. Колбэк BeforeConnect сработает, AfterConnect — нет; и никакой ошибки в обоих случаях не будет.
Цена трейсинга и OpenTelemetry#
Трейсинг не бесплатен, но дёшев: на каждый запрос — два вызова интерфейса и, если носите данные, один context.WithValue с аллокацией. На фоне сетевого рейса в Postgres это шум, однако на горячих путях с десятками тысяч мелких запросов в секунду даже такой overhead становится измеримым, а полный лог — заметной статьёй расходов на диск и сеть сборщика. Правило то же, что и с args: полный трафик — в разработке, в проде — ошибки и slow query log, полный режим включается флагом на время разбора инцидента. Когда SQL-лог указал на медленный запрос, следующий шаг — EXPLAIN из двадцать второго урока: трейсер даёт кандидата, план объясняет причину. Отдельная ветка — распределённая трассировка: для OpenTelemetry есть готовая обвязка otelpgx от exaring, которая реализует те же pgx-интерфейсы и создаёт спан на каждый запрос; её трейсер ставится в ConnConfig.Tracer так же, как ваш собственный, а при необходимости совмещается с лог-трейсером через multitracer. Логи отвечают на вопрос что тормозило, спаны — где в цепочке вызовов.
Инструменты наблюдаемости pgx#
| Инструмент | Что даёт | Порог и фильтры | Когда брать |
|---|---|---|---|
| Свой QueryTracer | Ровно нужные поля: SQL, время, строки, PID | Любой, например 200 мс | Slow query log в проде, специфичные поля |
| tracelog.TraceLog | Готовые события Query, Prepare, Batch, Connect, Acquire | Уровнем LogLevel | Разработка и отладка: полный трафик запросов |
| otelpgx (exaring) | Спаны OpenTelemetry на запросы | Настраивается опциями пакета | Микросервисы с распределённой трассировкой |
Правила трассировки одним списком#
- Трейсер назначается в cfg.ConnConfig.Tracer до pgxpool.NewWithConfig: после — не работает и не ругается
- TraceQueryEndData не содержит SQL: SQL и время старта едут из хука старта через контекст
- Ошибки логируются всегда, быстрые успешные запросы — никогда
- Args в проде скрыты или замаскированы: там живут ПДн, токены и пароли
- PID из conn.PgConn().PID() сращивает лог с pg_stat_activity
- tracelog покрывает Query, Prepare, Batch, Connect, Acquire и Release одним значением
- Несколько трейсеров совмещаются через multitracer.New или свой враппер
- Полный лог запросов — dev и включения по флагу; в проде постоянны только ошибки и slow log
Соберите подключение tracelog
Восстановите четыре пропуска: парсинг конфига, поле трейсера, структура и уровень.
Соберите slow query tracer
Расставьте строки хуков трейсера в правильном порядке.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: трассировка pgx
Песочница урока не исполняет pgx, поэтому трейсеры гоняются локально: поднимите контейнер Postgres из второго урока, включите tracelog на LogLevelInfo и посмотрите, как на простом SELECT сыплются строки с sql, time и pid, — ощущение полночности наступает быстро. Полезная привычка — сразу завести два порога: Warn на 200 миллисекундах для дежурных алертов и Info на 50 для еженедельного разбора трендов, потому что однажды выставленный порог уезжает от роста трафика и перестаёт ловить деградацию. Дальше по курсу: двадцать второй — индексы и EXPLAIN, куда ведёт каждая строка slow query log; четвёртый — пул и его конфигурация, через которую трейсер и подключается; восемнадцатый — batch-запросы, для наблюдения за которыми существует отдельный BatchTracer из этого урока. Домашнее задание собирает slow-трейсер и tracelog в один пул через multitracer. И общая мысль напоследок: наблюдаемость закладывается до инцидента — трейсер, который вписали после третьего ночного разбирательства, экономит четвёртое.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- pgx v5 логирует через трейсеры: QueryTracer с TraceQueryStart и TraceQueryEnd на каждый Exec, Query, QueryRow.
- Данные между хуками едут через контекст, который возвращает хук старта; SQL в хуке конца отсутствует.
- Трейсер ставится в cfg.ConnConfig.Tracer до создания пула; после — молча не работает.
- Slow query log: время старта в контексте, в конце сравнение с порогом и Warn в slog.
- В лог идут SQL, длительность, число строк, ошибка и PID; args в проде скрывают из-за ПДн и объёма.
- PrepareTracer, BatchTracer, CopyFromTracer и ConnectTracer подключаются тем же полем по интерфейсам.
- tracelog.TraceLog покрывает все события сразу; адаптер на slog — switch по уровню плюс распаковка карты.
- Для OpenTelemetry есть otelpgx от exaring; несколько трейсеров совмещает multitracer.New.
Домашнее задание
Соберите пул с двумя трейсерами через multitracer.New: свой SlowQueryTracer с порогом 200 миллисекунд и tracelog.TraceLog с адаптером на slog на уровне LogLevelInfo. Напишите тест, который выполняет SELECT pg_sleep(0.5) и быстрый SELECT 1, и убедитесь, что первый породил Warn slow query, а второй — только Info-строку tracelog. Затем поднимите порог до пяти секунд и проверьте, что slow-трейсер замолчал, а tracelog продолжил писать. В конце переключите LogLevel на LogLevelError и прогоните запрос с ошибкой — в логе должна остаться одна строка query failed. Проверьте, что трейсер назначен до pgxpool.NewWithConfig: переставьте строки местами и зафиксируйте в комментарии, почему логи исчезли.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему трейсер, назначенный после создания пула, не работает?
Трейсер снимается с ConnConfig в момент установления каждого соединения и запоминается в нём. Соединения, созданные до назначения, останутся без трейсера до конца жизни, а новых назначение не коснётся. Канонический порядок: pgxpool.ParseConfig, затем cfg.ConnConfig.Tracer, затем pgxpool.NewWithConfig; допустимо вписать трейсер и в BeforeConnect, который получает конфиг прямо перед подключением.
Как логировать args безопасно?
В проде — не логировать вовсе либо маскировать: tracelog уже обрезает длинные строки, но не чувствительность. Рабочая схема: полный args только на Debug и в dev-окружении, в проде — SQL, длительность и ошибка; при разборе конкретного инцидента уровень поднимают флагом на ограниченное время. Если ПДн всё же нужны, маскируйте поля по ключам или хэшируйте значения.
Чем QueryTracer отличается от хуков pgx v4?
В v4 логирование было встроено: ConnConfig принимал Logger с уровнями, и формат был фиксированным. В v5 драйвер зовёт ваши хуки через интерфейсы трейсеров — формат и назначение выбираете вы, зависимость от конкретной библиотеки логов исчезла. Пакет tracelog при желании возвращает привычное поведение v4, но уже поверх нового механизма.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
MongoDB в Go: официальный драйвер mongo-driver v2
Курс MongoDB для Go: подключение, bson-теги, CRUD, фильтры, агрегации, индексы, транзакции и Change Streams. 25 уроков по mongo-driver v2.
- Подключаться к MongoDB и настраивать клиента
- Моделировать документы структурами и bson-тегами
- Делать CRUD, сложные фильтры и проекции
- Строить агрегации $match/$group/$lookup