Что такое Observability и зачем она нужна Swift-разработчику?
Вы когда-нибудь пытались понять, почему ваше Swift-приложение на iOS тормозит именно на экране профиля, а серверная часть на Vapor отвечает с задержкой в 3 секунды только для пользователей из Европы? Если да — вы уже сталкивались с проблемой, которую решает observability (наблюдаемость).
Observability — это не просто «логирование». Это способность задавать системе любые вопросы о её состоянии без необходимости писать новый код. Три столпа observability: трейсинг (трассировка), метрики и логи. Вместе они дают полную картину происходящего: от запроса пользователя до выполнения в глубине базы данных.
В этой статье мы разберём, как внедрить observability в Swift-проектах — как на клиенте (iOS), так и на сервере (Vapor / Hummingbird). Вы узнаете, какие библиотеки использовать, как писать эффективные трейсы и метрики, и как не утонуть в логах.
Почему Swift — идеальный кандидат для Observability?
Swift — язык с сильной типизацией, производительностью на уровне C и растущей экосистемой для серверной разработки. Но есть нюанс: сообщество Swift только начинает активно внедрять observability. В отличие от Java или Go, где OpenTelemetry уже стал стандартом, в Swift мир observability — это свежий тренд, который даёт ранним последователям огромное преимущество.
Основные вызовы для Swift-разработчика:
- Асинхронность: async/await и Structured Concurrency меняют правила игры для трейсинга.
- Многоплатформенность: код на Swift может работать на iOS, macOS, сервере и даже embedded.
- Отсутствие единого стандарта: пока нет «золотой пули», как в мире Java.
Но хорошая новость в том, что Swift предоставляет все инструменты для построения надёжной observability — нужно лишь знать, куда смотреть.
Трейсинг (Tracing) в Swift: от запроса до ответа
Что такое трейсинг и зачем он нужен?
Трейсинг (или распределённая трассировка) позволяет отследить путь одного запроса через все сервисы и функции приложения. В мире микросервисов это незаменимо. Но даже в монолитном iOS-приложении трейсинг помогает понять, какая именно часть кода тормозит.
OpenTelemetry Swift: ваш главный инструмент
OpenTelemetry — это открытый стандарт для observability. Для Swift существует пакет opentelemetry-swift. Он поддерживает трейсинг, метрики и логи, а также экспорт данных в любой бэкенд (Jaeger, Prometheus, Grafana).
Установка через Swift Package Manager:
// Package.swift
.package(url: "https://github.com/open-telemetry/opentelemetry-swift.git", from: "1.10.0")
Пишем первый трейс на Swift
Представьте, что у вас есть функция загрузки профиля пользователя. Без трейсинга вы видите только общее время выполнения. С трейсингом — каждый шаг:
import OpenTelemetryApi
import OpenTelemetrySdk
let tracer = OpenTelemetry.instance.tracerProvider.get(
instrumentationName: "ProfileModule",
instrumentationVersion: "1.0.0"
)
func loadUserProfile(userId: String) async throws -> UserProfile {
let span = tracer.spanBuilder(spanName: "loadUserProfile")
.setAttribute(key: "user.id", value: userId)
.startSpan()
defer {
span.end()
}
// Шаг 1: проверка кэша
let cacheSpan = tracer.spanBuilder(spanName: "checkCache").startSpan()
let cached = checkCache(for: userId)
cacheSpan.end()
if let cachedProfile = cached {
span.setAttribute(key: "cache.hit", value: true)
return cachedProfile
}
// Шаг 2: запрос к API
let apiSpan = tracer.spanBuilder(spanName: "fetchFromAPI").startSpan()
let profile = try await api.fetchProfile(userId: userId)
apiSpan.end()
// Шаг 3: обработка данных
let processSpan = tracer.spanBuilder(spanName: "processProfile").startSpan()
let processed = processProfile(profile)
processSpan.end()
span.setAttribute(key: "cache.hit", value: false)
return processed
}
Обратите внимание на defer { span.end() } — это гарантирует, что трейс закроется даже при ошибке. В Swift это особенно удобно благодаря строгой обработке ошибок.
Structured Concurrency и трейсинг
С появлением async/await и Task Groups трейсинг стал ещё мощнее. Вы можете автоматически создавать дочерние трейсы для каждой параллельной задачи:
await withTaskGroup(of: Data.self) { group in
let parentSpan = tracer.spanBuilder(spanName: "parallelFetch").startSpan()
for url in urls {
group.addTask {
let childSpan = tracer.spanBuilder(spanName: "fetchURL")
.setParent(parentSpan)
.startSpan()
defer { childSpan.end() }
return try await downloadData(from: url)
}
}
for await result in group {
// обработка
}
parentSpan.end()
}
Это даёт вам полную картину: какой URL тормозит, сколько времени заняла каждая подзадача.
Метрики (Metrics) в Swift: считаем всё, что движется
Зачем нужны метрики, если есть трейсинг?
Трейсинг отвечает на вопрос «почему этот конкретный запрос медленный?». Метрики — на вопрос «как много запросов медленные в целом?». Это два разных уровня абстракции. Метрики позволяют увидеть тренды: рост времени ответа, увеличение числа ошибок, падение производительности после релиза.
OpenTelemetry Metrics API
OpenTelemetry Swift предоставляет три типа метрик: Counter, Histogram и Gauge. Рассмотрим каждый.
Counter — счётчик событий
let requestCounter = meter.createCounter(name: "api.requests.total")
requestCounter.add(value: 1, attributes: ["endpoint": "profile", "status": "200"])
Histogram — распределение значений
Идеально для измерения времени выполнения:
let latencyHistogram = meter.createHistogram(
name: "api.latency",
unit: "ms",
description: "Response time in milliseconds"
)
let startTime = Date()
// ... вызов API ...
let duration = Date().timeIntervalSince(start) * 1000
latencyHistogram.record(value: duration, attributes: ["endpoint": "profile"])
Gauge — текущее значение
Например, количество активных пользователей в памяти:
let activeUsersGauge = meter.createGauge(name: "active.users")
activeUsersGauge.record(value: Double(activeUsersCount))
Продвинутый пример: метрики для iOS-приложения
На клиенте метрики помогают отслеживать производительность UI. Используем SwiftUI и Combine для автоматического сбора:
class PerformanceMonitor: ObservableObject {
private let meter = OpenTelemetry.instance.meterProvider.get(
instrumentationName: "iOSApp",
instrumentationVersion: "1.0"
)
private let frameDropCounter: Counter
init() {
frameDropCounter = meter.createCounter(name: "ui.frame