add unified debug and release logging

This commit is contained in:
ngfchl
2026-07-18 22:16:08 +08:00
parent b5e4b0e441
commit 6b202475d1
7 changed files with 276 additions and 3 deletions
+3 -3
View File
@@ -4,6 +4,7 @@ import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import '../config/app_config.dart';
import 'interceptors/app_log_interceptor.dart';
import 'interceptors/auth_interceptor.dart';
import 'interceptors/response_interceptor.dart';
@@ -20,15 +21,14 @@ class DioClient {
baseUrl: AppConfig.apiBase,
interceptors: [
_authInterceptor!,
AppLogInterceptor(),
ResponseInterceptor(),
],
);
accountDio = _createDio(
baseUrl: AppConfig.accountBase,
interceptors: [
ResponseInterceptor(),
],
interceptors: [AppLogInterceptor(), ResponseInterceptor()],
);
}
@@ -0,0 +1,34 @@
import 'package:dio/dio.dart';
import '../../logging/app_logger.dart';
class AppLogInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
AppLogger.debug(
'HTTP',
'${options.method} ${options.uri.path}${options.uri.hasQuery ? '?${options.uri.query}' : ''}',
);
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
AppLogger.debug(
'HTTP',
'${response.statusCode ?? 0} ${response.requestOptions.method} ${response.requestOptions.uri.path}',
);
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
AppLogger.error(
'HTTP',
'${err.requestOptions.method} ${err.requestOptions.uri.path} 失败',
error: err.message,
stackTrace: err.stackTrace,
);
handler.next(err);
}
}
+114
View File
@@ -0,0 +1,114 @@
import 'dart:async';
import 'dart:developer' as developer;
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
enum AppLogLevel { debug, info, warning, error }
class AppLogEntry {
final DateTime timestamp;
final AppLogLevel level;
final String scope;
final String message;
const AppLogEntry({
required this.timestamp,
required this.level,
required this.scope,
required this.message,
});
String get text =>
'${timestamp.toIso8601String()} [${level.name.toUpperCase()}] '
'[$scope] $message';
}
/// One log sink for debug console, release file diagnostics and the in-app log
/// viewer. Never write request headers or authorization tokens here.
class AppLogger {
static const _maxEntries = 1200;
static final entries = ValueNotifier<List<AppLogEntry>>(const []);
static File? _file;
static Future<void> _pendingWrite = Future.value();
static Future<void> initialize() async {
try {
final directory = await getApplicationSupportDirectory();
final logsDirectory = Directory(path.join(directory.path, 'logs'));
await logsDirectory.create(recursive: true);
_file = File(path.join(logsDirectory.path, 'guangya.log'));
if (await _file!.exists() && await _file!.length() > 4 * 1024 * 1024) {
await _file!.rename(
path.join(logsDirectory.path, 'guangya.previous.log'),
);
}
info('App', '日志中心已初始化:${_file!.path}');
} catch (error) {
developer.log('日志中心初始化失败:$error', name: 'Guangya');
}
}
static void debug(String scope, String message) =>
_write(AppLogLevel.debug, scope, message);
static void info(String scope, String message) =>
_write(AppLogLevel.info, scope, message);
static void warning(String scope, String message) =>
_write(AppLogLevel.warning, scope, message);
static void error(
String scope,
String message, {
Object? error,
StackTrace? stackTrace,
}) => _write(
AppLogLevel.error,
scope,
error == null ? message : '$message$error',
stackTrace: stackTrace,
);
static void clear() => entries.value = const [];
static void _write(
AppLogLevel level,
String scope,
String message, {
StackTrace? stackTrace,
}) {
if (kReleaseMode && level == AppLogLevel.debug) return;
final entry = AppLogEntry(
timestamp: DateTime.now(),
level: level,
scope: scope,
message: message,
);
final next = [...entries.value, entry];
entries.value = next.length > _maxEntries
? next.sublist(next.length - _maxEntries)
: next;
developer.log(
entry.text,
name: 'Guangya',
level: switch (level) {
AppLogLevel.debug => 500,
AppLogLevel.info => 800,
AppLogLevel.warning => 900,
AppLogLevel.error => 1000,
},
stackTrace: stackTrace,
);
final file = _file;
if (file == null) return;
_pendingWrite = _pendingWrite
.then((_) async {
await file.writeAsString(
'${entry.text}${stackTrace == null ? '' : '\n$stackTrace'}\n',
mode: FileMode.append,
flush: false,
);
})
.catchError((_) {});
}
}
+15
View File
@@ -1,9 +1,11 @@
import 'dart:io';
import 'dart:ui' show PlatformDispatcher;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_manager/window_manager.dart';
import 'package:media_kit/media_kit.dart';
import 'core/storage/storage_manager.dart';
import 'core/logging/app_logger.dart';
import 'core/http/dio_client.dart';
import 'app/app.dart';
@@ -13,6 +15,19 @@ void main() async {
// Init Hive for persistent storage
await StorageManager.init();
await AppLogger.initialize();
FlutterError.onError = (details) {
AppLogger.error(
'Flutter',
details.exceptionAsString(),
stackTrace: details.stack,
);
FlutterError.presentError(details);
};
PlatformDispatcher.instance.onError = (error, stackTrace) {
AppLogger.error('Dart', '未捕获异常', error: error, stackTrace: stackTrace);
return true;
};
// Init Dio HTTP client
DioClient.init();
+25
View File
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../providers/theme_provider.dart';
import '../providers/media_library_provider.dart';
import '../widgets/app_log_dialog.dart';
import '../core/storage/storage_manager.dart';
class SettingsDialog extends ConsumerStatefulWidget {
@@ -306,6 +307,30 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
const SizedBox(height: 16),
const ShadSeparator.horizontal(),
const SizedBox(height: 16),
Text(
'诊断',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: cs.foreground,
),
),
const SizedBox(height: 12),
_SettingsRow(
icon: Icons.subject_rounded,
label: '运行日志',
child: ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: () => showShadDialog(
context: context,
builder: (_) => const AppLogDialog(),
),
child: const Text('查看'),
),
),
const SizedBox(height: 16),
const ShadSeparator.horizontal(),
const SizedBox(height: 16),
Text(
'关于',
style: TextStyle(
@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter_riverpod/legacy.dart';
import '../api/guangya_api.dart';
import '../core/logging/app_logger.dart';
import '../core/storage/file_metadata_cache.dart';
import '../core/storage/media_library_store.dart';
import '../core/storage/storage_manager.dart';
@@ -619,6 +620,11 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
}
void _appendScanLog(String message, {bool isError = false}) {
if (isError) {
AppLogger.error('Media', message);
} else {
AppLogger.info('Media', message);
}
final logs = [
...state.scanLogs,
MediaLibraryScanLog(
+79
View File
@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../core/logging/app_logger.dart';
class AppLogDialog extends StatelessWidget {
const AppLogDialog({super.key});
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
return ShadDialog(
title: const Text('运行日志'),
description: const Text('实时显示当前会话日志;RELEASE 同时写入本地日志文件。'),
actions: [
ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: AppLogger.clear,
leading: const Icon(Icons.delete_outline_rounded, size: 16),
child: const Text('清空显示'),
),
ShadButton(
size: ShadButtonSize.sm,
onPressed: () => Navigator.of(context).pop(),
child: const Text('关闭'),
),
],
child: SizedBox(
width: 840,
height: 520,
child: ValueListenableBuilder<List<AppLogEntry>>(
valueListenable: AppLogger.entries,
builder: (_, entries, _) {
if (entries.isEmpty) {
return Center(
child: Text(
'暂无运行日志',
style: TextStyle(color: cs.mutedForeground),
),
);
}
return Container(
decoration: BoxDecoration(
color: cs.muted.withValues(alpha: 0.45),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: cs.border),
),
child: ListView.builder(
padding: const EdgeInsets.all(10),
itemCount: entries.length,
itemBuilder: (_, index) {
final entry = entries[entries.length - index - 1];
final color = switch (entry.level) {
AppLogLevel.debug => cs.mutedForeground,
AppLogLevel.info => cs.foreground,
AppLogLevel.warning => cs.primary,
AppLogLevel.error => cs.destructive,
};
return Padding(
padding: const EdgeInsets.only(bottom: 7),
child: SelectableText(
entry.text,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 11,
height: 1.35,
color: color,
),
),
);
},
),
);
},
),
),
);
}
}