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

Никакого event sourcing в проекте не было. Готовой истории событий, из которой можно собрать этот 
таймлайн, тоже. Ее предстояло записывать самим.

Я решил не ограничиваться статусами и сразу сделать историю для всех полей сущности. В итоге фичу
так и не запустили, а код остался. Ну не пропадать же добру. Вынес его в отдельную библиотеку
[audit](https://github.com/w0rng/audit).

## А обычных логов не хватит?

Записать «пользователь поменял статус заказа» в обычный лог несложно. Потом можно найти эту запись
в системе сбора логов и посмотреть, что происходило.

Но здесь задача немного другая. Историю надо не просто найти при расследовании какого-то факапа,
а показать в интерфейсе. Причем не всю жизнь сервиса, а изменения конкретной сущности. Или вообще
одного ее поля.

Для этого хочется иметь понятную модель: вот сущность, вот автор изменения, вот поля, которые он
поменял. А не каждый раз восстанавливать происходящее по сообщениям «обновление прошло успешно».

Именно эту задачу и решает `audit`. Не CQRS, не event sourcing, а история изменений.

## Что записываем

Сущность в библиотеке - обычный строковый ключ. Например, `order:12345`. Библиотеке не нужно знать,
что такое заказ, какие у него методы и в какой таблице он лежит.

При создании записываем автора, описание и значения полей:

```go
logger := audit.New()

logger.Create(
    "order:12345",
    "john.doe",
    "Order created",
    map[string]audit.Value{
        "status":        audit.PlainValue("pending"),
        "total":         audit.PlainValue(99.99),
        "payment_token": audit.HiddenValue(),
    },
)
```

При изменении - то же самое, но только с нужными полями:

```go
logger.Update(
    "order:12345",
    "warehouse.system",
    "Order shipped",
    map[string]audit.Value{
        "status":          audit.PlainValue("shipped"),
        "tracking_number": audit.PlainValue("TRK123456789"),
    },
)
```

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

Если честно, я уже не помню, почему изначально отказался от автоматического diff. Возможно, хотел
больше контроля над тем, какие данные туда попадут. Придумывать красивую историю принятия этого
решения не буду.

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

Для чувствительных значений есть `HiddenValue()`. В истории будет видно само поле, но вместо
значения - `***`. Например, можно зафиксировать изменение токена, не записывая сам токен в
аудит-лог.

Понятно, что библиотека не угадывает, какие данные чувствительные. Если передать токен через
`PlainValue`, скрытым он от этого не станет.

## Что потом читаем

Полную историю сущности можно получить через `Logs`:

```go
logs := logger.Logs("order:12345")
```

В каждой записи будут автор, описание, время и изменения полей с `from` и `to`. Для статуса из
примера выше - сначала `null → pending`, потом `pending → shipped`.

Но ради истории статусов не хочется тащить изменения суммы, токенов и всего остального. Поэтому
можно запросить события только по конкретному полю:

```go
events := logger.Events("order:12345", "status")
```

Это как раз исходная задача: получить цепочку изменений статуса и отрисовать ее в интерфейсе. При
этом история остальных полей никуда не девается. Понадобится разобраться, кто поменял сумму, - можно
запросить и ее.

## Где все это хранить

Библиотека не выбирает за вас хранилище. Для этого есть интерфейс `Storage`: реализуете его и
подключаете через `WithStorage`.

Например, в репозитории есть
[простая реализация с JSON-файлом](https://github.com/w0rng/audit/blob/main/examples/custom_storage/main.go):

```go
storage := NewJSONFileStorage("audit_events.json")
logger := audit.New(audit.WithStorage(storage))
```

`NewJSONFileStorage` здесь - функция из примера, а не часть самой библиотеки.

Вместо файла можно использовать базу данных, Kafka или другой backend. Куда именно писать
аудит-логи - решаете вы. Библиотека занимается моделью событий и историей изменений.

## А можно через slog?

Да. Для этого есть отдельный пакет `audit/slog`. Его handler извлекает аудит-события из
структурированных логов. В записи указываются сущность, действие и автор, остальные атрибуты
передают данные:

```go
slog.Info(
    "User account created",
    auditslog.AttrEntity, "user:123",
    auditslog.AttrAction, "create",
    auditslog.AttrAuthor, "admin",
    "email", "alice@example.com",
    "role", "editor",
)
```

Этот пример предполагает, что audit-handler уже подключен к `slog`. Сам по себе вызов `slog.Info`
ничего в библиотеку не запишет.

При настройке handler можно указать, как извлекать ключ сущности и какие записи отправлять в аудит.
Если сущности в записи нет или запись не проходит фильтр, аудит-событие не создается.

Получается, одна запись может остаться обычным логом для системы сбора логов и одновременно стать
событием, с которым можно работать через `Logs` и `Events`.

## Что мне не нравится

Самая неудобная часть для меня - описание полей через `map[string]audit.Value`. Для каждого
изменения приходится руками собирать map со строковыми ключами. Не совсем go-way.

Явность здесь полезна, но записывать все это не так приятно, как хотелось бы. Так что интерфейс
записи - та часть библиотеки, которой я не вполне доволен.

В остальном она делает то, ради чего появилась: позволяет записывать аудит-логи в нужное хранилище и
получать историю изменений сущности или отдельного поля.

Фичу на работе так и не запустили. Но если вам тоже нужно просто показать, кто и когда поменял
статус, а готовой истории событий в проекте нет, возможно, [audit](https://github.com/w0rng/audit)
пригодится.
