Skip to content

Commit f229a37

Browse files
committed
log: experimental android logcat integ
1 parent ec863a3 commit f229a37

4 files changed

Lines changed: 174 additions & 4 deletions

File tree

intra/common.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,6 @@ func makeIPPorts(ips []netip.Addr, origipp netip.AddrPort, maybeIncludeOrig bool
557557
log.VV("com: makeIPPorts(v4? %t, v6? %t) for %v; tot: %d; in: %v, out: %v",
558558
use4, use6, origipp, len(ips), ips, r)
559559
}
560-
561560
if len(r) > 0 {
562561
s := core.ShuffleInPlace(r)
563562
if willIncludeOrig {

intra/log/logger.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ type simpleLogger struct {
8181

8282
o *golog.Logger
8383
e *golog.Logger
84+
x *xlog // may be used instead of golog o/e
8485
q *ring[string] // todo: use []byte instead of string for gc?
8586

8687
clock
@@ -255,12 +256,16 @@ func defaultLogger() *simpleLogger {
255256
// github.com/golang/mobile/blob/fa72addaaa/internal/mobileinit/mobileinit_android.go#L74-L92
256257
e: golog.New(os.Stderr, "", defaultFlags),
257258
o: golog.New(os.Stdout, "", defaultFlags),
259+
x: &xlog{}, // pipes output to logcat on Android, to fmt.Print otherwise
258260
q: newRing[string](context.TODO(), qSize),
259261
}
262+
if runtime.GOOS == "android" {
263+
golog.SetOutput(l.x)
264+
}
260265
return l
261266
}
262267

263-
// NewLogger creates a new Glogger with the given tag.
268+
// NewLogger creates a new Logger with the given tag.
264269
func NewLogger(tag string) *simpleLogger {
265270
l := defaultLogger()
266271
if len(tag) <= 0 { // if tag is empty, leave it as is
@@ -559,15 +564,23 @@ func (l *simpleLogger) msgstr(lvl LogLevel, f string, args ...any) (msg string)
559564
// out logs to stdout and pushes msg into ring buffer.
560565
// ref: github.com/golang/mobile/blob/c713f31d/internal/mobileinit/mobileinit_android.go#L51
561566
func (l *simpleLogger) out(msg string) {
562-
_ = l.o.Output(0 /*not used*/, msg) // may error
567+
if runtime.GOOS == "android" {
568+
l.x.Write([]byte(msg))
569+
} else {
570+
_ = l.o.Output(0 /*not used*/, msg) // may error
571+
}
563572
l.q.Push(msg)
564573
}
565574

566575
// err logs to stderr and pushes msg into ring buffer.
567576
func (l *simpleLogger) err(at int, msg string) {
568577
_, file := caller(at + nextframe)
569578
msg = file + msg
570-
_ = l.e.Output(0 /*unused*/, msg) // may error
579+
if runtime.GOOS == "android" {
580+
l.x.Write([]byte(msg))
581+
} else {
582+
_ = l.e.Output(0 /*unused*/, msg) // may error
583+
}
571584
l.q.Push(msg)
572585
}
573586

@@ -644,6 +657,35 @@ func tracecaller(s string) bool {
644657
return true
645658
}
646659

660+
// splitmsg parses the two-character level prefix prepended by msgstr
661+
// ("D ", "I ", "W ", "E ") and returns the corresponding LogLevel
662+
// together with the message with that prefix stripped.
663+
// If the prefix is not recognised, INFO and the original slice are returned.
664+
func splitmsg(p []byte) (LogLevel, Logmsg) {
665+
if len(p) >= 2 && p[1] == ' ' {
666+
switch p[0] {
667+
case 'Y':
668+
return VVERBOSE, Logmsg(p[2:])
669+
case 'V':
670+
return VERBOSE, Logmsg(p[2:])
671+
case 'D':
672+
return DEBUG, Logmsg(p[2:])
673+
case 'I':
674+
return INFO, Logmsg(p[2:])
675+
case 'W':
676+
return WARN, Logmsg(p[2:])
677+
case 'E':
678+
return ERROR, Logmsg(p[2:])
679+
case 'F':
680+
return STACKTRACE, Logmsg(p[2:])
681+
case 'U':
682+
return USR, Logmsg(p[2:])
683+
}
684+
}
685+
return INFO, Logmsg(p)
686+
}
687+
688+
// shortfile strips the last path component from a file path.
647689
func shortfile(file string) string {
648690
if i := strings.LastIndexByte(file, '/'); i >= 0 {
649691
file = file[i+1:]

intra/log/xlog_android.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) 2026 RethinkDNS and its authors.
2+
//
3+
// This Source Code Form is subject to the terms of the Mozilla Public
4+
// License, v. 2.0. If a copy of the MPL was not distributed with this
5+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
//
7+
// This file incorporates work covered by the following copyright and
8+
// permission notice:
9+
//
10+
// Copyright 2014 The Go Authors. All rights reserved.
11+
// Use of this source code is governed by a BSD-style
12+
// license that can be found in the LICENSE file.
13+
14+
//go:build android && cgo
15+
16+
package log
17+
18+
// adb logcat Firestack:D Firestack:I Firestack:W Firestack:E *:S
19+
20+
/*
21+
#cgo LDFLAGS: -landroid -llog
22+
23+
#include <stdarg.h>
24+
#include <stdlib.h>
25+
#include <android/log.h>
26+
*/
27+
import "C"
28+
29+
import (
30+
"io"
31+
"strings"
32+
"unsafe"
33+
)
34+
35+
// ctag is the logcat tag used for all log output.
36+
var ctag = C.CString("Firestack")
37+
38+
// xlog is a Console implementation that routes log entries to Android logcat
39+
// using the appropriate log priority for each LogLevel:
40+
//
41+
// - VVERBOSE / VERBOSE / DEBUG = ANDROID_LOG_DEBUG
42+
// - INFO = ANDROID_LOG_INFO
43+
// - WARN = ANDROID_LOG_WARN
44+
// - ERROR / STACKTRACE = ANDROID_LOG_ERROR
45+
// - USR = ANDROID_LOG_INFO
46+
type xlog struct{}
47+
48+
var _ Console = (*xlog)(nil)
49+
var _ io.Writer = (*xlog)(nil)
50+
51+
// NewAndroidConsole returns a Console that writes to Android logcat.
52+
func NewAndroidConsole() Console {
53+
return &xlog{}
54+
}
55+
56+
// Log implements Console.
57+
func (a *xlog) Log(level LogLevel, msg Logmsg) {
58+
if len(msg) <= 0 {
59+
return
60+
}
61+
cstr := C.CString(string(msg))
62+
C.__android_log_write(androidPriority(level), ctag, cstr)
63+
C.free(unsafe.Pointer(cstr))
64+
}
65+
66+
func (a *xlog) Write(p []byte) (n int, err error) {
67+
s := strings.TrimRight(string(p), "\n\r")
68+
for line := range strings.SplitSeq(s, "\n") {
69+
lvl, msg := splitmsg([]byte(line))
70+
if len(msg) > 0 {
71+
a.Log(lvl, msg)
72+
}
73+
}
74+
return len(p), nil
75+
}
76+
77+
// androidPriority maps a LogLevel to the corresponding Android log priority.
78+
func androidPriority(level LogLevel) C.int {
79+
switch level {
80+
case VVERBOSE, VERBOSE, DEBUG:
81+
return C.ANDROID_LOG_DEBUG
82+
case INFO, USR:
83+
return C.ANDROID_LOG_INFO
84+
case WARN:
85+
return C.ANDROID_LOG_WARN
86+
case ERROR, STACKTRACE:
87+
return C.ANDROID_LOG_ERROR
88+
default:
89+
return C.ANDROID_LOG_DEBUG
90+
}
91+
}

intra/log/xlog_other.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2026 RethinkDNS and its authors.
2+
//
3+
// This Source Code Form is subject to the terms of the Mozilla Public
4+
// License, v. 2.0. If a copy of the MPL was not distributed with this
5+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
7+
//go:build !(android && cgo)
8+
9+
package log
10+
11+
import (
12+
"fmt"
13+
"io"
14+
"strings"
15+
)
16+
17+
// xlog pipes logs to logcat on Android; to fmt.Print otherwise.
18+
type xlog struct{}
19+
20+
var _ Console = (*xlog)(nil)
21+
var _ io.Writer = (*xlog)(nil)
22+
23+
// Log implements Console.
24+
func (a *xlog) Log(level LogLevel, msg Logmsg) {
25+
fmt.Printf("%s\n", msg)
26+
}
27+
28+
// Write implements io.Writer.
29+
func (a *xlog) Write(p []byte) (n int, err error) {
30+
s := strings.TrimRight(string(p), "\n\r")
31+
for line := range strings.SplitSeq(s, "\n") {
32+
lvl, msg := splitmsg([]byte(line))
33+
if len(msg) > 0 {
34+
a.Log(lvl, msg)
35+
}
36+
}
37+
return len(p), nil
38+
}

0 commit comments

Comments
 (0)