| name | log-levels-and-hygiene |
| description | Rules for choosing log levels, keeping signal/noise healthy, managing on-device log retention, and avoiding logger abuse on mobile. Use whenever adding or reviewing log statements. |
Log Levels and Hygiene
Instructions
Mobile loggers double as crash breadcrumbs, support artifacts, and product telemetry. Without discipline they also become the loudest, most expensive channel in the stack. This skill defines the levels, the on-device retention policy, and the review rules.
1. Level Definitions
Use exactly five levels. No custom levels, no verbose vs trace variants.
| Level | Meaning | Ships remote? | Kept after crash? |
|---|
trace | Only during local debugging. Very chatty, per-frame or per-iteration. | Never | No |
debug | Developer-oriented. Off in release builds unless a debug flag is set. | Sampled low | Yes (last 64) |
info | Business events and state transitions worth surfacing in production. | Sampled med | Yes |
warn | Recoverable anomaly; user experience may be degraded. | Always | Yes |
error | Failed operation; requires investigation. Often paired with a non-fatal. | Always | Yes |
fatal is not a log level; it is a crash. Use recordError(fatal=true) / SentrySDK.capture instead.
2. When to Log What
- Every screen transition:
info nav_screen_shown { from, to }.
- Every network request outcome:
info on success, warn on retry, error on final failure. Include status, duration_ms, request_id.
- Every auth boundary:
info on sign-in / sign-out, warn on token refresh retries, error on auth failure.
- Every feature-flag evaluation that changes user experience:
debug level with flag name and assignment.
- Never log inside
build, render, onDraw, layoutSubviews, scrollViewDidScroll, or any per-frame callback.
3. Anti-Patterns
Log.d("TAG", "user=$user") where user is a full object -- leaks PII and blows up allocation. Log specific fields, after redaction.
catch (e: Exception) { Log.e("err", e) } with no context. Always include what was being attempted and what is being recovered.
- Logging success of every operation at
info. If 99% of calls succeed, one success log per release build is enough; log only failures at info or above.
- Reusing one logger name for the whole app. Use
feature.subfeature so filtering works.
4. On-Device Retention
- Crashlytics keeps the last 64 log lines per crash. Do not rely on longer history on the client.
- Write-ahead log for the remote shipper is not a persistent diagnostic log -- it is a transient queue.
- For support-driven diagnostics (opt-in "share logs" button), write a separate rotated file log:
- Rolling file size 2 MB, 3 files max (6 MB ceiling).
- Stored in the app sandbox, never in shared storage.
- Cleared on sign-out and on app uninstall (automatic on both platforms).
- Encrypted at rest if any PII can land in it, which it usually will.
5. Kotlin Example
inline fun <T> traced(logger: String, op: String, block: () -> T): T {
Log.event(logger, "${op}_start")
val start = System.nanoTime()
return try {
val result = block()
Log.event(logger, "${op}_success",
attrs = mapOf("duration_ms" to (System.nanoTime() - start) / 1_000_000))
result
} catch (t: Throwable) {
Log.event(logger, "${op}_error", level = Level.ERROR,
attrs = mapOf("error_class" to (t::class.simpleName ?: "Unknown"),
"duration_ms" to (System.nanoTime() - start) / 1_000_000))
throw t
}
}
6. Swift Example
func traced<T>(_ logger: String, op: String, _ block: () throws -> T) rethrows -> T {
AppLog.event(logger: logger, msg: "\(op)_start")
let start = DispatchTime.now().uptimeNanoseconds
do {
let r = try block()
AppLog.event(logger: logger, msg: "\(op)_success",
attrs: ["duration_ms": (DispatchTime.now().uptimeNanoseconds - start) / 1_000_000])
return r
} catch {
AppLog.event(logger: logger, msg: "\(op)_error", level: .error,
attrs: ["error_class": String(describing: type(of: error)),
"duration_ms": (DispatchTime.now().uptimeNanoseconds - start) / 1_000_000])
throw error
}
}
7. Debug Flags
Do not ship an in-app "verbose logging" toggle that a user can flip without consent implications. Instead:
- Gate verbose logging behind a remote config flag assigned only to internal QA cohorts.
- Require re-consent to ship logs if verbose mode is enabled mid-session.
- Auto-expire verbose mode after 1 hour or at app restart.
8. Review Rules
In code review, reject any new log statement that:
- Does not have a stable
msg event name.
- Logs an object reference or a full stack trace at
info or below.
- Runs inside a hot path (render, scroll, animation tick).
- Leaks PII per the redactor deny-list.
- Adds a new logger name without being listed in the logger registry.
9. Metrics on Your Own Logs
Track log_volume_per_logger_per_release and log_error_rate_per_session. Alert when either doubles release over release -- that's usually a regression, not a new feature.
Checklist