JavaRush /Курсы /Swift SELF /Контекст в логах: #fileID...

Контекст в логах: #fileID / #line / #function

Swift SELF
54 уровень , 2 лекция
Открыта

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: он экономит место и лучше защищает приватность разработчика и окружения.

Таблица: что именно даёт каждый идентификатор

Идентификатор Тип Пример (примерный) Когда уместен
#line
Int
128
Всегда полезен: быстро перейти в нужную строку
#function
String
addBook(title:)
Полезно, если логов много и функции похожи
#fileID
String
LibraryCLI/Parser.swift
Хороший дефолт для логов (компактно и аккуратно)
#filePath
String
/Users/alex/Projects/.../Parser.swift
Иногда удобно локально, но может раскрывать лишнее

Чтобы не быть голословными, давайте посмотрим, что они печатают:

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).

1
Задача
Swift SELF, 54 уровень, 2 лекция
Недоступна
След вызова
След вызова
1
Задача
Swift SELF, 54 уровень, 2 лекция
Недоступна
Быстрый дебаг
Быстрый дебаг
1
Задача
Swift SELF, 54 уровень, 2 лекция
Недоступна
Прокси лог
Прокси лог
1
Задача
Swift SELF, 54 уровень, 2 лекция
Недоступна
Безопасный парсер
Безопасный парсер
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ