Every slog line the app wrote on Android went to /dev/null, including the one naming the error it was about to os.Exit on. #52 is what that cost: a process that vanished with no tombstone, no AndroidRuntime stack and nothing in `logcat -b crash`, at Priority/Critical for months, whose entire diagnosis was one sLogger.Error main.go was already writing. backend/androidlog is a slog.Handler over __android_log_write, chosen in main() by build tag rather than by a runtime check so that a desktop binary links no cgo for a platform it cannot run on. **Everything except the write itself is untagged.** That is androidpayload.go's discipline pushed as far as it goes: the only toolchain that compiles the android tag is a cross-compiler and the only thing that runs it is a phone, so the priority mapping, the formatting, the chunking and the handler's own attr and group bookkeeping are ordinary Go that `go test` exercises everywhere, and android.go is fifteen lines that hand a string to liblog. Four things in it are load-bearing. **The tag is a fixed string, not the application id.** The debug build carries `applicationIdSuffix ".dev"` so it can be installed beside the release app, and it is the only build whose WebView can be inspected -- so a tag derived from the id is a different tag on the one build anybody debugging this app is running, and the filter meant to show these lines would hide them exactly where they were being looked for. **The priorities are android/log.h's own values, asserted twice.** android.go carries constant expressions that do not compile as uint if the header renumbers; the untagged test writes the six numbers out longhand, because comparing a constant to itself passes on any renumbering. A wrong priority is the failure that hides rather than breaks -- logcat prints whatever number it is handed, so an Error filed as Info is present, correct, and invisible to every filter. **Formatting is delegated to slog's TextHandler.** WithAttrs and WithGroup are the half of slog.Handler that is easy to get subtly wrong, and a logger whose groups are wrong is a logger nobody reads. The derived handlers share the parent's buffer *and its mutex*: a second mutex would guard nothing, and two loggers derived from one would splice their bytes into a single line under load. **A line is chunked, because liblog drops what does not fit.** The kernel logger's entry is 4068 bytes for tag and message together and the remainder goes without comment, so a long record would be truncated in the middle of the thing worth reading. Time and level are dropped from the formatted line, since logcat stamps every entry with both -- and dropping them by *key* also ate a caller's own "level" attribute, which the on-device probe caught and TestACallersOwnLevelAttrSurvives now holds. ReplaceAttr sees an empty group path for the built-ins and for every top-level attribute alike, so the kinds are what separate them. Verified on the reference device (TLP301, Android 14): a debug build logs I/W/E under the `yellowjacket` tag at the right priorities, and the first thing it surfaced was a real warning nobody could previously see -- `champion index rebuild failed ... disk I/O error (6410)`. Closes #160
56 lines
1.6 KiB
Go
56 lines
1.6 KiB
Go
//go:build android
|
|
|
|
// The write itself, and nothing else. Everything decidable off a phone
|
|
// is in androidlog.go; see the package comment for why.
|
|
|
|
package androidlog
|
|
|
|
/*
|
|
#cgo LDFLAGS: -llog
|
|
#include <stdlib.h>
|
|
#include <android/log.h>
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"log/slog"
|
|
"unsafe"
|
|
)
|
|
|
|
// The priorities in androidlog.go are android/log.h's own values, and
|
|
// these are what says so. A constant expression that would be negative
|
|
// does not compile as a uint, so a renumbered header fails the build
|
|
// here rather than logging everything at the wrong severity -- which is
|
|
// the failure that would otherwise be invisible, since logcat would
|
|
// happily print whatever number it was handed.
|
|
const (
|
|
_ = uint(C.ANDROID_LOG_VERBOSE - PrioVerbose)
|
|
_ = uint(PrioVerbose - C.ANDROID_LOG_VERBOSE)
|
|
_ = uint(C.ANDROID_LOG_DEBUG - PrioDebug)
|
|
_ = uint(PrioDebug - C.ANDROID_LOG_DEBUG)
|
|
_ = uint(C.ANDROID_LOG_INFO - PrioInfo)
|
|
_ = uint(PrioInfo - C.ANDROID_LOG_INFO)
|
|
_ = uint(C.ANDROID_LOG_WARN - PrioWarn)
|
|
_ = uint(PrioWarn - C.ANDROID_LOG_WARN)
|
|
_ = uint(C.ANDROID_LOG_ERROR - PrioError)
|
|
_ = uint(PrioError - C.ANDROID_LOG_ERROR)
|
|
_ = uint(C.ANDROID_LOG_FATAL - PrioFatal)
|
|
_ = uint(PrioFatal - C.ANDROID_LOG_FATAL)
|
|
)
|
|
|
|
// New returns the handler main() installs on Android.
|
|
func New(opts *slog.HandlerOptions) slog.Handler {
|
|
return NewHandler(opts, write)
|
|
}
|
|
|
|
// write hands one line to liblog.
|
|
func write(prio int, tag, msg string) {
|
|
cTag := C.CString(tag)
|
|
defer C.free(unsafe.Pointer(cTag))
|
|
|
|
cMsg := C.CString(msg)
|
|
defer C.free(unsafe.Pointer(cMsg))
|
|
|
|
C.__android_log_write(C.int(prio), cTag, cMsg)
|
|
}
|