Реалізація 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 тижня. Вартість розраховується індивідуально після аудиту поточної інфраструктури.







