Logging on a vehicle sits between two hard constraints. A defect may occur once, in a customer's car, in a country you will never visit — so you want detail. And flash memory has a write budget that must last fifteen years — so you cannot have much of it.
Where logs actually go#
adb logcat -b main # app logs
adb logcat -b system # framework
adb logcat -b crash # crashes only
adb logcat -b events # structured system events
adb logcat -b kernel # dmesg
adb logcat -b all
adb logcat -g # how big is each buffer?
adb logcat -G 64M # make them bigger before a long reproductionLevels, and what they should actually mean#
| Level | Use for | In production |
|---|---|---|
VERBOSE | Tracing individual values | Compiled out |
DEBUG | Development detail | Off |
INFO | State changes worth knowing | On |
WARN | Something unexpected, recovered | On |
ERROR | Something failed | On |
ASSERT/wtf | Should be impossible | On |
What to log#
State transitions. "Charging started", "user switched to 11", "VHAL connected". These are the skeleton of any incident reconstruction.
Decisions and their reason. Not "request rejected" but "request rejected: level 9 exceeds max 5". The reason is the whole value.
Boundary crossings. What you sent to another process or another ECU, and what came back — with a sequence number. This is what settles cross-team disputes.
Failures, with context. The error, the inputs, and what you did about it.
What not to log#
// Bad
Log.i(TAG, "navigating to ${destination.address}")
// Better — useful for debugging, not a location record
Log.i(TAG, "navigating: ${destination.hashCode()} (${route.distanceKm} km)")Making verbose logging free#
private const val DEBUG = false // set from BuildConfig
fun onPropertyChanged(id: Int, value: Any) {
// Constant condition — the compiler removes the whole block.
if (DEBUG) Log.v(TAG, "property $id -> $value")
}if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "expensive ${buildDiagnosticString()}")
}adb shell setprop log.tag.VegaComfort VERBOSENative logging#
#define LOG_TAG "VegaVHAL"
#include <android-base/logging.h>
LOG(INFO) << "connected, " << config_count << " properties";
LOG(WARNING) << "property " << prop_id << " unavailable";
PLOG(ERROR) << "cannot open " << path; // appends the errno descriptionPLOG is worth knowing — it adds the system error automatically, which turns
"cannot open /dev/can0" into "cannot open /dev/can0: Permission denied" and often
identifies the problem on its own.
Structured events, for things that are counted#
For events you will want to aggregate across a fleet, a log line is the wrong shape. The event log is structured and machine-readable.
Persistent logging, carefully#
Sometimes you genuinely need logs to survive a reboot — an intermittent fault that reboots the head unit.
adb shell setprop persist.logd.logpersistd logcatd # on
adb shell setprop persist.logd.logpersistd "" # off
adb shell ls -la /data/misc/logd/A strategy that holds up#
- Default to logcat. It is a memory ring buffer and costs no flash.
INFOand above are the incident report. Keep them meaningful.- Compile out verbose logging, or guard it with
isLoggable. - Never log personal data. Redact or hash.
- Log decisions with reasons, and boundary crossings with sequence numbers.
- Structured events for anything you will count across a fleet.
- Persistent logging is a targeted tool, not a default.
- Raise the buffer size before a long reproduction, not after.
Next#
Compliance, and the suites that decide whether any of this can ship.

