<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Abramov blog</title><link>http://abramov.blog/</link><description>Abramov blog</description><language>ru-ru</language><atom:link href="http://abramov.blog/" rel="self" type="application/rss+xml"/><item><title>Audit: практичное audit-логирование для Go</title><link>http://abramov.blog/audit/</link><pubDate>Sun, 18 Jan 2026 00:00:00 +0000</pubDate><guid>http://abramov.blog/audit/</guid><description> Версия на английском: dev.to
Audit-логирование почти всегда появляется позже, чем нужно. Сначала хватает обычных логов, потом внезапно становится важно понять, кто именно поменял статус заказа, почему у пользователя пропала роль или откуда взялись странные данные в системе.
В большинстве проектов audit либо размазывается по бизнес-коду, либо пытается жить поверх обычного логгера без четкой модели. Я вынес эту задачу в отдельную библиотеку: audit.
Репозиторий: https://github.com/w0rng/audit
Что делает audit # Библиотека решает конкретную задачу: хранить историю изменений сущностей.
На уровне модели это выглядит так:
сущность идентифицируется строковым ключом (order:12345, user:42) каждое событие имеет автора, описание и timestamp изменения фиксируются по полям значения могут быть видимыми или скрытыми Никакой магии, рефлексии и автоматических diff. Все изменения описываются явно.
Базовый сценарий # Простейший пример: жизненный цикл заказа.
logger := audit.New() logger.Create( &amp;amp;#34;order:12345&amp;amp;#34;, &amp;amp;#34;john.doe&amp;amp;#34;, &amp;amp;#34;Order created&amp;amp;#34;, map[string]audit.Value{ &amp;amp;#34;status&amp;amp;#34;: audit.PlainValue(&amp;amp;#34;pending&amp;amp;#34;), &amp;amp;#34;total&amp;amp;#34;: audit.PlainValue(99.99), &amp;amp;#34;payment_token&amp;amp;#34;: audit.HiddenValue(), }, ) Чувствительные данные явно помечаются как скрытые. Они участвуют в событии, но не попадают в логи в открытом виде.
Дальнейшие изменения выглядят так же декларативно:
logger.Update( &amp;amp;#34;order:12345&amp;amp;#34;, &amp;amp;#34;warehouse.system&amp;amp;#34;, &amp;amp;#34;Order shipped&amp;amp;#34;, map[string]audit.Value{ &amp;amp;#34;status&amp;amp;#34;: audit.PlainValue(&amp;amp;#34;shipped&amp;amp;#34;), &amp;amp;#34;tracking_number&amp;amp;#34;: audit.PlainValue(&amp;amp;#34;TRK123456789&amp;amp;#34;), }, ) История изменений и события # Библиотека предоставляет два уровня чтения данных.
Полная история изменений сущности:
logs := logger.Logs(&amp;amp;#34;order:12345&amp;amp;#34;) Каждая запись содержит список полей с from и to, автора и описание. Для вышеупомянутых логов данные следующие:
[ { &amp;amp;#34;Fields&amp;amp;#34;: [ { &amp;amp;#34;Field&amp;amp;#34;: &amp;amp;#34;status&amp;amp;#34;, &amp;amp;#34;From&amp;amp;#34;: null, &amp;amp;#34;To&amp;amp;#34;: &amp;amp;#34;pending&amp;amp;#34; }, { &amp;amp;#34;Field&amp;amp;#34;: &amp;amp;#34;total&amp;amp;#34;, &amp;amp;#34;From&amp;amp;#34;: null, &amp;amp;#34;To&amp;amp;#34;: 99.99 }, { &amp;amp;#34;Field&amp;amp;#34;: &amp;amp;#34;payment_token&amp;amp;#34;, &amp;amp;#34;From&amp;amp;#34;: &amp;amp;#34;***&amp;amp;#34;, &amp;amp;#34;To&amp;amp;#34;: &amp;amp;#34;***&amp;amp;#34; } ], &amp;amp;#34;Description&amp;amp;#34;: &amp;amp;#34;Order created&amp;amp;#34;, &amp;amp;#34;Author&amp;amp;#34;: &amp;amp;#34;john.doe&amp;amp;#34;, &amp;amp;#34;Timestamp&amp;amp;#34;: &amp;amp;#34;2026-01-18T17:06:29.126233926+07:00&amp;amp;#34; }, { &amp;amp;#34;Fields&amp;amp;#34;: [ { &amp;amp;#34;Field&amp;amp;#34;: &amp;amp;#34;status&amp;amp;#34;, &amp;amp;#34;From&amp;amp;#34;: &amp;amp;#34;pending&amp;amp;#34;, &amp;amp;#34;To&amp;amp;#34;: &amp;amp;#34;shipped&amp;amp;#34; }, { &amp;amp;#34;Field&amp;amp;#34;: &amp;amp;#34;tracking_number&amp;amp;#34;, &amp;amp;#34;From&amp;amp;#34;: null, &amp;amp;#34;To&amp;amp;#34;: &amp;amp;#34;TRK123456789&amp;amp;#34; } ], &amp;amp;#34;Description&amp;amp;#34;: &amp;amp;#34;Order shipped&amp;amp;#34;, &amp;amp;#34;Author&amp;amp;#34;: &amp;amp;#34;warehouse.system&amp;amp;#34;, &amp;amp;#34;Timestamp&amp;amp;#34;: &amp;amp;#34;2026-01-18T17:06:29.126239744+07:00&amp;amp;#34; } ] Если нужны более легковесные события, можно работать через Events и фильтровать их по полям:
events := logger.Events(&amp;amp;#34;order:12345&amp;amp;#34;, &amp;amp;#34;status&amp;amp;#34;) Это удобно для построения таймлайнов или агрегатов, например цепочки статусов заказа:
[ { &amp;amp;#34;Timestamp&amp;amp;#34;: &amp;amp;#34;2026-01-18T17:06:53.12607001+07:00&amp;amp;#34;, &amp;amp;#34;Action&amp;amp;#34;: &amp;amp;#34;create&amp;amp;#34;, &amp;amp;#34;Author&amp;amp;#34;: &amp;amp;#34;john.doe&amp;amp;#34;, &amp;amp;#34;Description&amp;amp;#34;: &amp;amp;#34;Order created&amp;amp;#34;, &amp;amp;#34;Payload&amp;amp;#34;: { &amp;amp;#34;status&amp;amp;#34;: { &amp;amp;#34;Data&amp;amp;#34;: &amp;amp;#34;pending&amp;amp;#34;, &amp;amp;#34;Hidden&amp;amp;#34;: false }} }, { &amp;amp;#34;Timestamp&amp;amp;#34;: &amp;amp;#34;2026-01-18T17:06:53.126075226+07:00&amp;amp;#34;, &amp;amp;#34;Action&amp;amp;#34;: &amp;amp;#34;update&amp;amp;#34;, &amp;amp;#34;Author&amp;amp;#34;: &amp;amp;#34;warehouse.system&amp;amp;#34;, &amp;amp;#34;Description&amp;amp;#34;: &amp;amp;#34;Order shipped&amp;amp;#34;, &amp;amp;#34;Payload&amp;amp;#34;: { &amp;amp;#34;status&amp;amp;#34;: { &amp;amp;#34;Data&amp;amp;#34;: &amp;amp;#34;shipped&amp;amp;#34;, &amp;amp;#34;Hidden&amp;amp;#34;: false }} } ] Кастомное хранилище # audit не привязывается к способу хранения данных. Для этого используется интерфейс Storage.
Пример простой реализации, которая сохраняет события в JSON-файл, есть в репозитории. Такое хранилище подключается через options pattern:
storage := NewJSONFileStorage(&amp;amp;#34;audit_events.json&amp;amp;#34;) logger := audit.New(audit.WithStorage(storage)) Это может быть файл, база данных, Kafka или любой другой backend. Core-библиотека об этом не знает.
Интеграция с slog # Отдельный пакет audit/slog позволяет подключить audit к стандартному log/slog.
Handler извлекает audit-события из структурированных логов:
handler := auditslog.NewHandler(auditLogger, auditslog.HandlerOptions{ Handler: slog.NewJSONHandler(os.Stdout, &amp;amp;amp;slog.HandlerOptions{ Level: slog.LevelInfo, }), KeyExtractor: auditslog.AttrExtractor(auditslog.AttrEntity), ShouldAudit: func(record slog.Record) bool { return record.Level &amp;amp;gt;= slog.LevelInfo }, }) Обычный лог автоматически становится audit-событием:
slog.Info( &amp;amp;#34;User account created&amp;amp;#34;, auditslog.AttrEntity, &amp;amp;#34;user:123&amp;amp;#34;, auditslog.AttrAction, &amp;amp;#34;create&amp;amp;#34;, auditslog.AttrAuthor, &amp;amp;#34;admin&amp;amp;#34;, &amp;amp;#34;email&amp;amp;#34;, &amp;amp;#34;alice@example.com&amp;amp;#34;, &amp;amp;#34;role&amp;amp;#34;, &amp;amp;#34;editor&amp;amp;#34;, ) Если в записи нет entity или уровень не проходит фильтр, событие не попадет в audit.
Плюс таких логов в том, что они будут видны и в системах сбора логов, и с ними можно работать как с аудит событиями через .Logs, .Events.
Итог # audit это небольшой, изолированный слой для audit-логирования:
явная модель данных контроль над чувствительными полями расширяемое хранилище нативная интеграция со slog Подходит для Go-сервисов, где audit важен как инфраструктурный слой, а не формальность.</description><category>Go</category><category>Audit</category><category>Logging</category><category>Slog</category></item><item><title>Всякие macos хаки</title><link>http://abramov.blog/some-about-macos/</link><pubDate>Sat, 08 Mar 2025 21:45:01 +0700</pubDate><guid>http://abramov.blog/some-about-macos/</guid><description> Иконки для macos # Если нужно установить икноку для приложения/папки на macos, идем на этот сайт: macosicons
Скачиваем иконку и перетаскиваем скаченный файл на иконку в свойствах приложения/папки: Очистить dns кэш на macos # sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder Очистка прав доступа на macos # Иногда бывает, что удаленное приложение остается в разделе «конфеденциальность» и удалить его оттуда не получается.
Можно сбросить все права доступа и тогда оно пропадет:
tccutil reset All ❗❗❗это удалит все разрешения для приложения
Получить заряд macos в терминале # pmset -g batt | grep -Eo &amp;amp;#34;\d+%&amp;amp;#34; | cut -d% -f1 Открытие закладок в safari в новой вкладке # defaults write ~/Library/Preferences/com.apple.Safari TargetedClicksCreateTabs -bool true Хоткей копирования текущего url в сафари # 1. Создание AppleScript в Быстром действии # Открой Automator и выбери Быстрое действие. В верхней части окна рядом с пунктом Workflow receives («Рабочий процесс получает») выбери no input(«без ввода») и Safari в списке приложений. В панели слева в разделе Утилиты выбери и перетащи Запустить AppleScript в рабочую область. Замени текст скрипта на следующий: tell application &amp;amp;#34;Safari&amp;amp;#34; set theURL to URL of current tab of front window set the clipboard to theURL end tell Сохрани быстрое действие с именем, например, Copy URL to Clipboard. 2. Привязка комбинации клавиш к Быстрому действию # Открой System Settings («Системные настройки») и перейди в раздел Keyboard («Клавиатура»). Выбери Keyboard Shortcuts («Сочетания клавиш»), затем Quick Actions («Быстрые действия») в левом меню. Найди созданное быстрое действие Copy URL to Clipboard в списке и добавь к нему сочетание клавиш, например, Command + Shift + C. Теперь, при использовании этой комбинации клавиш в Safari, URL текущей страницы будет копироваться в буфер обмена, аналогично функции в Arc Browser.
Если возник конфликт с существующим хоткеем # Открой System Settings («Системные настройки») и перейди в раздел Keyboard («Клавиатура»). Выбери вкладку Keyboard Shortcuts («Сочетания клавиш»). В левом меню выбери App Shortcuts («Сочетания клавиш приложений»). Нажми на кнопку + для добавления нового сочетания клавиш. В поле Application («Приложение») выбери Safari. В поле Menu Title («Название меню») введи точное название команды — Начать выбор элементов. В поле Keyboard Shortcut («Сочетание клавиш») назначь новое сочетание клавиш, которое не будет конфликтовать с твоей комбинацией Command + Shift + C, или оставь это поле пустым, чтобы отключить команду. Нажми Add («Добавить»).</description><category>Macos</category></item><item><title>Всякие git хаки</title><link>http://abramov.blog/some-about-git/</link><pubDate>Sun, 05 Jan 2025 21:45:01 +0700</pubDate><guid>http://abramov.blog/some-about-git/</guid><description> Разделение настроек # У гита есть 2 вида настроек:
локальные - определяются в .git/config глобальные - определяются либо в .config/git/config, либо в .gitconfig Если нужно сделать глобальные настройки для определнных репозиториев, можно воспользоваться диррективой includeIf. В глобальные настройки гита добавляем следующее:
[includeIf &amp;amp;#34;gitdir:~/Projects/work/&amp;amp;#34;] path = ~/Projects/work/.gitconfig Теперь если мы находимся в дирректории work, будут подключены настройки спецефичные для данных проектов
удалить файл из git repo # git filter-repo -f --index-filter &amp;amp;#39;git rm --cached --ignore-unmatch unwanted-file.txt&amp;amp;#39; после этого надо пушить с форсом:
git push --force -u origin main Перезапись автора коммитов # Перезапись с определенного коммита
git rebase -r --root --exec &amp;amp;#34;git commit --amend --no-edit --reset-author&amp;amp;#34; Перезапись всех коммитов
git rebase -r --root --exec &amp;amp;#34;git commit --amend --no-edit --reset-author ❗ Если во время перезаписи будут происходить какие-то махинации с gitconfig, нужно автора прописать в локальные настройки
Подсчет колличества строк в репозитории # git ls-files | xargs cloc Git home config # alias home=&amp;amp;#39;git --work-tree=$HOME --git-dir=$HOME/.cfg&amp;amp;#39;</description><category>Git</category></item></channel></rss>