Skip to content

Структурированные поля

DD предоставляет 20 типобезопасных конструкторов полей, единый тип Field и опциональный механизм валидации ключей полей для структурированного вывода логов.

Тип Field

Field — тип структурированного поля лога, экспортируемый как псевдоним типа internal.Field:

go
type Field = internal.Field

// Фактическая структура (internal/fields.go)
type Field struct {
    Key   string  // Ключ поля
    Value any     // Значение поля (произвольный тип)
}

Все конструкторы полей возвращают значения Field; форматтер (internal.FormatFields) выводит их в формате Key=Value. Базовые типы (string / числа / bool / time.Duration / time.Time / nil) проходят быстрый путь; «сложные типы» — срезы, массивы, map, struct — fallback'ят к JSON-сериализации (определяется internal.IsComplexValue), прочие типы (например, значения, реализующие интерфейс fmt.Stringer или error) проходят через fmt.Fprint.

Базовые поля

КонструкторСигнатураОписание
Any(key string, value any) FieldПроизвольный тип
String(key, value string) FieldСтрока
Bool(key string, value bool) FieldЛогическое значение
Err(err error) FieldОшибка (ключ фиксирован "error"; при err == nil Value равно nil, иначе err.Error())
ErrWithKey(key string, err error) FieldОшибка с пользовательским ключом (как у Err: при err == nil Value равно nil)
ErrWithStack(err error) FieldОшибка со стеком вызовов (ключ "error", при err == nil Value равно nil; кадры стека фильтруются от runtime/ и собственных кадров dd, захват несёт небольшие накладные расходы)

Числовые поля

КонструкторТипПример
Intintdd.Int("count", 42)
Int8int8dd.Int8("flags", 1)
Int16int16dd.Int16("port", 8080)
Int32int32dd.Int32("code", 200)
Int64int64dd.Int64("id", 123456789)
Uintuintdd.Uint("size", 1024)
Uint8uint8dd.Uint8("level", 3)
Uint16uint16dd.Uint16("year", 2026)
Uint32uint32dd.Uint32("seq", 1000)
Uint64uint64dd.Uint64("hash", 0xABCD)
Float32float32dd.Float32("rate", 0.95)
Float64float64dd.Float64("elapsed", 1.234)

Временные поля

КонструкторСигнатураОписание
Time(key string, value time.Time) FieldВременная метка (форматируется по RFC3339)
Duration(key string, value time.Duration) FieldДлительность (вызывает Duration.String())

Поля ошибок

go
// Стандартное поле ошибки (ключ фиксирован "error", nil error → Value равно nil)
dd.Err(err)

// Пользовательский ключ
dd.ErrWithKey("db_error", err)

// С информацией о стеке вызовов (кадры стека фильтруются от runtime/ и собственных кадров dd)
dd.ErrWithStack(err)

Способы использования

Комбинация с InfoWith

go
dd.InfoWith("Пользователь вошёл",
    dd.String("username", "admin"),
    dd.Time("login_at", time.Now()),
    dd.Bool("mfa", true),
    dd.String("ip", "192.168.1.1"),
)

Цепочные вызовы с WithFields

go
entry := logger.WithFields(
    dd.String("service", "api"),
    dd.Int("pid", os.Getpid()),
)
entry.Info("Сервис запущен")

Добавление к Entry

go
base := logger.WithFields(dd.String("req_id", id))
base.InfoWith("Ответ",
    dd.Int("status", 200),
    dd.Duration("elapsed", took),
    dd.Err(err),
)

Валидация полей

DD предоставляет механизм валидации ключей полей, поддерживающий проверку соглашений об именовании и проверку безопасности (инъекция Log4Shell, атаки гомоглифами, overlong UTF-8). Конфигурация валидации FieldValidationConfig может быть привязана к Config.FieldValidation и применена при конструировании, либо динамически заменена во время выполнения через Logger.SetFieldValidation. При каждом вызове *With для ключа каждого поля вызывается ValidateFieldKey; в режиме Strict при неудаче ошибка протоколируется как лог (сам метод логирования не возвращает error).

FieldValidationMode

Режим валидации, определяет поведение при неудаче валидации.

go
type FieldValidationMode int

const (
    FieldValidationNone   FieldValidationMode = iota // Отключить валидацию (по умолчанию, сокращает все проверки)
    FieldValidationWarn                              // При несоответствии именования записывает один warning-лог
    FieldValidationStrict                            // При несоответствии именования записывает один error-лог
)

Метод String() типа FieldValidationMode возвращает: "none" / "warn" / "strict" (при неизвестном значении — "unknown").

FieldNamingConvention

Соглашение об именовании.

go
type FieldNamingConvention int

const (
    NamingConventionAny         FieldNamingConvention = iota // Принимать любой валидный ключ (по умолчанию)
    NamingConventionSnakeCase                                // snake_case: user_id
    NamingConventionCamelCase                                // camelCase: userId
    NamingConventionPascalCase                               // PascalCase: UserId
    NamingConventionKebabCase                                // kebab-case: user-id
)

Метод String() типа FieldNamingConvention возвращает: "any" / "snake_case" / "camelCase" / "PascalCase" / "kebab-case" (при неизвестном значении — "unknown").

FieldValidationConfig

Конфигурация валидации полей.

go
type FieldValidationConfig struct {
    Mode                     FieldValidationMode    // Режим валидации
    Convention               FieldNamingConvention  // Соглашение об именовании
    AllowCommonAbbreviations bool                   // Разрешить распространённые сокращения (ID, URL, HTTP, JSON и др.)
    EnableSecurityValidation bool                   // Включить проверку безопасности (Log4Shell / гомоглифы / overlong UTF-8)
}

Ловушка нулевого значения

Литерал FieldValidationConfig{} приведёт к EnableSecurityValidation=false, что молча отключит проверку безопасности — предпочитайте конструктор DefaultFieldValidationConfig (он устанавливает этот пункт в true). Кроме того, при Mode == FieldValidationNone выполнение прерывается до проверки безопасности: даже если включён EnableSecurityValidation, она не выполняется.

Предустановленные конфигурации

go
// Конфигурация по умолчанию: отключает валидацию именования, но включает проверку безопасности
func DefaultFieldValidationConfig() *FieldValidationConfig

// Строгий snake_case
func StrictSnakeCaseConfig() *FieldValidationConfig

// Строгий camelCase
func StrictCamelCaseConfig() *FieldValidationConfig

Все три пресета устанавливают AllowCommonAbbreviations=true и EnableSecurityValidation=true; у двух последних Mode=FieldValidationStrict.

ValidateFieldKey

go
func (c *FieldValidationConfig) ValidateFieldKey(key string) error

Проверяет, соответствует ли ключ поля конфигурации. При неудаче возвращает error с описанием причины, при успехе возвращает nil. Если получатель nil или Mode == FieldValidationNone, сразу возвращает nil. Порядок проверки:

  1. Пустой ключ → вернуть "field key cannot be empty"
  2. При включённом EnableSecurityValidation выполняется internal.ValidateFieldKeyStrict (Log4Shell / гомоглифы / overlong UTF-8)
  3. Convention == NamingConventionAny → пропустить проверку именования
  4. Если AllowCommonAbbreviations включён и ключ попадает в таблицу распространённых сокращений (id/url/http/json/jwt и др., либо оканчивается на _id/_url/_uri/_ip/_api) → проходит
  5. Построчная проверка по соглашению: snake_case / camelCase / PascalCase / kebab-case
go
package main

import (
    "fmt"

    "github.com/cybergodev/dd"
)

func main() {
    // Пресет строгого snake_case
    cfg := dd.StrictSnakeCaseConfig()

    if err := cfg.ValidateFieldKey("user_id"); err != nil {
        fmt.Println("user_id:", err)
    } else {
        fmt.Println("user_id OK")
        // Вывод: user_id OK
    }

    if err := cfg.ValidateFieldKey("userId"); err != nil {
        fmt.Println("userId:", err)
        // Вывод: userId: field key "userId" does not match snake_case convention
    }

    // Освобождение от распространённых сокращений: URL не соответствует snake_case, но попадает в таблицу сокращений, поэтому проходит
    if err := cfg.ValidateFieldKey("URL"); err != nil {
        fmt.Println("URL:", err)
    } else {
        fmt.Println("URL OK (освобождение по сокращению)")
        // Вывод: URL OK (освобождение по сокращению)
    }

    // В конфигурации по умолчанию Mode=None, именование не проверяется
    defaultCfg := dd.DefaultFieldValidationConfig()
    if err := defaultCfg.ValidateFieldKey("anyKey"); err != nil {
        fmt.Println("anyKey:", err)
    } else {
        fmt.Println("anyKey OK (Mode=None)")
        // Вывод: anyKey OK (Mode=None)
    }
}

Следующие шаги