Почему remote logging жизненно необходим?
Представьте: баг воспроизводится только на устройстве конкретного пользователя в продакшене. Crashlytics показывает крэш, но без контекста — никаких шагов, никакого состояния. Без remote logging вы тратите в среднем 3–5 дней на воспроизведение, а 80% багов остаются нелокализованными. Remote logging передаёт детальные логи на сервер в реальном времени или по запросу, сокращая поиск до 2–3 часов.
Однажды у нас был случай, когда приложение падало на iPad mini 5 при повороте экрана из-за race condition в UICollectionView. Без remote logging мы бы потратили недели на перебор гипотез. Вместо этого включили verbose-логи для нескольких пользователей, через час нашли проблему в коллбэке ориентации и выпустили hotfix.
Remote logging — это не отправка всех логов с каждого устройства на сервер. Это дорого по трафику, хранению и производительности. Правильная архитектура предполагает несколько режимов, переключаемых динамически.
Пассивный режим (по умолчанию): логи пишутся в локальный кольцевой буфер. На сервер ничего не уходит.
Активный режим: включается по триггеру — крэш, конкретный user ID, флаг из Remote Config. Буфер сбрасывается на сервер.
Debug-сессия для конкретного пользователя: по запросу поддержки включается расширенное логирование для конкретного userId через Firebase Remote Config или feature flag.
Сравнение режимов:
| Режим | Когда используется | Объем данных | Необходимость сервера |
|---|---|---|---|
| Пассивный | По умолчанию | Минимальный (только метрики) | Нет |
| Активный | При крэшах или инцидентах | Средний (буфер последних N логов) | Да |
| Debug-сессия | Для конкретного пользователя | Высокий (все логи сессии) | Да |
Firebase Remote Config для динамического управления логированием
// Android: проверка флагов логирования при старте
val remoteConfig = Firebase.remoteConfig
remoteConfig.fetchAndActivate().addOnCompleteListener {
val logLevel = remoteConfig.getString("debug_log_level") // "OFF", "ERROR", "VERBOSE"
val targetUserId = remoteConfig.getString("debug_user_id") // пустая строка = все
RemoteLogger.configure(
level = LogLevel.fromString(logLevel),
targetUserId = targetUserId
)
}
Включить подробное логирование для конкретного пользователя без релиза: меняем Remote Config — через 30 минут устройство подтянет новый конфиг, следующая сессия пишет verbose-логи.
Транспорт логов
Батчевая отправка — система удаленного сбора
Не отправляем каждый лог-вызов как отдельный HTTP-запрос: накапливаем в очереди и отправляем пачками по 100 записей.
class RemoteLogTransport(
private val apiService: LogApiService,
private val batchSize: Int = 100,
private val flushIntervalMs: Long = 30_000
) {
private val pendingLogs = ConcurrentLinkedQueue<LogEntry>()
fun enqueue(entry: LogEntry) {
pendingLogs.add(entry)
if (pendingLogs.size >= batchSize) {
flush()
}
}
private fun flush() {
val batch = mutableListOf<LogEntry>()
repeat(batchSize) {
pendingLogs.poll()?.let { batch.add(it) } ?: return@repeat
}
if (batch.isNotEmpty()) {
scope.launch {
runCatching {
apiService.sendLogs(LogBatch(
sessionId = sessionId,
deviceInfo = deviceInfo,
logs = batch
))
}
}
}
}
}
WorkManager для гарантированной доставки при восстановлении сети:
val logUploadWork = OneTimeWorkRequestBuilder<LogUploadWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
.build()
WorkManager.getInstance(context).enqueue(logUploadWork)
iOS — комбинация OSLog и remote transport
// OSLog для системного логирования + remote transport
actor RemoteLogger {
private var buffer: [LogEntry] = []
private var isRemoteEnabled = false
private let transport: LogTransport
func log(_ message: String, level: LogLevel) async {
let entry = LogEntry(timestamp: Date(), level: level, message: message)
buffer.append(entry)
if buffer.count > 500 { buffer.removeFirst() }
if isRemoteEnabled {
await transport.enqueue(entry)
}
}
func enableRemote(for userId: String) async {
isRemoteEnabled = true
// Отправляем буфер накопленных логов
let bufferedLogs = buffer
await transport.sendBatch(bufferedLogs)
}
}
actor обеспечивает thread safety без explicit locking — правильный подход в Swift 5.5+. Подробнее о OSLog в документации Apple.
Backend для хранения логов
Стандартные решения:
| Хранилище | Подходит для | Особенности |
|---|---|---|
| Elasticsearch + Kibana | Полнотекстовый поиск по логам | Ресурсоёмкий, но мощный |
| Loki + Grafana | Структурированные логи, мало ресурсов | Дешевле Elastic |
| Datadog | SaaS, без инфраструктуры | Дорогой при большом объёме |
| Sentry | Уже используется для крэшей | Breadcrumbs + remote logs в одном месте |
Sentry Breadcrumbs — часто недооценённая функция. Кастомные breadcrumbs прикрепляются к каждому Event (крэшу или ошибке) и показывают, что происходило до проблемы:
SentrySDK.configureScope { scope in
scope.addBreadcrumb(Breadcrumb(
level: .info,
category: "navigation",
message: "User opened PaymentScreen",
data: ["orderId": orderId]
))
}
Отметим: когда случается крэш, в Sentry видны последние 100 breadcrumbs — фактически готовый лог пользовательского пути.
Безопасность и соответствие GDPR
Remote логи потенциально содержат персональные данные. Мы гарантируем:
- Логи не содержат полных имён, email, номеров карт — только
userIdдля корреляции. - Данные логов хранятся не дольше 30 дней (настраивается TTL в хранилище).
- Пользователь может отказаться от сбора диагностики в настройках приложения — флаг сохраняется в
UserDefaults/SharedPreferences, проверяется перед каждой отправкой. - В Privacy Policy описан сбор диагностических данных.
Оперативная отладка без релиза
Сценарий: продакшен падает у 0.3% пользователей на конкретном устройстве. Последовательность без remote logging:
- Попросить пользователя включить developer mode — маловероятно.
- Ждать воспроизведения — неизвестно когда.
С remote logging:
- Включить verbose-режим через Remote Config для конкретного userId.
- Пользователь воспроизводит проблему в следующей сессии.
- Через 30 минут в Kibana/Grafana видны подробные логи сессии.
- Находим место, выпускаем hotfix, выключаем verbose-режим.
Что входит в работу
- Архитектурная документация с описанием схемы передачи логов.
- SDK для iOS (Swift) и Android (Kotlin) с поддержкой OSLog и WorkManager.
- Интеграция с Firebase Remote Config и Sentry breadcrumbs.
- Бэкенд-хранилище (Elasticsearch или Loki) с настроенными дашбордами.
- Политика безопасности и TTL в соответствии с GDPR.
- Обучение команды работе с системой и техническая поддержка на этапе внедрения.
Процесс работы
Аналитика и проектирование
Мы изучаем вашу инфраструктуру, определяем критические точки сбора логов. Проектируем архитектуру: выбираем хранилище, настраиваем TTL, определяем уровни логирования.
Реализация на iOS и Android
Пишем SDK для iOS (Swift, с использованием OSLog) и Android (Kotlin, с WorkManager). Добавляем Remote Config для динамического управления. Интегрируем Sentry breadcrumbs.
Тестирование и деплой
Проводим нагрузочное тестирование: проверяем, что при 1000 логов в секунду приложение не тормозит. Развёртываем backend, настраиваем дашборды. Обучаем команду.
Ориентиры по срокам
Базовая система remote logging с батчевой отправкой, Remote Config управлением и интеграцией с Sentry — 1–2 недели. Полная инфраструктура с Elasticsearch, Kibana-дашбордами, GDPR-механизмами и iOS+Android — 3–4 недели. Стоимость рассчитывается индивидуально после аудита текущей инфраструктуры.
Как уменьшить затраты на отладку?
Remote logging экономит до 50% времени на поиск и исправление багов. Вместо того чтобы заказывать тестовые устройства и воспроизводить проблемы вслепую, вы сразу видите логи реальных пользователей. Это снижает затраты на QA на 40% и ускоряет релизы на 30%. Наша команда имеет 10+ лет опыта в мобильной разработке и реализовала удаленное логирование для более чем 200 проектов. Закажите внедрение remote logging, чтобы повысить качество приложения. Получите консультацию — наши инженеры проанализируют вашу инфраструктуру и предложат оптимальное решение. Свяжитесь с нами, чтобы начать экономить время на отладке.







