# orhanobut/logger: readable Logcat output and the file writer Android moved away from

> orhanobut/logger formats messages, collections, JSON and XML before they reach Logcat, and hands every call to adapters you register once. Its file output is the weak point, because the default destination still points at a storage location that scoped storage closes off.

**orhanobut/logger** — ✔️ Simple, pretty and powerful logger for android

- Repository: https://github.com/orhanobut/logger
- Stars: 13,831 · Forks: 2,120
- Language: Kotlin
- License: Apache-2.0
- Published: 2026-10-13 · Updated: 2026-10-13 · Language: en
- Canonical page: https://hysenlabs.com/projects/orhanobut-logger

## Readable collections and JSON in raw Logcat

Android's own logging entry point takes a String, so a List, a Map, an IntArray or a JSON document handed to it collapses into one unreadable line. orhanobut/logger accepts those values and formats them before they reach Logcat, which is the whole reason the library exists. Repository topics name 4 tags, json-logging, logcat, pretty-printer and timber, matching what the code does: message formatting, collection layout, structured payloads, and a bridge to another logging facade. Stack traces, custom tags and destinations beyond Logcat belong to the same output, and a screenshot of a real Logcat window sits at art/logger_output.png beside art/logger-logo.png. The audience is narrow on purpose. Application developers debugging on a device, where the Logcat window scrolls fast and a single line has to carry its own structure, are the readers this output is written for. Nobody untangling a payment failure at 2am wants a second logging dialect to learn, so the design keeps one familiar sink and spends its effort on how each line reads.

## A static Logger fans out to the adapters you add

`Logger` is a static facade that keeps no formatting state of its own. Formatting and destinations belong to adapters, and every call is handed to all adapters you registered. Initialization is one call in your application, after which logging works from anywhere in the process. Two adapters ship by name: `AndroidLogAdapter` writes to Logcat, `DiskLogAdapter` writes to a file, and you can also register an anonymous `AndroidLogAdapter(format)` when you need to override behaviour. Because dispatch loops over the registered list, adding a destination means adding an adapter instead of editing call sites. Two consequences follow from that shape. Nothing in the facade exposes a way to remove a single adapter; the README documents `Logger.clearLogAdapters()` for replacing the configured set, so silencing only the file destination has no documented path. And because the list is global, any library that registers an adapter on Logger changes the output of every log line your app writes.

## Two repositories and one dependency line before any output

Logger arrives as a Gradle dependency, so both repositories have to be declared in `dependencyResolutionManagement` before any coordinate resolves. Google Maven and Maven Central are the two the setup asks for:

```kotlin
dependencyResolutionManagement {
  repositories {
    google()
    mavenCentral()
  }
}
```

Then the dependency itself, pinned to one version string:

```kotlin
dependencies {
  implementation("com.orhanobut:logger:2.2.0")
}
```

Initialize once inside your Application class, then log from any thread that needs it:

```kotlin
Logger.addLogAdapter(AndroidLogAdapter())
Logger.d("hello %s", "world")
```

After that first call every `Logger.d` in the process reaches Logcat with collections and JSON already formatted. The repository root shows the build shape without opening anything: `build.gradle.kts`, `gradle.properties`, `settings.gradle.kts` and a checked-in wrapper in `gradlew` and `gradlew.bat`, with modules in `logger/` and `sample/`. One thing to check before pinning anything: the setup line still names 2.2.0, an artifact dated 2018-03-24, while the last push to the default branch landed on 2026-10-03, so the tree you are reading and the artifact your build resolves are not the same code.

## Collections, JSON and XML all log at debug priority

Six priorities are exposed as one-letter calls, debug, error, warning, verbose, information and wtf, and each takes a format string plus arguments, so `Logger.d("hello %s", "world")` pairs a placeholder with a value. Collections and structured payloads get their own helpers instead of being stringified:

```kotlin
Logger.d(listOf("foo", "bar"))
Logger.d(mapOf("key" to "value"))
Logger.d(intArrayOf(1, 2, 3))
Logger.json("""{"items":[1,2,3]}""")
Logger.xml("<root><item>value</item></root>")
```

Exceptions travel with a throwable-first overload, `Logger.e(throwable, "message")`, which keeps the message readable while the cause stays attached. Here is the detail that matters once filtering is in play: collections and structured output use debug priority, every time, even inside failure handling. A build that admits warnings and above therefore never emits JSON or XML, because those helpers have no way to raise their own priority. A plain `Logger.e("error")` carries a plain string at error priority, so the gap is specific to payloads rather than to the whole API.

## The per-call tag prefixes onto the strategy tag

`Logger.t` sets a tag for exactly one message on the calling thread, and the README's second example shows what that tag becomes once the format strategy has a name of its own. With `tag("MyApp")` configured, the line renders as MyApp-Screen, so the two tags are composed rather than replacing each other:

```kotlin
Logger.t("Checkout").d("Order submitted")
```

The strategy builder exposes 4 dials, each with its default written next to it:

```kotlin
val format = PrettyFormatStrategy.newBuilder()
  .showThreadInfo(false) // Default: true
  .methodCount(0)        // Default: 2 caller frames
  .methodOffset(0)       // Default: 0 additional frames skipped
  .tag("MyApp")          // Default: PRETTY_LOGGER
  .build()
```

A default build therefore writes the tag, the thread name, 2 frames of caller and then the message, which is enough context to identify the call site without printing a stack trace on every line. The example turns thread info off and caller frames to zero, which suits code that logs from many threads on hot paths. Note what the builder is not: there is no field-stacking option, no timestamp setting and no level filter here, because those decisions live elsewhere in the adapter.

## isLoggable gates every write, so non-debug builds stay silent

Filtering is a method override on the adapter rather than a property on the facade. The example subclasses `AndroidLogAdapter(format)` and returns `BuildConfig.DEBUG` from `isLoggable`, which means every write is gated at the moment it happens and a non-debug build produces an empty Logcat rather than a trimmed one:

```kotlin
Logger.addLogAdapter(object : AndroidLogAdapter(format) {
  override fun isLoggable(priority: Int, tag: String?): Boolean = BuildConfig.DEBUG
})
```

Because the gate lives inside the adapter you registered, it travels with that adapter. The plain `AndroidLogAdapter()` from the setup snippet carries no override at all, so adding it later without repeating the `isLoggable` body turns logging back on in shipped builds with no other visible change. Swap order matters for the same reason: `Logger.clearLogAdapters()` is the only documented way to drop what is already registered before replacements go in. On the Logcat side, filtering by tag uses `PRETTY_LOGGER` by default, or whatever the strategy's `tag("MyApp")` call set, so changing that one builder line also changes the filter string you type into Logcat.

## File output splits into a format strategy and a write strategy

File writing reuses the two-part design from the Logcat path. A format strategy decides the text, `CsvFormatStrategy` in this case, and a write strategy decides where bytes land, which the README expects you to supply yourself as a custom `DiskLogStrategy`. The sample module wires the whole thing, and the exact file to read is `sample/src/main/kotlin/com/orhanobut/sample/MainActivity.kt`, which covers background writes and cleanup as well as the strategy itself. Point that strategy at app-private storage and you need no storage permission at all, which is the cleanest arrangement available. Leave `DiskLogAdapter()` as it ships and you land on the legacy shared external-storage location, which scoped storage restricts on current Android, and the setup notes send you back to that sample for a working replacement. Beyond the cleanup shown there, no file rotation size, no retention window and no redaction step are documented anywhere, so whatever your code prints is written verbatim into a file inside your own data directory. The library ships under Apache-2.0, which covers the code and says nothing about the content of your logs.

## Two ways in: direct adapter calls or a planted Timber tree

Two entry paths are documented, and they differ in who owns the tag. The direct path is the setup snippet: adapters registered on the static facade, and every call site naming Logger itself. The bridge path plants a tree instead, and the entire override forwards 4 arguments unchanged:

```kotlin
Timber.plant(object : Timber.DebugTree() {
  override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
    Logger.log(priority, tag, message, t)
  }
})
```

Destinations, formatting and the `isLoggable` gate stay in the adapter list either way, because both paths end at the same facade. What moves is the calling code. Under Timber the tag arrives from the tree rather than from `Logger.t`, so the per-thread prefix described earlier has no role on those lines, and priority follows whichever tree you plant. The snippet overrides a single method, so nothing in it alters Timber's own filtering rules, and the tree it plants is `Timber.DebugTree()`, a debug configuration rather than a production one. If your app already logs through Timber, sitting Logger behind that tree is less work than rewriting call sites; if you carry no Timber dependency, the direct path has fewer moving parts. For a destination the library does not ship, the documented route is a custom `DiskLogStrategy` or an anonymous adapter subclass, so you can drop the formatting layer entirely when plain lines are enough.

## Conclusion

Android developers who need collections, JSON or XML to stay readable while debugging on device can adopt this, and adding a destination costs one call in the Application class. Projects that already send every log through Timber should read the bridging snippet first, since tag and priority ownership moves to the planted tree. Filtering is a write-time override, so a debug-only gate has to be repeated on every adapter, and file output needs your own DiskLogStrategy pointed at app-private storage rather than the legacy external path.

## FAQ

### How do I add orhanobut/logger to an Android project?

Declare Google Maven and Maven Central in dependencyResolutionManagement, add implementation("com.orhanobut:logger:2.2.0"), then call Logger.addLogAdapter(AndroidLogAdapter()) once in your Application class. Logging calls work from any thread after that single initialization.

### How do I turn off orhanobut/logger output in release builds?

Override isLoggable on your adapter and return BuildConfig.DEBUG, which is what the README example does, because the check happens at write time rather than at build time. In Logcat you can also filter by the PRETTY_LOGGER tag or by the tag set on your format strategy.

### Can orhanobut/logger still write logs to a file on modern Android?

Yes, by pairing CsvFormatStrategy with your own DiskLogStrategy that targets app-private storage, which needs no storage permission. The default DiskLogAdapter() still uses the legacy shared external-storage location that scoped storage restricts, so the README points new applications at the sample module instead.

### How does orhanobut/logger work with Timber?

Plant a Timber.DebugTree whose log override forwards priority, tag, message and throwable into Logger.log unchanged. Destinations, formatting and filtering still belong to the adapters already registered on Logger, so Timber only decides what reaches them.

### What log levels and formats does orhanobut/logger support?

One-letter calls cover debug, error, warning, verbose, information and wtf, and format strings take arguments, for example Logger.d("hello %s", "world"). Lists, maps, arrays, JSON and XML have their own helpers, and all of those use debug priority.

### How do I tag a single log message with orhanobut/logger?

Call Logger.t("Checkout") before the message call, and the tag applies to that one message on the calling thread. With tag("MyApp") set on the strategy, the README shows the result rendered as MyApp-Screen.

## Sources

- [Issues](https://github.com/orhanobut/logger/issues)
- [License: Apache-2.0](https://github.com/orhanobut/logger/blob/master/LICENSE)
- [orhanobut/logger on GitHub](https://github.com/orhanobut/logger)
- [README](https://github.com/orhanobut/logger/blob/master/README.md)
- [Releases](https://github.com/orhanobut/logger/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/orhanobut-logger
