Реализация Remote Logging для отладки мобильного приложения в продакшене
Баг воспроизводится только на конкретном устройстве конкретного пользователя — и только в продакшене. Подключить debugger невозможно. Crashlytics показывает крэш, но стектрейс без контекста не объясняет, как приложение дошло до этого состояния. Remote logging решает именно эту проблему: детальные логи с устройств пользователей поступают на сервер в реальном времени или по запросу.
Архитектура
Remote logging — это не «отправлять все логи с каждого устройства на сервер». Это дорого по трафику, хранению и влияет на производительность. Правильная архитектура предполагает несколько режимов.
Пассивный режим (по умолчанию): логи пишутся в локальный кольцевой буфер. На сервер ничего не уходит.
Активный режим: включается по триггеру — крэш, конкретный user ID, флаг из Remote Config. Буфер сбрасывается на сервер.
Debug-сессия для конкретного пользователя: по запросу поддержки включается расширенное логирование для конкретного userId через Firebase Remote Config или feature flag.
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-запрос — накапливаем в очереди и отправляем пачками:
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+.
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/CCPA
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-режим
Ориентиры по срокам
Базовая система remote logging с батчевой отправкой, Remote Config управлением и интеграцией с Sentry — 1–2 недели. Полная инфраструктура с Elasticsearch, Kibana-дашбордами, GDPR-механизмами и iOS+Android — 3–4 недели. Стоимость рассчитывается индивидуально после аудита текущей инфраструктуры.







