// Global structured logging bus for BadNote. // // Always-on (unlike the old PDF-only DiagnosticLogger opt-in). Writes NDJSON // lines to a rotating session file under the app documents directory so a // Surface user can export a diagnostic pack without attaching a debugger. import 'dart:async'; import 'dart:convert'; import 'dart:developer' as developer; import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; enum LogLevel { trace, debug, info, warn, error } /// Known subsystems — keep the set small so filters stay useful. abstract final class LogSubsystem { static const shell = 'shell'; static const ink = 'ink'; static const arbiter = 'arbiter'; static const penNative = 'pen_native'; static const pdf = 'pdf'; static const office = 'office'; static const board = 'board'; static const sync = 'sync'; static const diag = 'diag'; static const frame = 'frame'; } class BadNoteLog { BadNoteLog._(); static final BadNoteLog instance = BadNoteLog._(); final String sessionId = const Uuid().v4(); final List> _ring = >[]; static const int _ringCap = 4000; File? _file; Directory? _dir; Timer? _flushTimer; final List _pending = []; bool _started = false; LogLevel minLevel = LogLevel.debug; /// Absolute path of the current session log, once [start] succeeds. String? get path => _file?.path; Directory? get directory => _dir; Future start() async { if (_started) return; _started = true; try { Directory base; try { base = await getApplicationDocumentsDirectory(); } catch (_) { base = await getTemporaryDirectory(); } _dir = Directory( '${base.path}${Platform.pathSeparator}badnote_diagnostics', ); if (!await _dir!.exists()) { await _dir!.create(recursive: true); } final stamp = DateTime.now() .toIso8601String() .replaceAll(':', '-') .replaceAll('.', '-'); _file = File( '${_dir!.path}${Platform.pathSeparator}session_$stamp.ndjson', ); await _file!.writeAsString( '${jsonEncode({ 'ts': DateTime.now().toIso8601String(), 'level': 'info', 'subsystem': LogSubsystem.diag, 'msg': 'session_start', 'sessionId': sessionId, 'platform': Platform.operatingSystem, 'osVersion': Platform.operatingSystemVersion, })}\n', flush: true, ); _flushTimer = Timer.periodic(const Duration(seconds: 1), (_) => _flush()); info(LogSubsystem.diag, 'log file ready', fields: {'path': _file!.path}); } catch (e) { // Logging must never crash the app. developer.log('BadNoteLog start failed: $e', name: 'badnote'); } } void trace(String subsystem, String msg, {Map? fields}) => _emit(LogLevel.trace, subsystem, msg, fields); void debug(String subsystem, String msg, {Map? fields}) => _emit(LogLevel.debug, subsystem, msg, fields); void info(String subsystem, String msg, {Map? fields}) => _emit(LogLevel.info, subsystem, msg, fields); void warn(String subsystem, String msg, {Map? fields}) => _emit(LogLevel.warn, subsystem, msg, fields); void error(String subsystem, String msg, {Map? fields}) => _emit(LogLevel.error, subsystem, msg, fields); void _emit( LogLevel level, String subsystem, String msg, Map? fields, ) { if (level.index < minLevel.index) return; final entry = { 'ts': DateTime.now().toIso8601String(), 'level': level.name, 'subsystem': subsystem, 'msg': msg, 'sessionId': sessionId, if (fields != null) ...fields, }; _ring.add(entry); if (_ring.length > _ringCap) { _ring.removeRange(0, _ring.length - _ringCap); } final line = jsonEncode(entry); developer.log(line, name: 'badnote.$subsystem'); if (_file != null) { _pending.add(line); if (_pending.length >= 200) { unawaited(_flush()); } } } Future _flush() async { final file = _file; if (file == null || _pending.isEmpty) return; final chunk = '${_pending.join('\n')}\n'; _pending.clear(); try { await file.writeAsString(chunk, mode: FileMode.append, flush: true); } catch (_) {} } /// Snapshot of the in-memory ring (newest last). List> snapshotRing() => List>.unmodifiable(_ring); Future flush() => _flush(); Future stop() async { _flushTimer?.cancel(); _flushTimer = null; await _flush(); } } /// Bridge for legacy call sites that still use plain strings. void logLegacyInputLine(String line) { BadNoteLog.instance.debug(LogSubsystem.penNative, line); }