From bb9e3bf91b7e7829e5a611d2f1c113b974c48e39 Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:17:23 +0800 Subject: [PATCH] feat(logging): add logging module skeleton --- apps/lib/core/logging/log_config.dart | 27 ++++ apps/lib/core/logging/log_entry.dart | 71 ++++++++ apps/lib/core/logging/log_file_handler.dart | 36 +++++ apps/lib/core/logging/log_service.dart | 171 ++++++++++++++++++++ apps/lib/core/logging/logger.dart | 70 ++++++++ 5 files changed, 375 insertions(+) create mode 100644 apps/lib/core/logging/log_config.dart create mode 100644 apps/lib/core/logging/log_entry.dart create mode 100644 apps/lib/core/logging/log_file_handler.dart create mode 100644 apps/lib/core/logging/log_service.dart create mode 100644 apps/lib/core/logging/logger.dart diff --git a/apps/lib/core/logging/log_config.dart b/apps/lib/core/logging/log_config.dart new file mode 100644 index 0000000..f5a46e4 --- /dev/null +++ b/apps/lib/core/logging/log_config.dart @@ -0,0 +1,27 @@ +import 'log_entry.dart'; + +enum LogOutput { console, file } + +class LogConfig { + final LogLevel minLevel; + final LogOutput output; + final String logFileName; + final String logDir; + + const LogConfig({ + this.minLevel = LogLevel.debug, + this.output = LogOutput.console, + this.logFileName = 'app.log', + this.logDir = 'logs', + }); + + static LogConfig forDebug() => + const LogConfig(minLevel: LogLevel.debug, output: LogOutput.console); + + static LogConfig forRelease() => const LogConfig( + minLevel: LogLevel.warning, + output: LogOutput.file, + logFileName: 'app.log', + logDir: 'logs', + ); +} diff --git a/apps/lib/core/logging/log_entry.dart b/apps/lib/core/logging/log_entry.dart new file mode 100644 index 0000000..378e1ac --- /dev/null +++ b/apps/lib/core/logging/log_entry.dart @@ -0,0 +1,71 @@ +enum LogLevel { debug, info, warning, error } + +class LogEntry { + final DateTime timestamp; + final LogLevel level; + final String message; + final String module; + final String? funcName; + final int? lineNo; + final String? errorType; + final String? stackTrace; + final Map? extra; + + LogEntry({ + required this.timestamp, + required this.level, + required this.message, + required this.module, + this.funcName, + this.lineNo, + this.errorType, + this.stackTrace, + this.extra, + }); + + Map toJson() => { + 'timestamp': timestamp.toIso8601String(), + 'level': level.name, + 'message': message, + 'module': module, + if (funcName != null) 'func_name': funcName, + if (lineNo != null) 'line_no': lineNo, + if (errorType != null) 'error_type': errorType, + if (stackTrace != null) 'stack_trace': stackTrace, + if (extra != null && extra!.isNotEmpty) 'extra': extra, + }; + + String toConsoleString() { + final ts = timestamp.toIso8601String(); + final location = [ + if (funcName != null) funcName, + if (lineNo != null) '@$lineNo', + ].join(''); + final locationStr = location.isNotEmpty ? ' [$location]' : ''; + final errorStr = errorType != null ? ' [$errorType]' : ''; + final extraStr = extra != null && extra!.isNotEmpty ? ' $extra' : ''; + return '$ts ${level.name.toUpperCase().padRight(7)} [$module$locationStr]$errorStr $message$extraStr'; + } + + String toFileString() { + final sb = StringBuffer(); + sb.writeln('[$timestamp] ${level.name.toUpperCase()} [$module]'); + if (funcName != null || lineNo != null) { + sb.write(' at $funcName' ?? ''); + if (lineNo != null) sb.write(':$lineNo'); + sb.writeln(); + } + sb.writeln(' $message'); + if (errorType != null) { + sb.writeln(' Error: $errorType'); + } + if (stackTrace != null) { + sb.writeln(' StackTrace:'); + sb.writeln(stackTrace); + } + if (extra != null && extra!.isNotEmpty) { + sb.writeln(' Extra: $extra'); + } + return sb.toString(); + } +} diff --git a/apps/lib/core/logging/log_file_handler.dart b/apps/lib/core/logging/log_file_handler.dart new file mode 100644 index 0000000..a9a0aa3 --- /dev/null +++ b/apps/lib/core/logging/log_file_handler.dart @@ -0,0 +1,36 @@ +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; + +class LogFileHandler { + File? _file; + IOSink? _sink; + + Future init(String logDir, String logFileName) async { + final dir = await getApplicationDocumentsDirectory(); + final logPath = '${dir.path}/$logDir'; + await Directory(logPath).create(recursive: true); + _file = File('$logPath/$logFileName'); + _sink = _file!.openWrite(mode: FileMode.append); + } + + void write(String content) { + _sink?.writeln(content); + } + + Future flush() async { + await _sink?.flush(); + } + + Future close() async { + await _sink?.close(); + _sink = null; + _file = null; + } + + Future> readAllLines() async { + if (_file == null || !await _file!.exists()) return []; + return await _file!.readAsLines(); + } + + String? get filePath => _file?.path; +} diff --git a/apps/lib/core/logging/log_service.dart b/apps/lib/core/logging/log_service.dart new file mode 100644 index 0000000..22d4e38 --- /dev/null +++ b/apps/lib/core/logging/log_service.dart @@ -0,0 +1,171 @@ +import 'package:flutter/foundation.dart'; +import 'log_config.dart'; +import 'log_entry.dart'; +import 'log_file_handler.dart'; + +class LogService { + final LogConfig _config; + LogFileHandler? _fileHandler; + final _buffer = []; + static const _maxBufferSize = 50; + + LogService._({required LogConfig config}) : _config = config; + + static Future create({LogConfig? config}) async { + final isRelease = kReleaseMode; + + final effectiveConfig = + config ?? (isRelease ? LogConfig.forRelease() : LogConfig.forDebug()); + + final service = LogService._(config: effectiveConfig); + + if (effectiveConfig.output == LogOutput.file) { + service._fileHandler = LogFileHandler(); + await service._fileHandler!.init( + effectiveConfig.logDir, + effectiveConfig.logFileName, + ); + } + + return service; + } + + String? get logFilePath => _fileHandler?.filePath; + + void _log(LogEntry entry) { + if (entry.level.index < _config.minLevel.index) return; + + if (_config.output == LogOutput.console) { + debugPrint(entry.toConsoleString()); + if (entry.stackTrace != null) { + debugPrint(entry.stackTrace!); + } + } else { + _buffer.add(entry.toFileString()); + if (_buffer.length >= _maxBufferSize) { + _flushBuffer(); + } + } + } + + void _flushBuffer() { + for (final line in _buffer) { + _fileHandler?.write(line); + } + _buffer.clear(); + _fileHandler?.flush(); + } + + (String?, int?) _extractLocation(StackTrace stackTrace) { + final frames = stackTrace.toString().split('\n'); + for (final frame in frames) { + if (frame.contains('.dart')) { + final match = RegExp( + r'#\d+\s+(.+?)\s+\((.+?):(\d+)\)', + ).firstMatch(frame); + if (match != null) { + return (match.group(1), int.tryParse(match.group(3) ?? '')); + } + } + } + return (null, null); + } + + void debug({ + required String message, + required String module, + Map? extra, + StackTrace? stackTrace, + }) { + final trace = stackTrace ?? StackTrace.current; + final (funcName, lineNo) = _extractLocation(trace); + _log( + LogEntry( + timestamp: DateTime.now(), + level: LogLevel.debug, + message: message, + module: module, + funcName: funcName, + lineNo: lineNo, + extra: extra, + stackTrace: trace.toString(), + ), + ); + } + + void info({ + required String message, + required String module, + required Map extra, + StackTrace? stackTrace, + }) { + final trace = stackTrace ?? StackTrace.current; + final (funcName, lineNo) = _extractLocation(trace); + _log( + LogEntry( + timestamp: DateTime.now(), + level: LogLevel.info, + message: message, + module: module, + funcName: funcName, + lineNo: lineNo, + extra: extra, + stackTrace: trace.toString(), + ), + ); + } + + void warning({ + required String message, + required String module, + required Map extra, + StackTrace? stackTrace, + }) { + final trace = stackTrace ?? StackTrace.current; + final (funcName, lineNo) = _extractLocation(trace); + _log( + LogEntry( + timestamp: DateTime.now(), + level: LogLevel.warning, + message: message, + module: module, + funcName: funcName, + lineNo: lineNo, + extra: extra, + stackTrace: trace.toString(), + ), + ); + } + + void error({ + required String message, + required Object error, + required StackTrace stackTrace, + required String module, + Map? extra, + }) { + final (funcName, lineNo) = _extractLocation(stackTrace); + _log( + LogEntry( + timestamp: DateTime.now(), + level: LogLevel.error, + message: message, + module: module, + funcName: funcName, + lineNo: lineNo, + errorType: error.runtimeType.toString(), + stackTrace: stackTrace.toString(), + extra: extra, + ), + ); + } + + void flush() { + _flushBuffer(); + _fileHandler?.flush(); + } + + Future> readLogs() async { + return await _fileHandler?.readAllLines() ?? []; + } +} diff --git a/apps/lib/core/logging/logger.dart b/apps/lib/core/logging/logger.dart new file mode 100644 index 0000000..4b1343b --- /dev/null +++ b/apps/lib/core/logging/logger.dart @@ -0,0 +1,70 @@ +import 'log_service.dart'; + +LogService? _globalLogService; + +void setLogService(LogService service) { + _globalLogService = service; +} + +LogService _ensureService() { + return _globalLogService ?? (throw StateError('LogService not initialized')); +} + +class Logger { + final String module; + final LogService _service; + + Logger(this.module, this._service); + + factory Logger.get(String module) { + return Logger(module, _ensureService()); + } + + void debug({ + required String message, + Map? extra, + StackTrace? stackTrace, + }) => _service.debug( + message: message, + module: module, + extra: extra ?? {}, + stackTrace: stackTrace, + ); + + void info({ + required String message, + required Map extra, + StackTrace? stackTrace, + }) => _service.info( + message: message, + module: module, + extra: extra, + stackTrace: stackTrace, + ); + + void warning({ + required String message, + required Map extra, + StackTrace? stackTrace, + }) => _service.warning( + message: message, + module: module, + extra: extra, + stackTrace: stackTrace, + ); + + void error({ + required String message, + required Object error, + required StackTrace stackTrace, + Map? extra, + }) => _service.error( + message: message, + error: error, + stackTrace: stackTrace, + module: module, + extra: extra, + ); +} + +Logger getLogger(String module) => Logger.get(module);