Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f6aa77c
Add Rust-to-Dart logging bridge
fzyzcjy May 10, 2026
d67df46
Document Dart logging bridge helper
fzyzcjy May 10, 2026
ac8b05c
Allow disabling default Dart logging output
fzyzcjy May 10, 2026
6a6ac38
Document Rust-to-Dart logging bridge
fzyzcjy May 10, 2026
1e861c6
Allow user-crate path in logging macro
fzyzcjy May 10, 2026
00f818a
Add logging bridge tests
fzyzcjy May 10, 2026
db04128
Test logging macro expansion
fzyzcjy May 10, 2026
4dcfd96
Update logging lockfiles
fzyzcjy May 10, 2026
426945a
Organize logging guide sections
fzyzcjy May 10, 2026
6f467cd
Restore logging guide examples
fzyzcjy May 10, 2026
3ff52d1
Clarify logging defaults
fzyzcjy May 10, 2026
5fa33f1
Rename Rust-to-Dart logging macro
fzyzcjy May 10, 2026
8b73962
Clarify logging overview
fzyzcjy May 10, 2026
2c8036a
Simplify Rust-to-Dart logging example
fzyzcjy May 10, 2026
d71e4d7
Clarify Dart logging customization
fzyzcjy May 10, 2026
17d73fb
Trim logging option wording
fzyzcjy May 10, 2026
3ab1dfb
Remove combined logging example
fzyzcjy May 10, 2026
f6b3f5c
Keep log dependency out of template
fzyzcjy May 10, 2026
df2e42c
Use named logging max levels
fzyzcjy May 10, 2026
3e4a5a0
Use generic Dart initializer for logging
fzyzcjy May 10, 2026
e91fd16
Remove logging codegen hook placeholder
fzyzcjy May 10, 2026
2aefb29
Extract logging max level parser
fzyzcjy May 10, 2026
f9b512a
Handle logging stream errors
fzyzcjy May 10, 2026
89f1575
Merge branch 'master' into codex/logging-overhaul-redesign
fzyzcjy May 16, 2026
babef63
Fix logging review comments
fzyzcjy May 16, 2026
cd9d1e1
Support custom Dart initializer code
fzyzcjy May 16, 2026
256c265
Add logging bridge e2e test
fzyzcjy May 16, 2026
7ae89a2
Fix logging initializer generation
fzyzcjy May 16, 2026
25f5f64
Merge remote logging branch updates
fzyzcjy May 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frb_dart/lib/flutter_rust_bridge_for_generated.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ library;

export 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'
if (dart.library.js_interop) 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
export 'package:flutter_rust_bridge/src/logging/frb_logging.dart'
show FrbDartLogging, FrbLogRecordData;
102 changes: 102 additions & 0 deletions frb_dart/lib/src/logging/frb_logging.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// ignore_for_file: avoid_print

import 'dart:async';

import 'package:logging/logging.dart';

/// A normalized Rust log record received through flutter_rust_bridge.
class FrbLogRecordData {
/// The Rust `log` level name, for example `INFO` or `WARN`.
final String level;

/// The formatted log message.
final String message;

/// The Rust log target.
final String target;

/// The Rust module path, when available.
final String? modulePath;

/// The Rust source file, when available.
final String? file;

/// The Rust source line, when available.
final int? line;

/// Creates a normalized log record for Dart-side processing.
const FrbLogRecordData({
required this.level,
required this.message,
required this.target,
required this.modulePath,
required this.file,
required this.line,
});
}

/// Installs the Dart side of the Rust `log` to Dart `logging` bridge.
class FrbDartLogging {
static StreamSubscription<Object?>? _subscription;
static StreamSubscription<LogRecord>? _defaultOutputSubscription;

/// Connects a generated Rust log stream to the Dart `logging` package.
static void init<T>({
required Stream<T> rustLogStream,
required FrbLogRecordData Function(T record) mapRecord,
bool setupDefaultOutput = true,
}) {
if (_subscription != null) {
throw StateError('FRB logging should not be initialized twice');
}

_subscription = rustLogStream.listen(
(record) {
final mapped = mapRecord(record);
Logger(mapped.target).log(_toDartLevel(mapped.level), mapped.message);
},
onError: (Object error, StackTrace stackTrace) {
Logger(
'flutter_rust_bridge.logging',
).severe('Error in Rust log stream', error, stackTrace);
},
);

if (setupDefaultOutput) {
_setupDefaultOutput();
}
}

/// Disconnects the Rust log stream listener.
static Future<void> dispose() async {
await _subscription?.cancel();
_subscription = null;
}

static void _setupDefaultOutput() {
_defaultOutputSubscription ??= Logger.root.onRecord.listen((record) {
final loggerName = record.loggerName.isEmpty ? 'root' : record.loggerName;
print(
'${record.level.name}: ${record.time}: $loggerName: ${record.message}',
);
});
}

static Level _toDartLevel(String level) {
switch (level.toUpperCase()) {
case 'TRACE':
return Level.FINER;
case 'DEBUG':
return Level.FINE;
case 'INFO':
return Level.INFO;
case 'WARN':
case 'WARNING':
return Level.WARNING;
case 'ERROR':
return Level.SEVERE;
default:
return Level.INFO;
}
}
}
1 change: 1 addition & 0 deletions frb_dart/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies:
async: ^2.11.0
build_cli_annotations: ^2.1.0
collection: ^1.16.0
logging: ^1.3.0
meta: ^1.3.0
path: ^1.8.1
web: '>=0.5.0 <2.0.0'
Expand Down
149 changes: 149 additions & 0 deletions frb_dart/test/frb_logging_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import 'dart:async';

import 'package:flutter_rust_bridge/src/logging/frb_logging.dart';
import 'package:logging/logging.dart';
import 'package:test/test.dart';

void main() {
tearDown(() async {
await FrbDartLogging.dispose();
});

test('Rust log records are forwarded to Dart logging', () async {
final controller = StreamController<_RustLogRecord>();
final receivedRecords = <LogRecord>[];
final previousLevel = Logger.root.level;
final subscription = Logger.root.onRecord.listen(receivedRecords.add);
Logger.root.level = Level.ALL;

try {
FrbDartLogging.init<_RustLogRecord>(
rustLogStream: controller.stream,
setupDefaultOutput: false,
mapRecord: (record) => FrbLogRecordData(
level: record.level,
message: record.message,
target: record.target,
modulePath: null,
file: null,
line: null,
),
);

controller.add(
const _RustLogRecord(
level: 'WARN',
message: 'disk almost full',
target: 'rust.storage',
),
);
await pumpEventQueue();

expect(receivedRecords, hasLength(1));
expect(receivedRecords.single.level, Level.WARNING);
expect(receivedRecords.single.message, 'disk almost full');
expect(receivedRecords.single.loggerName, 'rust.storage');
} finally {
Logger.root.level = previousLevel;
await subscription.cancel();
await controller.close();
}
});

test('Rust log stream errors are forwarded to Dart logging', () async {
final controller = StreamController<_RustLogRecord>();
final receivedRecords = <LogRecord>[];
final previousLevel = Logger.root.level;
final subscription = Logger.root.onRecord.listen(receivedRecords.add);
Logger.root.level = Level.ALL;

try {
FrbDartLogging.init<_RustLogRecord>(
rustLogStream: controller.stream,
setupDefaultOutput: false,
mapRecord: (record) => FrbLogRecordData(
level: record.level,
message: record.message,
target: record.target,
modulePath: null,
file: null,
line: null,
),
);

final error = StateError('stream failed');
controller.addError(error, StackTrace.current);
await pumpEventQueue();

expect(receivedRecords, hasLength(1));
expect(receivedRecords.single.level, Level.SEVERE);
expect(receivedRecords.single.message, 'Error in Rust log stream');
expect(receivedRecords.single.error, same(error));
expect(receivedRecords.single.loggerName, 'flutter_rust_bridge.logging');
} finally {
Logger.root.level = previousLevel;
await subscription.cancel();
await controller.close();
}
});

test('Rust log levels are mapped to idiomatic Dart logging levels', () async {
final controller = StreamController<_RustLogRecord>();
final receivedRecords = <LogRecord>[];
final previousLevel = Logger.root.level;
final subscription = Logger.root.onRecord.listen(receivedRecords.add);
Logger.root.level = Level.ALL;

try {
FrbDartLogging.init<_RustLogRecord>(
rustLogStream: controller.stream,
setupDefaultOutput: false,
mapRecord: (record) => FrbLogRecordData(
level: record.level,
message: record.message,
target: record.target,
modulePath: null,
file: null,
line: null,
),
);

controller.add(
const _RustLogRecord(
level: 'TRACE',
message: 'trace message',
target: 'rust.trace',
),
);
controller.add(
const _RustLogRecord(
level: 'DEBUG',
message: 'debug message',
target: 'rust.debug',
),
);
await pumpEventQueue();

expect(receivedRecords.map((record) => record.level), [
Level.FINER,
Level.FINE,
]);
} finally {
Logger.root.level = previousLevel;
await subscription.cancel();
await controller.close();
}
});
}

class _RustLogRecord {
final String level;
final String message;
final String target;

const _RustLogRecord({
required this.level,
required this.message,
required this.target,
});
}
55 changes: 55 additions & 0 deletions frb_example/dart_minimal/lib/src/rust/api/minimal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,60 @@
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';

// These functions are ignored because they are not marked as `pub`: `frb_parse_logging_max_level`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `FrbDartLogger`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `enabled`, `flush`, `fmt`, `log`

Stream<FrbLogRecord> frbInitLogger({required String maxLevel}) =>
RustLib.instance.api.crateApiMinimalFrbInitLogger(maxLevel: maxLevel);

String frbLoggingMaxLevel() =>
RustLib.instance.api.crateApiMinimalFrbLoggingMaxLevel();

bool frbLoggingSetupDartLoggingOutput() =>
RustLib.instance.api.crateApiMinimalFrbLoggingSetupDartLoggingOutput();

Future<int> minimalAdder({required int a, required int b}) =>
RustLib.instance.api.crateApiMinimalMinimalAdder(a: a, b: b);

Future<void> emitLogMessage() =>
RustLib.instance.api.crateApiMinimalEmitLogMessage();

class FrbLogRecord {
final String level;
final String message;
final String target;
final String? modulePath;
final String? file;
final int? line;

const FrbLogRecord({
required this.level,
required this.message,
required this.target,
this.modulePath,
this.file,
this.line,
});

@override
int get hashCode =>
level.hashCode ^
message.hashCode ^
target.hashCode ^
modulePath.hashCode ^
file.hashCode ^
line.hashCode;

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FrbLogRecord &&
runtimeType == other.runtimeType &&
level == other.level &&
message == other.message &&
target == other.target &&
modulePath == other.modulePath &&
file == other.file &&
line == other.line;
}
Loading
Loading