Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 24 из 25 ~18 мин +18 XP

Логирование и трассировка pgx

Запрос съел три секунды, а в логах тишина — типичная ситуация сервиса, у которого pgx работает без наблюдаемости. В пятой версии pgx встроенного логгера нет: вместо него каждое соединение несёт трейсер — объект с двумя хуками, которые вызываются до и после каждого Exec, Query и QueryRow. Этого хватает, чтобы собрать slow query log с порогом в двести миллисекунд, вывести весь SQL в slog на разработке или подключить OpenTelemetry. Разберём интерфейс pgx.QueryTracer и его собратьев для Prepare и Batch, подключение трейсера через конфигурацию пула, готовый пакет tracelog и цену, которую трейсинг берёт в проде.

Содержание урока · 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 своими руками

GO
type 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-окружению и временным включениям по флагу.

Проверка: канал между хуками

Трейсеру нужно показать в лог конца запроса 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

GO
type 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. Какие события попадут в лог?

Цена трейсинга и 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

Восстановите четыре пропуска: парсинг конфига, поле трейсера, структура и уровень.

cfg, err := pgxpool.(dsn) if err != nil { return nil, err } cfg..Tracer = &tracelog.{ Logger: SlogAdapter{Logger: slog.Default()}, LogLevel: tracelog., } return pgxpool.NewWithConfig(ctx, cfg)

Соберите slow query tracer

Расставьте строки хуков трейсера в правильном порядке.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: трассировка pgx

Термин

pgx.QueryTracer

нажмите, чтобы перевернуть
Определение

Интерфейс из двух методов: TraceQueryStart возвращает контекст с данными старта, TraceQueryEnd получает CommandTag и Err. Покрывает Exec, Query и QueryRow.

1 / 6

Песочница урока не исполняет 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: переставьте строки местами и зафиксируйте в комментарии, почему логи исчезли.

Личные заметкиПусто
Хранятся только в вашем браузере (pgx · Логирование и трассировка pgx)

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

Почему трейсер, назначенный после создания пула, не работает?

Трейсер снимается с ConnConfig в момент установления каждого соединения и запоминается в нём. Соединения, созданные до назначения, останутся без трейсера до конца жизни, а новых назначение не коснётся. Канонический порядок: pgxpool.ParseConfig, затем cfg.ConnConfig.Tracer, затем pgxpool.NewWithConfig; допустимо вписать трейсер и в BeforeConnect, который получает конфиг прямо перед подключением.

Как логировать args безопасно?

В проде — не логировать вовсе либо маскировать: tracelog уже обрезает длинные строки, но не чувствительность. Рабочая схема: полный args только на Debug и в dev-окружении, в проде — SQL, длительность и ошибка; при разборе конкретного инцидента уровень поднимают флагом на ограниченное время. Если ПДн всё же нужны, маскируйте поля по ключам или хэшируйте значения.

Чем QueryTracer отличается от хуков pgx v4?

В v4 логирование было встроено: ConnConfig принимал Logger с уровнями, и формат был фиксированным. В v5 драйвер зовёт ваши хуки через интерфейсы трейсеров — формат и назначение выбираете вы, зависимость от конкретной библиотеки логов исчезла. Пакет tracelog при желании возвращает привычное поведение v4, но уже поверх нового механизма.

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

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

MongoDB25 уроков~12 чпрактика25 уроков уже доступны

MongoDB в Go: официальный драйвер mongo-driver v2

Курс MongoDB для Go: подключение, bson-теги, CRUD, фильтры, агрегации, индексы, транзакции и Change Streams. 25 уроков по mongo-driver v2.

  • Подключаться к MongoDB и настраивать клиента
  • Моделировать документы структурами и bson-тегами
  • Делать CRUD, сложные фильтры и проекции
  • Строить агрегации $match/$group/$lookup