Logging
BlazorNative routes its diagnostics through one level-gated seam that is quiet by default and consistent across the .NET runtime and both native shells. In a Release build you get warnings and errors only; the boot narration, per-frame tracing and developer detail are suppressed unless you turn the level up.
You never see any of this on screen — there is no on-device console. Logging is for you, at a terminal, with the device or simulator attached.
The levels
The managed enum is BlazorNative.Core.BnLogLevel;
the Android (io.blazornative.jni.BnLogLevel) and iOS (BnLogLevel) shells mirror the
same ordinals byte-for-byte, because the level crosses the ABI at boot.
| Level | Ships in Release? | What it carries |
|---|---|---|
Error | yes | A fault. |
Warn | yes — the default | Something the author asked for was dropped, or a host contract was bent. |
Info | no | Success narration (boot lines such as "native init ok", "mounted …"). |
Debug | no | Developer detail, including full exception stacks. |
Verbose | no | Per-frame / per-patch tracing. |
Setting the threshold to a level emits that level and everything more severe. The
default is Warn — deliberately a runtime default, not a #if DEBUG switch, so a
Release build can be asked for one verbose session without a rebuild and without the two
configurations' code paths diverging.
A name that is unrecognised or absent resolves to the default (Warn) — a typo never
silently turns logging off.
Raising the level
You can raise verbosity three ways. On each shell a per-launch override beats the app's
declared default, which beats the runtime default, and the shell hands the result to .NET at
boot. A managed BnLog.Level assignment made from your ConfigureServices callback runs
later than that — when the first page mounts — so it replaces the level .NET was booted with,
for the rest of the session. It does not reach back into the shells: their own narration keeps
the level they resolved, and on iOS the shell's stderr pump re-applies that level to the lines
.NET writes, so a managed raise shows up there only once the shell's level is raised too.
From managed code — BnLog.Level
BnLog.Level is a public setter on BlazorNative.Core. Set it anywhere — most naturally
from your app's BlazorNativeApp.ConfigureServices callback — when a consumer wants
verbosity for a session without touching the shell:
using BlazorNative.Core;
// e.g. inside the callback you pass to BlazorNativeApp.ConfigureServices:
BnLog.Level = BnLogLevel.Debug;
Assigning BnLogLevel.Unset or an out-of-range value resolves back to the default
(Warn). This is the same threshold the runtime's own renderer logging flows through, so
raising it turns up the framework's diagnostics as a whole.
On Android — manifest meta-data or an adb Intent extra
App-wide default (ships with the APK). Add a <meta-data> to your
AndroidManifest.xml, on the <application> (or on the <activity>, which takes
precedence):
<meta-data android:name="io.blazornative.logLevel" android:value="Debug" />
The value is a BnLogLevel name — Error, Warn, Info, Debug or Verbose
(case-insensitive). An unparseable value falls back to the quiet default.
One launch only (no rebuild). Pass the EXTRA_LOG_LEVEL Intent extra, which has the
highest precedence:
adb shell am start -e io.blazornative.shell.EXTRA_LOG_LEVEL Debug \
-n com.example.myapp/io.blazornative.shell.MainActivity
Framework lines reach logcat through the shell's stderr pump, under the tag
BlazorNative/<category>. The shell's own narration — the [BOOT] lines, the
[deep-link] routes and the bridge's navigate — is written directly by the Kotlin
shell under the plain BlazorNative tag, and since
#200 it obeys the same
threshold: it is Info, so it is silent at the default Warn and returns as soon as you
raise the level with either knob above. Filter for both:
adb logcat | grep BlazorNative
On iOS — Info.plist or the BN_LOG_LEVEL environment variable
App-wide default (ships with the build). Add a string to your Info.plist:
<key>io.blazornative.logLevel</key>
<string>Debug</string>
One launch only (no rebuild). Set the BN_LOG_LEVEL environment variable — from your
Xcode scheme's Run → Arguments → Environment Variables, or when launching a simulator:
xcrun simctl launch --console --env BN_LOG_LEVEL=Debug booted com.example.myapp
BN_LOG_LEVEL takes precedence over the Info.plist value. The iOS shell logs through
os_log / Logger under the subsystem io.blazornative, so you can stream and filter
the whole framework from a terminal:
log stream --predicate 'subsystem == "io.blazornative"'
Debug and Verbose need one more switchBoth map onto OSLogType.debug, which the unified log drops unless the subsystem is
enabled — and log config has no --device flag. The one route that works needs
OS_ACTIVITY_DT_MODE=YES and devicectl --console; the recipe is on the
iOS shell page. Everything above is simulator-accurate as written.
Note that iOS redacts message payloads by default in logs collected off-device
(<private>); only compile-time-constant text (and the framework version) is written in
the clear. This is deliberate — it keeps app, user and exception data out of the unified
log unless a call site explicitly marks it safe.
Android redacts the same way
Android mirrors that rule. In a non-debuggable build (no FLAG_DEBUGGABLE), every
shell log line and every .NET line forwarded through the stderr pump writes its text as
<private>. A message the shell marks SAFE, which is a constant, keeps its text, and a
throwable is dropped, so no stack trace reaches logcat. The level and the category stay
readable, so you can still see that an error happened and which part of the framework
raised it. The shell's own warnings and errors follow the same rule.
A debuggable build, one with FLAG_DEBUGGABLE set, shows everything: the text of every
line and the stack of every throwable.
The switch is the shell log's reveal flag. The Android shell's main activity sets it in
onCreate from the app's FLAG_DEBUGGABLE, before the stderr pump is installed, so
even the earliest .NET line follows the rule.
What a line looks like
The default sink tags the level into the text so the shells' stderr pumps can map a line
back onto android.util.Log / os_log:
[BN|E|renderer] <message>
The single character after BN| is the level (E/W/I/D/V); the token after it is
the category.
Custom sinks
For structured logging (Serilog, OpenTelemetry, JSON-per-line), assign
BnLog.Sink — an Action<BnLogLevel, string, string> (level, category, message). It is
the narrowest extension point that works; the framework does not host an
ILoggerProvider ecosystem. A sink that throws is swallowed, because a logger that faults
its caller is worse than a quiet one.
using BlazorNative.Core;
// MyLogger stands for your own structured-logging sink — Serilog, OpenTelemetry, or similar.
internal static class MyLogger
{
internal static void Write(string level, string category, string message) { }
}
internal static class MyLoggingSetup
{
internal static void Configure() =>
BnLog.Sink = (level, category, message) =>
MyLogger.Write(level.ToString(), category, message);
}