add unified debug and release logging
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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((_) {});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user