Skip to main content

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.

LevelShips in Release?What it carries
ErroryesA fault.
Warnyes — the defaultSomething the author asked for was dropped, or a host contract was bent.
InfonoSuccess narration (boot lines such as "native init ok", "mounted …").
DebugnoDeveloper detail, including full exception stacks.
VerbosenoPer-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"'
warning
On a real device, Debug and Verbose need one more switch

Both 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);
}