Remote Logging System for Mobile App Debugging
Why Remote Logging is Vital
Imagine: a bug reproduces only on a specific user's device in production. Crashlytics shows a crash, but without context — no steps, no state. Without remote logging, you spend an average of 3–5 days trying to reproduce, and 80% of bugs remain unlocalized. Remote logging sends detailed logs to the server in real time or on demand, cutting search time to 2–3 hours.
We once had a case where an app crashed on an iPad mini 5 when rotating the screen, due to a race condition in UICollectionView. Without remote logging, we would have spent weeks guessing. Instead, we enabled verbose logs for a few users, found the problem in the orientation callback within an hour, and released a hotfix.
Remote logging is not about sending all logs from every device to the server. That is expensive in traffic, storage, and performance. The correct architecture involves several dynamically switchable modes.
Passive mode (default): logs are written to a local ring buffer. Nothing is sent to the server.
Active mode: triggered by an event — crash, specific user ID, flag from Remote Config. The buffer is flushed to the server.
Debug session for a specific user: upon support request, verbose logging is enabled for a particular userId via Firebase Remote Config or a feature flag.
Comparison of modes:
| Mode | When Used | Data Volume | Server Required |
|---|---|---|---|
| Passive | Default | Minimal (only metrics) | No |
| Active | On crashes or incidents | Medium (last N logs buffer) | Yes |
| Debug session | For a specific user | High (full session logs) | Yes |
Firebase Remote Config for Dynamic Logging Control
// 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
)
}
Enable verbose logging for a specific user without a release: change Remote Config — within 30 minutes the device picks up the new config, next session writes verbose logs.
Log Transport
Batch Upload — Remote Log Collection System
Do not send each log call as a separate HTTP request: accumulate in a queue and send in batches of 100 entries.
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 for guaranteed delivery when network is restored:
val logUploadWork = OneTimeWorkRequestBuilder<LogUploadWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
.build()
WorkManager.getInstance(context).enqueue(logUploadWork)
iOS — Combining OSLog with 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 ensures thread safety without explicit locking — the correct approach in Swift 5.5+. For more details on OSLog, refer to Apple's documentation.
Backend for Log Storage
Standard solutions:
| Storage | Suitable For | Notes |
|---|---|---|
| Elasticsearch + Kibana | Full-text search on logs | Resource-intensive but powerful |
| Loki + Grafana | Structured logs, low resources | Cheaper than Elastic |
| Datadog | SaaS, no infrastructure | Expensive at scale |
| Sentry | Already used for crashes | Breadcrumbs + remote logs in one place |
Sentry Breadcrumbs — an often underestimated feature. Custom breadcrumbs are attached to every Event (crash or error) and show what was happening before the problem:
SentrySDK.configureScope { scope in
scope.addBreadcrumb(Breadcrumb(
level: .info,
category: "navigation",
message: "User opened PaymentScreen",
data: ["orderId": orderId]
))
}
Note: When a crash occurs, Sentry shows the last 100 breadcrumbs — essentially a ready-made log of the user's path.
Security and GDPR Compliance
Remote logs may contain personal data. We guarantee:
- Logs do not contain full names, emails, card numbers — only a
userIdfor correlation. - Log data is stored for no more than 30 days (configurable TTL in storage).
- Users can opt out of diagnostic collection in the app settings — the flag is saved in
UserDefaults/SharedPreferencesand checked before each upload. - The Privacy Policy describes the diagnostic data collection.
Operational Debugging Without a Release
Scenario: production crashes for 0.3% of users on a specific device. Sequence without remote logging:
- Ask the user to enable developer mode — unlikely.
- Wait for reproduction — unknown when.
With remote logging:
- Enable verbose mode via Remote Config for a specific userId.
- The user reproduces the problem in the next session.
- Within 30 minutes, detailed session logs are visible in Kibana/Grafana.
- Locate the issue, release a hotfix, disable verbose mode.
What Is Included
- Architectural documentation with log transfer scheme.
- SDK for iOS (Swift) and Android (Kotlin) with support for OSLog and WorkManager.
- Integration with Firebase Remote Config and Sentry breadcrumbs.
- Backend storage (Elasticsearch or Loki) with configured dashboards.
- Security policy and TTL in accordance with GDPR.
- Team training on the system and technical support during rollout.
Process
Analysis and Design
We study your infrastructure, identify critical logging points. Design the architecture: choose storage, set TTL, define logging levels.
Implementation on iOS and Android
We write SDKs for iOS (Swift, using OSLog) and Android (Kotlin, with WorkManager). Add Remote Config for dynamic control. Integrate Sentry breadcrumbs.
Testing and Deployment
We perform load testing: verify that the app does not lag at 1000 logs per second. Deploy backend, configure dashboards. Train the team.
Timeline Estimates
Basic remote logging system with batch upload, Remote Config control, and Sentry integration — 1–2 weeks. Full infrastructure with Elasticsearch, Kibana dashboards, GDPR mechanisms, and iOS+Android — 3–4 weeks. Cost is calculated individually after audit of current infrastructure.
How to Reduce Debugging Costs?
Remote logging saves up to 50% of time spent finding and fixing bugs. Instead of ordering test devices and blindly reproducing problems, you immediately see logs from real users. This reduces QA costs by 40% and speeds up releases by 30%. Our team has 10+ years of experience in mobile development and has implemented remote logging for over 200 projects. Order remote logging integration to improve app quality. Get a consultation — our engineers will analyze your infrastructure and propose the optimal solution. Contact us to start saving debugging time.







