1. Зачем логам «координаты»: когда без них больно
Когда вы только начинаете, кажется, что лог — это просто “напечатать строку”. Но уже на второй-третий баг выясняется неприятная правда: одна и та же фраза "parse failed" может появляться в разных местах программы, иногда даже в разных файлах, а вы смотрите на лог и думаете: “Окей… а где именно оно упало?”.
Контекст в логах — это как адрес на посылке. Можно, конечно, написать “для Васи” и надеяться, что почта догадается. Но чаще хочется всё же “город, улица, дом, квартира” — чтобы не бегать по району с криком “я ищу Васю, он где-то тут!”.
Давайте сравним два подхода. Первый — лог без координат:
func log(_ message: String) {
print("[LOG] \(message)")
}
func parseCommand(_ input: String) {
log("parse failed") // а где именно failed?
}
parseCommand("add Swift")
С точки зрения вывода это выглядит “нормально”, но пользы мало. Второй подход — тот же лог, но с координатами:
func log(_ message: String, file: String, line: Int, function: String) {
print("[LOG] \(file):\(line) \(function) — \(message)")
}
func parseCommand(_ input: String) {
log("parse failed", file: "Parser.swift", line: 42, function: "parseCommand(_:)")
}
parseCommand("add Swift")
Это уже похоже на реальную диагностику. Проблема лишь в том, что вручную писать file/line/function — занятие для людей, которые ненавидят себя (или хотят развить очень специфическую дисциплину). Поэтому в Swift есть магические идентификаторы, которые компилятор подставляет сам.
2. Магические идентификаторы Swift: #fileID, #line, #function
Сейчас мы познакомимся с маленькой магией Swift: “псевдолитералами”, которые компилятор заменяет на значения прямо в месте вызова. Это важно: они не “вычисляются где-то потом”, а подставляются в конкретной точке кода. И это именно то, что нужно логам: лог пишет координаты того места, где вы вызвали логирование.
В Swift есть несколько полезных идентификаторов “про местоположение”:
- #line — номер строки (целое число),
- #function — имя функции/метода,
- #fileID — короткий идентификатор файла (обычно “модуль/файл”),
- #filePath — полный путь к файлу (используется реже, и сегодня мы обсудим почему).
Важная деталь: #fileID появился как компактная и более безопасная альтернатива полному пути. Для production‑логов обычно стоит предпочитать именно #fileID: он экономит место и лучше защищает приватность разработчика и окружения.
Таблица: что именно даёт каждый идентификатор
| Идентификатор | Тип | Пример (примерный) | Когда уместен |
|---|---|---|---|
|
|
|
Всегда полезен: быстро перейти в нужную строку |
|
|
|
Полезно, если логов много и функции похожи |
|
|
|
Хороший дефолт для логов (компактно и аккуратно) |
|
|
|
Иногда удобно локально, но может раскрывать лишнее |
Чтобы не быть голословными, давайте посмотрим, что они печатают:
func showContext(
fileID: String = #fileID,
line: Int = #line,
function: String = #function
) {
print("fileID=\(fileID)") // fileID=... (зависит от проекта)
print("line=\(line)") // line=...
print("function=\(function)") // function=showContext(fileID:line:function:)
}
showContext()
Обратите внимание: мы не передали аргументы, но значения всё равно появились — потому что они дефолтные, и компилятор подставил их.
И ещё одна концептуально важная вещь: эти “магические штуки” — механизм уровня языка/компилятора, а не чья-то библиотека.
3. Параметры по умолчанию: контекст берётся в месте вызова
Теперь соберём правильную технику: мы хотим, чтобы вызов лога выглядел коротко, а координаты подставлялись автоматически. Для этого идеально подходит паттерн “параметры по умолчанию”. Он звучит скучно, но даёт суперсилу: вы пишете debugLog("..."), а программа сама добавляет “где это произошло”.
Сделаем минимальную логгер‑функцию (пока без протоколов и архитектурных наворотов — сегодня нам важно именно место в коде):
func debugLog(
_ message: String,
fileID: String = #fileID,
line: Int = #line,
function: String = #function
) {
print("[DEBUG] \(fileID):\(line) \(function) — \(message)")
}
func parse() {
debugLog("start parsing")
}
parse()
// Пример вывода:
// [DEBUG] LibraryCLI/main.swift:12 parse() — start parsing
Три важные мысли, которые обычно “щёлкают” не сразу:
Первая: значения берутся в месте вызова, то есть fileID/line/function будут указывать на строку с debugLog("start parsing"), а не на внутренности debugLog.
Вторая: параметры fileID/line/function вы почти никогда не передаёте руками — но они существуют, чтобы мы могли “протянуть” контекст дальше, если появится обёртка.
Третья: если вы начнёте делать обёртки неправильно, координаты “съедут”. Покажу классическую ловушку.
Ловушка: обёртка, которая ломает координаты
func debugLog(_ message: String,
fileID: String = #fileID,
line: Int = #line,
function: String = #function) {
print("[DEBUG] \(fileID):\(line) \(function) — \(message)")
}
func logParser(_ message: String) {
debugLog("[parser] \(message)")
}
logParser("unexpected token")
// Координаты укажут на logParser(...), а не на реальное место вызова logParser(...)
Почему так? Потому что #fileID/#line/#function подставились внутри logParser, а не в месте, где вы вызвали logParser. То есть координаты будут “адресом посредника”.
Исправление: протягиваем контекст наружу → внутрь
func debugLog(_ message: String,
fileID: String = #fileID,
line: Int = #line,
function: String = #function) {
print("[DEBUG] \(fileID):\(line) \(function) — \(message)")
}
func logParser(_ message: String,
fileID: String = #fileID,
line: Int = #line,
function: String = #function) {
debugLog("[parser] \(message)", fileID: fileID, line: line, function: function)
}
logParser("unexpected token")
// Теперь координаты укажут на строку с logParser("unexpected token")
Это выглядит как небольшая бюрократия, но на практике спасает тонну времени: вы видите лог — вы сразу знаете точку входа, где событие было зафиксировано.
4. Приватность: логи не должны «болтать лишнее»
Контекст — это супер. Но у него есть “тёмная сторона”: как только вы начинаете добавлять больше диагностической информации, появляется соблазн логировать вообще всё подряд — включая пользовательский ввод, токены, пароли, персональные данные и даже “ну я просто временно распечатаю весь JSON, потом уберу” (спойлер: потом часто не убирают).
Здесь полезно держать в голове простое правило: лог — это не сейф. Лог читают люди, лог улетает в отчёты, лог попадает в баг‑репорты, лог может оказаться в CI, в истории терминала — да где угодно. Поэтому логируем так, как будто завтра это увидит кто-то ещё (потому что, сюрприз, скорее всего увидит).
И тут #fileID снова выигрывает у полного пути: абсолютный путь может раскрывать имя пользователя на машине, структуру директорий, корпоративные названия и прочие детали окружения. А #fileID задуман как более компактный и более безопасный для production‑логов формат.
Мини‑приём: маскирование (redaction)
Самая простая техника: если значение чувствительное — в лог уходит <redacted>, а не оригинал.
func redacted(_ value: String) -> String {
"<redacted>"
}
let token = "SECRET_TOKEN_123"
debugLog("auth token=\(redacted(token))")
// [DEBUG] ... — auth token=<redacted>
Это банально, но работает. И главное — у вас появляется привычка: “я всегда думаю, можно ли печатать это значение”.
Мини‑приём: логируем характеристики, а не контент
Очень часто для диагностики не нужен весь ввод пользователя. Нужно понять, что он был пустой, слишком длинный, содержит странные символы, или что парсер ожидал 3 токена, а получил 1.
func inputSummary(_ input: String) -> String {
"length=\(input.count)"
}
let input = #"add "The Swift Programming Language""#
debugLog("rawInput \(inputSummary(input))")
// [DEBUG] ... — rawInput length=34
Если нужно чуть больше, можно логировать “первые N символов”, но аккуратно — и только если это точно не секреты. В CLI это особенно актуально: пользователь может вставить туда что угодно, включая случайно скопированный токен.
func safePrefix(_ input: String, limit: Int = 10) -> String {
let prefix = input.prefix(limit)
return "\(prefix)… (len=\(input.count))"
}
debugLog("inputPreview=\(safePrefix("hello world", limit: 5))")
// [DEBUG] ... — inputPreview=hello… (len=11)
Да, даже превью может быть спорным для приватности — но иногда в учебных задачах удобно увидеть хотя бы кусочек. В реальном продукте лучше быть ещё осторожнее и чаще ограничиваться длиной/количеством токенов.
Мини‑приём: не логировать абсолютные пути “на автомате”
Порой хочется взять #filePath, потому что “удобнее, сразу файл открою”. Для локальной отладки — возможно. Но как только логи начинают жить дольше одного запуска, абсолютный путь становится “слишком разговорчивым”.
Поэтому для production‑логов обычно выбирают #fileID, а полный путь используют только там, где это действительно оправдано и точно не попадёт конечным пользователям.
Мини‑интеграция в LibraryCLI: ошибки парсинга без утечек
Давайте сделаем небольшой кусочек нашего CLI‑приложения “библиотека книг” чуть более диагностируемым. Сценарий простой: пользователь вводит команду текстом, мы пытаемся распарсить. Если не получилось — пользователю говорим “Некорректная команда”, а в лог пишем контекст: где сломались и что именно было странно, но без утечек содержимого строки.
Сделаем мини‑модель команды и ошибку:
enum Command {
case help
case add(title: String)
}
enum CommandParseError: Error {
case emptyInput
case unknownCommand
case missingTitle
}
Теперь — парсер. Обратите внимание: логировать будем “сколько символов пришло” и “сколько токенов”, а не печатать всю строку:
func parseCommand(_ input: String) -> Result<Command, CommandParseError> {
let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return .failure(.emptyInput) }
let parts = trimmed.split(separator: " ", maxSplits: 1).map(String.init)
guard let name = parts.first else { return .failure(.emptyInput) }
switch name {
case "help":
return .success(.help)
case "add":
guard parts.count == 2 else { return .failure(.missingTitle) }
return .success(.add(title: parts[1]))
default:
return .failure(.unknownCommand)
}
}
И теперь — “обвязка” выполнения, где появляется и пользовательское сообщение, и диагностический лог с координатами:
import Foundation
func debugLog(_ message: String,
fileID: String = #fileID,
line: Int = #line,
function: String = #function) {
print("[DEBUG] \(fileID):\(line) \(function) — \(message)")
}
let input = readLine() ?? ""
switch parseCommand(input) {
case .success(let command):
print("OK: \(command)") // UX-упрощение для примера
case .failure(let error):
print("Некорректная команда. Введите help.") // сообщение пользователю
debugLog("parse failed: \(error) inputLength=\(input.count)")
}
Обратите внимание на баланс: пользователю мы не показываем .missingTitle и не рассказываем, что там split вернул parts.count == 1. А в лог мы добавляем и тип ошибки, и длину ввода, и координаты места, где именно это событие было зафиксировано.
Если потом окажется, что ошибка возникает только на длинных строках или только на пустом вводе — вы увидите это в логах, не раскрывая содержимое ввода. Это и есть “диагностика без болтовни”.
5. Типичные ошибки при добавлении контекста и защите приватности
Ошибка №1: использовать #filePath “по привычке” и тащить в логи полный путь.
На локальной машине это кажется удобным, но полный путь легко раскрывает детали окружения: имя пользователя, структуру каталогов, рабочие директории, иногда даже названия проектов/команд. Для production‑логов лучше держаться #fileID, потому что он компактнее и более дружелюбен к приватности.
Ошибка №2: делать обёртку над логированием и не протягивать fileID/line/function внутрь.
Очень частая проблема: вы создаёте logParser(...), logService(...), а потом внезапно все логи “происходят” из одного и того же файла и одной и той же строки — из вашей обёртки. Лечится простым правилом: у обёртки должны быть такие же дефолтные параметры, и она обязана передавать их дальше.
Ошибка №3: логировать “сырой ввод пользователя”, потому что так проще отлаживать.
Да, проще. А ещё это прямой путь к тому, что в логах окажутся токены, пароли, номера карт (иногда — случайно, иногда — потому что пользователь вставил не туда). В большинстве случаев достаточно length, количества токенов, типа ошибки и категории события.
Ошибка №4: думать, что “временно залогирую секрет, потом уберу”.
Это классический программистский оптимизм. “Потом” часто не наступает: фикс уехал, задача закрыта, вы забыли. Если значение потенциально чувствительное — сразу используйте маскирование (<redacted>) или не логируйте вовсе.
Ошибка №5: путать назначение координат в логах и превращать их в “отчёт о всей вселенной”.
file/line/function — это не повод писать гигантские логи на 20 строк. Координаты должны помогать быстро найти место в коде. А детали лучше добавлять дозированно: тип ошибки, короткий код события, безопасные метаданные (длина, количество, id).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ