Android: one logcat tag for the Rust side, and say what it is
Build Debug APK / build (push) Successful in 5m1s
Build Debug APK / build (push) Successful in 5m1s
Forwarded records were tagged with the Rust module path they came from, so `adb logcat -s <the app>` returned silence and a raised verbosity looked broken. The per-module tags that cost us buy nothing back: logcat's filterspec splits at the first colon, so `iroh_gossip::net:V` parses as tag `iroh_gossip` with priority `:` and is rejected — a tag containing `::` is unaddressable. So: one fixed tag for everything crossing the FFI boundary, with the record's target moved to the head of the message where grep still reaches it. The backend is held privately rather than installed via `android_logger::init_once`, which would take the `log` global and cut our runtime filtering out of the path. The tag is exported over uniffi and shown under Log Verbosity in Settings — which tag to filter on is not guessable from outside the process.
This commit is contained in:
@@ -372,29 +372,17 @@ since it isn't worth taking before it shows up in a profile.
|
||||
`iroh_gossip::net - dial failed: No addressing information available` for an Android peer
|
||||
it knows about via the ensemble's pkarr registry.
|
||||
- **How to confirm**: check whether Android ever logs successful DHT publish/resolve
|
||||
activity from `iroh_mainline_address_lookup`, or only silence. Quicker still: `adb shell`
|
||||
into the emulator and see whether an arbitrary raw UDP send/receive succeeds at all.
|
||||
activity from `iroh_mainline_address_lookup`, or only silence — with "Show iroh logs" +
|
||||
Verbose on in Settings, `adb logcat -s synchronicity | grep mainline` (every record
|
||||
forwarded from Rust carries that one tag, with the module path at the head of the
|
||||
message). Quicker still: `adb shell` into the emulator and see whether an arbitrary raw
|
||||
UDP send/receive succeeds at all.
|
||||
- **If confirmed**: the DoH trick doesn't transfer (DHT isn't HTTP), so likely a TCP-based
|
||||
DHT bootstrap/proxy, or disabling `DhtAddressLookup` on Android in favour of the working
|
||||
DoH path plus persisted/gossip relay-URL caching, or emulator network config. **Testing
|
||||
on a physical device is the fastest way to rule this in or out** — if a real phone
|
||||
bootstraps via DHT, the bug is emulator-specific and low priority.
|
||||
|
||||
### Android logcat filtering: forwarded Rust logs are tagged by module path
|
||||
- **Description**: `ConfigurableAndroidLogger` (`rust/lib/src/log.rs`) forwards records to
|
||||
`android_logger::log()` with the default `Config` (no `tag`). Without one,
|
||||
`android_logger` uses each record's Rust module path as the logcat tag
|
||||
(`iroh_gossip::net`, `synchronicity_lib::p2p::gossip`) rather than a fixed app tag.
|
||||
- **Impact**: filtering logcat by tag or package name shows nothing, regardless of the
|
||||
app's verbosity settings — which *are* wired correctly. The likely explanation for "I
|
||||
don't see any iroh logs even with verbose logging on."
|
||||
- **Workaround**: `adb logcat | grep -iE "iroh|gossip|synchronicity"`, no tag filter, with
|
||||
"Show iroh logs" + Verbose enabled in Settings.
|
||||
- **Possible fix**: set one fixed tag via `android_logger`'s `Config` and prepend the
|
||||
module path into the message body. Trade-off: loses free per-module tag filtering for one
|
||||
greppable tag — probably a net win, but worth deciding deliberately rather than as a side
|
||||
effect of a bug fix.
|
||||
|
||||
### Android error surfacing, and the general sync-status signal it's part of
|
||||
- **The general gap**: per-peer gossip reachability is visible (Connected/Degraded badges
|
||||
on the Ensemble screen), but there is still no user-facing "a sync is happening / just
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -31,6 +32,7 @@ import com.gregshuflin.synchronicity.ui.theme.ThemeState
|
||||
import kotlinx.coroutines.launch
|
||||
import uniffi.synchronicity.LogVerbosity
|
||||
import uniffi.synchronicity.ThemeMode
|
||||
import uniffi.synchronicity.androidLogTag
|
||||
|
||||
private const val TAG = "SettingsView"
|
||||
|
||||
@@ -207,6 +209,20 @@ fun SettingsView(
|
||||
},
|
||||
)
|
||||
}
|
||||
// Which tag to filter on is not guessable from outside: everything the Rust side
|
||||
// logs — ours and iroh's alike — arrives under one tag, so say what it is rather
|
||||
// than leaving a raised verbosity looking like it did nothing.
|
||||
val logTag = remember { androidLogTag() }
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text =
|
||||
"The Rust side logs to logcat under one tag: $logTag. " +
|
||||
"Filter with: adb logcat -s $logTag",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f),
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
}
|
||||
|
||||
// Timezone picker
|
||||
SectionHeading(
|
||||
|
||||
@@ -322,6 +322,14 @@ pub fn update_android_log_filter(verbosity: LogVerbosity, show_iroh_logs: bool)
|
||||
crate::log::update_android_log_filter(crate::log::LogConfig { verbosity, show_iroh_logs });
|
||||
}
|
||||
|
||||
/// The logcat tag every record forwarded from Rust carries —
|
||||
/// [`crate::log::ANDROID_LOG_TAG`]. Exported so the settings screen can tell
|
||||
/// the user what to filter on without keeping its own copy of the string.
|
||||
#[uniffi::export]
|
||||
pub fn android_log_tag() -> String {
|
||||
crate::log::ANDROID_LOG_TAG.to_string()
|
||||
}
|
||||
|
||||
/// Called by the JVM when the shared library is first loaded. Captures the
|
||||
/// JavaVM pointer into a static so initAndroidContext can use it without
|
||||
/// calling GetJavaVM through jni-sys 0.4's macro-transformed union API.
|
||||
|
||||
+83
-2
@@ -148,8 +148,62 @@ fn android_effective_level(config: &LogConfig, target: &str) -> log::LevelFilter
|
||||
}
|
||||
}
|
||||
|
||||
/// The logcat tag every forwarded Rust record carries.
|
||||
///
|
||||
/// # Why one fixed tag
|
||||
///
|
||||
/// Left to itself `android_logger` tags each record with the Rust module path
|
||||
/// it came from — `iroh_gossip::net`, `synchronicity::p2p::gossip`. That reads
|
||||
/// fine in a full dump and is useless for filtering, for two reasons. There is
|
||||
/// no tag to guess: a person who reaches for `adb logcat -s <the app>` gets
|
||||
/// silence and concludes the verbosity setting is broken. And the per-module
|
||||
/// tags it trades that away for cannot be selected anyway — logcat's
|
||||
/// filterspec is `tag:priority`, split at the *first* colon, so
|
||||
/// `iroh_gossip::net:V` parses as tag `iroh_gossip` with priority `:` and is
|
||||
/// rejected as malformed. Any tag containing `::` is unaddressable.
|
||||
///
|
||||
/// So: one tag for the whole Rust side, with the module path moved into the
|
||||
/// message where `grep` can still reach it. `adb logcat -s synchronicity`
|
||||
/// shows every forwarded record; `... | grep iroh_gossip` narrows it. Kotlin
|
||||
/// keeps its own per-class tags — this is only about records crossing the FFI
|
||||
/// boundary from Rust.
|
||||
///
|
||||
/// It shares its spelling with [`OWN_TARGET_PREFIX`] because both come from the
|
||||
/// lib target's name, but they are not the same thing: that one selects *our*
|
||||
/// records out of everyone's, this one labels every record we forward.
|
||||
pub const ANDROID_LOG_TAG: &str = "synchronicity";
|
||||
|
||||
/// Writes the body of a logcat line: the record's target, then its message.
|
||||
///
|
||||
/// The target — not `module_path!()`, which `android_logger` would otherwise
|
||||
/// use — because the target is what [`android_effective_level`] filters on, so
|
||||
/// what a reader greps for is exactly what decided whether the line appears.
|
||||
fn write_android_message(out: &mut dyn std::fmt::Write, record: &log::Record) -> std::fmt::Result {
|
||||
write!(out, "{}: {}", record.target(), record.args())
|
||||
}
|
||||
|
||||
/// The `android_logger` backend, configured with our fixed tag and message
|
||||
/// format. Held privately rather than installed via `android_logger::init_once`,
|
||||
/// which would race with us for the `log` crate's single global logger slot and
|
||||
/// take our runtime filtering out of the path.
|
||||
static ANDROID_BACKEND: std::sync::OnceLock<android_logger::AndroidLogger> =
|
||||
std::sync::OnceLock::new();
|
||||
|
||||
fn android_backend() -> &'static android_logger::AndroidLogger {
|
||||
ANDROID_BACKEND.get_or_init(|| {
|
||||
android_logger::AndroidLogger::new(
|
||||
android_logger::Config::default()
|
||||
// Filtering is [`ConfigurableAndroidLogger`]'s job; this backend
|
||||
// must not apply a second, unconfigurable one on top of it.
|
||||
.with_max_level(log::LevelFilter::Trace)
|
||||
.with_tag(ANDROID_LOG_TAG)
|
||||
.format(write_android_message),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// A `log::Log` implementation that pre-filters records using a runtime-updatable
|
||||
/// `LogConfig` and forwards accepted records to `android_logger::log`, which
|
||||
/// `LogConfig` and forwards accepted records to an `android_logger` backend, which
|
||||
/// writes to the Android logcat buffer.
|
||||
struct ConfigurableAndroidLogger;
|
||||
|
||||
@@ -161,7 +215,7 @@ impl log::Log for ConfigurableAndroidLogger {
|
||||
|
||||
fn log(&self, record: &log::Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
android_logger::log(record);
|
||||
log::Log::log(android_backend(), record);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +265,33 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The module path has to survive somewhere once it stops being the tag,
|
||||
/// or a single-tag logcat dump loses the only marker of where a line came
|
||||
/// from — and with it the ability to tell an iroh line from one of ours.
|
||||
#[test]
|
||||
fn the_module_path_moves_into_the_message() {
|
||||
let mut line = String::new();
|
||||
write_android_message(
|
||||
&mut line,
|
||||
&log::Record::builder()
|
||||
.target("iroh_gossip::net")
|
||||
.level(log::Level::Info)
|
||||
.args(format_args!("dial failed"))
|
||||
.build(),
|
||||
)
|
||||
.expect("writing to a String cannot fail");
|
||||
assert_eq!(line, "iroh_gossip::net: dial failed");
|
||||
}
|
||||
|
||||
/// A tag containing `::` cannot appear in a logcat filterspec at all — it
|
||||
/// is split at the first colon and rejected as a bad priority — so the one
|
||||
/// tag we do use must be plainly selectable.
|
||||
#[test]
|
||||
fn the_fixed_tag_is_addressable_by_logcat() {
|
||||
assert!(!ANDROID_LOG_TAG.contains(':'), "`adb logcat -s {ANDROID_LOG_TAG}` must parse");
|
||||
assert!(!ANDROID_LOG_TAG.is_empty());
|
||||
}
|
||||
|
||||
/// The floor is ours alone: iroh is the reason the default is low, and
|
||||
/// lifting it here would reopen exactly the spam that motivated it.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user