1. Чому гігієна логів важлива
Коли ви тільки починаєте програмувати, логи здаються «розумним fmt.Println»: ними швидко можна підглянути, де ви опинилися, і що лежить у змінній. Але в реальних програмах логи живуть довго, копіюються далеко й читаються не лише вами. Тому «гігієна» — це про акуратність: що писати, куди, в якому обсязі й у якому стилі, щоб потім не довелося червоніти, розбирати інцидент і пояснювати, чому пароль опинився в логах.
Уявіть, що логи — це не особистий щоденник, а дошка оголошень у підʼїзді: ви можете писати корисні речі, але краще не приклеювати туди ксерокопію паспорта й PIN-код банківської картки.
Невелика схема допоможе закріпити, куди розлітаються логи й чому вони рідко залишаються лише на вашому ноутбуці:
flowchart LR
A[Застосунок] -->|stderr: логи| B[Консоль/сервіс]
B --> C[Файл/ротація]
B --> D[Збирач логів]
D --> E[Сховище/пошук]
E --> F[Команда розробки]
E --> G[Підтримка/черговий]
2. Секрети та персональні дані в логах
Що вважається секретом
Коли кажуть «не логуйте секрети», новачок зазвичай киває й думає: «Гаразд, пароль не логуємо». І це хороший початок, але на практиці секретів більше, і вони підступніші. У логах особливо часто спливають токени, ключі API, заголовки авторизації, cookie, значення змінних середовища, «корисні» дампи структур і взагалі все, що зручно надрукувати «на хвилинку», а потім забути видалити.
Окрема категорія — персональні дані. Формально це може бути не «секрет» у криптографічному сенсі, але наслідки витоку бувають нітрохи не менш серйозні: електронна пошта, телефон, адреса, ПІБ, а іноді навіть вміст задач чи нотаток користувача. У нашому навчальному TODO-застосунку, наприклад, текст задачі може бути будь-яким: від «купити молоко» до «зустріч із лікарем о 14:30, діагноз такий-то». І якщо ви логуєте title задачі «для налагодження», то раптово робите дуже погану річ.
Корисно тримати просту таблицю «що робимо з даними»:
| Категорія | Приклади | Що робимо в логах |
|---|---|---|
| Секрети доступу | пароль, API key, Authorization: Bearer ..., ідентифікатор сесії | Не логуємо. Якщо дуже потрібно — маскуємо (редагуємо). |
| Персональні дані | електронна пошта, телефон, адреса, ПІБ, вміст нотаток | Зазвичай не логуємо. Якщо потрібно — логуємо мінімум (наприклад, user_id, але не email). |
| Внутрішні деталі системи | абсолютні шляхи, «сирі» помилки ОС | У логах — можна, але обережно: не перетворюйте їх на витік і не показуйте користувачу. |
| Технічні метрики | тривалість, кількість записів | Логуємо охоче: це безпечно й дуже корисно. |
До речі, сама ідея «користувачу — просте повідомлення, у лог — подробиці» вважається нормальною практикою: людині показуємо дружній текст, а для діагностики зберігаємо реальну причину, щоб розробник міг розібратися. Саме це й ілюструється в прикладі з обробником HTTP-помилок: користувачу повертають зрозумілий Message, а детальну помилку логуємо для розробника.
Маскування секретів
Заборона «взагалі нічого не логувати» звучить безпечно, але веде до іншої крайності: ви перестаєте розуміти, що відбувається в програмі. Тому ми часто використовуємо компроміс — редагування / маскування. Ідея проста: ми не пишемо секрет повністю, але залишаємо шматочок, за яким можна відрізнити один токен від іншого й повʼязати події.
Найпростіший прийом — показувати лише останні 4 символи (або перші 4), а середину замінювати на ***. Це не криптографія, але як гігієнічний мінімум працює.
Приклад маленької функції для маскування:
package main
import "strings"
func maskSecret(s string) string {
if len(s) <= 4 {
return "****"
}
return strings.Repeat("*", len(s)-4) + s[len(s)-4:]
}
Тепер застосуємо це в логах. Припустімо, наш CLI колись отримує токен (наприклад, через env-конфіг, який ви вже зустрічали раніше). Ми не будемо логувати його «як є»:
package main
import (
"log/slog"
"os"
)
func main() {
log := slog.New(slog.NewTextHandler(os.Stderr, nil))
token := os.Getenv("TODO_TOKEN")
log.Info("конфігурацію завантажено", slog.String("token", maskSecret(token)))
}
Тут важливо памʼятати про психологічну пастку: «я залогую токен тільки в Debug». А потім хтось вмикає Debug на проді «на пʼять хвилин», і токени їдуть у централізований збирач. Тому правило просте: секрет не повинен потрапляти в лог на жодному рівні. Якщо вже дуже потрібно щось повʼязати, логуйте не секрет, а безпечний ідентифікатор (наприклад, token_id, якщо він у вас є), або маску.
3. Не шуміти: рівні логів за змістом
Шумні логи — це коли в них багато тексту, але мало змісту. Найнеприємніше в шумі те, що він не виглядає помилкою: програма працює, логи пишуться… просто ніхто їх не читає. А коли стається реальна проблема, ви відкриваєте логи й бачите 50 тисяч рядків “starting”, “processing”, “still processing”, “processing again”, і десь серед них губиться одне справжнє попередження.
Щоб логи не перетворювалися на суцільний шум, у slog є рівні, і ми зобовʼязані користуватися ними за змістом, а не за настроєм. Згадаємо модель рівнів як «гучність події»:
| Рівень | Коли доречний | Типовий зміст |
|---|---|---|
|
налагодження, деталі, кроки алгоритму | «що саме сталося всередині» |
|
нормальні важливі події | «що ми зробили корисного» |
|
дивно, але не аварія | «щось підозріле» |
|
операція не вдалася | «ми не зробили того, що обіцяли» |
Тепер головне: рівень — це фільтр шуму. Якщо ви все пишете як Info, фільтра немає. Якщо ви все пишете як Error, ви самі собі підпалюєте тривожність. Тому дисципліна така: детальну балаканину — у Debug, робочі факти — у Info, підозріле — у Warn, провали — у Error.
І ще один важливий нюанс: якщо ви передаєте помилку далі й логуєте її там, не треба по дорозі логувати кожен крок. Це повʼязано з ідеєю «логувати на межі», щоб не отримувати дублікати, і чудово поєднується з рекомендацією розділяти «повідомлення користувачу» та «подробицю для розробника».
4. Структурні логи: стабільність, ключі та обсяг
Повідомлення має бути стабільним
Дуже хочеться писати логи «по-людськи»: «Ой-ой-ой, щось пішло не так, зараз спробуємо ще раз, тримайтеся!». Але логи читають не лише очима — їх ще шукають, групують, порівнюють, а інколи будують на їх основі статистику. Тому хороші логи мають одну важливу властивість: стабільність.
Стабільне повідомлення — це короткий, повторюваний текст, який класифікує подію, а деталі лежать у полях. Тоді пошук і аналіз стають простішими: ви шукаєте "задачу створено" і бачите всі такі події, а не 20 варіантів «створив задачу», «задачу створено», «створення задачі пройшло успішно», «ура, ми створили задачу».
Порівняйте два підходи. Поганий — бо в тексті все змінюється:
log.Info("задачу створено #42 з назвою 'Купити молоко' за 12ms")
Хороший — бо текст стабільний, а деталі в полях:
log.Info(
"задачу створено",
slog.Int("id", 42),
slog.Int("title_len", 8),
slog.Int("duration_ms", 12),
)
Зверніть увагу на хитрість: замість title ми пишемо title_len. Так, зараз це менш зручно, зате ви не зіллєте в лог вміст користувацьких даних. І саме так працює гігієна: логи корисні, але не токсичні.
Один зміст — один ключ
Коли ви починаєте додавати поля, виникає нова небезпека: не витоки й не шум, а безлад ключів. В одному місці ви пишете op, в іншому operation, у третьому action. В одному місці taskId, в іншому id, у третьому task_id. Підсумок передбачуваний: фільтрувати й шукати неможливо. Ніби структурні логи є, а відчуття — як від каші.
Тому ми фіксуємо маленький «словничок» і дотримуємося його:
| Ключ | Що означає | Приклад |
|---|---|---|
|
підсистема/шар | |
|
дія | |
|
ідентифікатор сутності | |
|
тривалість | |
|
помилка | |
Також важливо тримати тип стабільним. Якщо id — це число, нехай воно всюди буде числом. Інакше ви отримаєте ситуацію, де в половині логів id=42, а в іншій половині id="42", і фільтр раптово перестає працювати.
Не перетворювати Any на смітник
log/slog дає спокусливу кнопку: slog.Any("something", x). Здається, що можна просто закинути туди весь обʼєкт — і проблема зникне. Але це часта пастка.
По-перше, великий обʼєкт може містити секрети й персональні дані всередині. Ви можете думати, що логуєте «задачу», а всередині структури опиниться, наприклад, електронна пошта автора або коментар. По-друге, великі обʼєкти роблять логи важкими: вони повільніше пишуться, гірше читаються, дорожче зберігаються. По-третє, вони часто ламають стабільність: структура змінюється — формат логів теж змінюється.
Тому практика така: замість «логувати весь світ» ми логуємо мінімальний набір ознак, який допомагає діагностиці. Часто це id, count, duration і акуратний err.
Наприклад, під час виведення списку задач нам зазвичай не потрібен повний дамп масиву задач у лог. Нам достатньо знати, скільки їх:
package main
import (
"log/slog"
"os"
)
func main() {
log := slog.New(slog.NewTextHandler(os.Stderr, nil))
tasks := []string{"купити молоко", "писати код", "спати"}
log.Info("задачі завантажено", slog.Int("count", len(tasks)))
}
Якщо вам дуже потрібно зрозуміти, які саме це задачі, краще тимчасово зробити це локально в розробці, а не перетворювати прод-логи на файл із усією базою користувачів.
5. Застосовуємо гігієну в TODO CLI й не ламаємо stdout
Тепер давайте приземлимо все це на наш навчальний проєкт: CLI для керування задачами. Ми вже домовилися, що результат команди йде в stdout, а діагностика — у stderr. Отже, наш slog-логер налаштовуємо на os.Stderr, а друк таблиць, JSON чи списків залишаємо через fmt/tabwriter у stdout (ви вже знайомилися з цим розділенням раніше).
Зробимо маленький «скелет» запуску команди, де вимірюємо тривалість і логуємо її як поле. Тут важливо: повідомлення стабільне, деталі — у полях.
package main
import (
"log/slog"
"os"
"time"
)
func main() {
log := slog.New(slog.NewTextHandler(os.Stderr, nil))
start := time.Now()
exitCode := 0 // припустімо, команда успішна
log.Info("команду завершено",
slog.String("component", "cli"),
slog.String("operation", "todo"),
slog.Int("exit_code", exitCode),
slog.Duration("duration", time.Since(start)),
)
}
Тепер приклад «додати задачу»: логуємо факт додавання за id, але не пишемо title повністю — памʼятаємо про персональні дані.
package main
import (
"log/slog"
)
func addTask(log *slog.Logger, id int, title string) {
log.Info("задачу створено",
slog.String("operation", "task_add"),
slog.Int("id", id),
slog.Int("title_len", len(title)),
)
}
А ось приклад обробки помилки: користувачу ми покажемо коротке повідомлення, а в лог запишемо err і контекст операції.
package main
import (
"fmt"
"log/slog"
)
func run(log *slog.Logger) int {
err := fmt.Errorf("open storage: permission denied")
if err != nil {
log.Error("команду не вдалося виконати",
slog.String("operation", "task_list"),
slog.Any("err", err),
)
fmt.Println("ПОМИЛКА: не вдалося отримати список задач") // stdout: коротко для користувача
return 1
}
return 0
}
Тут є тонкий момент: іноді розробник за звичкою хоче показати користувачу err.Error() — мовляв, нехай бачить правду. Але часто це розкриває внутрішні деталі: шляхи до файлів, системні повідомлення, внутрішні типи помилок. Для публічного інтерфейсу це погана ідея: нутрощі мають залишатися всередині, а контракт із користувачем — стабільним і безпечним.
6. Типові помилки під час гігієни логів
Ця тема здається простою рівно до першого реального інциденту, коли ви раптово розумієте, що логи читає не лише людина, яка їх написала, а дані там живуть довше, ніж ваша памʼять про те, чому ви так написали. Нижче — граблі, на які наступають навіть дуже бадьорі новачки, а якщо чесно, то й не лише новачки, причому майже завжди з найкращих намірів.
Помилка № 1: логувати секрет «лише на час налагодження», а потім забути.
Це класика жанру: ви додали log.Debug("token", ...), перевірили, що все працює, а потім забули прибрати. Через тиждень хтось вмикає Debug на проді, і токени їдуть у сховище логів. Лікується нудно: секрети не логуємо взагалі; максимум — маска або безпечний ідентифікатор.
Помилка № 2: плутати корисну діагностику та користувацьке повідомлення.
Коли в CLI у stdout вилітає «open /var/lib/todo/data.json: permission denied», ви одночасно розкриваєте нутрощі й ламаєте формат результату. Правильніше тримати користувацьке повідомлення коротким і стабільним, а подробиці, включно з початковим err, відправляти в лог на stderr.
Помилка № 3: перетворювати Info на «ми живі, ми живі, ми живі» в кожному рядку.
Якщо ви пишете Info на кожному кроці циклу, лог стає шумом. У підсумку реальні попередження ніхто не бачить. Зазвичай допомагає просте правило: Info — для значущих подій (старт/фініш команди, важливий результат), «покрокова балаканина» — у Debug і лише коли це справді потрібно.
Помилка № 4: змінювати ключі й типи полів від місця до місця.
Сьогодні ви пишете taskId=42, завтра id="42", післязавтра task_id=42. Ззовні все виглядає як «структурні логи», але фільтрація стає неможливою. Оберіть словник ключів (operation, component, id, duration, err) і дотримуйтеся його, а id тримайте одного типу.
Помилка № 5: логувати цілі структури через Any, не думаючи про склад даних.
Усередині структури може опинитися секрет або персональні дані. Крім того, логи роздуваються й стають непридатними для читання. Краще логувати мінімальні ознаки: id, count, duration, плюс err у разі помилки. Якщо треба діагностувати вміст — робіть це точково й усвідомлено.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ