♻️ Refactor logging module

This commit is contained in:
LittleSheep 2025-02-27 23:30:08 +08:00
parent 32bf834108
commit ae9743c84f
15 changed files with 230 additions and 91 deletions

View File

@ -730,5 +730,6 @@
"forceUpdateDescription": "Force to show the application update popup, even the new version is not available.",
"debugLogging": "Runtime Logs",
"runtimeLogsOpen": "Open Logs",
"runtimeLogsDescription": "Show the runtime logs to help debugging."
"runtimeLogsDescription": "Show the runtime logs to help debugging.",
"signinResetPasswordHint": "Please enter the username / email address to help us to find your account and reset your password."
}

View File

@ -728,5 +728,6 @@
"forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。",
"runtimeLogs": "运行时日志",
"runtimeLogsOpen": "打开日志文件",
"runtimeLogsDescription": "显示运行时的日志记录。"
"runtimeLogsDescription": "显示运行时的日志记录。",
"signinResetPasswordHint": "请输入用户名/电子邮箱地址以帮助我们找到您的帐户并重置密码。"
}

View File

@ -728,5 +728,6 @@
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。",
"runtimeLogs": "運行時日誌",
"runtimeLogsOpen": "打開日誌文件",
"runtimeLogsDescription": "顯示運行時的日誌記錄。"
"runtimeLogsDescription": "顯示運行時的日誌記錄。",
"signinResetPasswordHint": "請輸入用户名/電子郵箱地址以幫助我們找到您的帳户並重置密碼。"
}

View File

@ -728,5 +728,6 @@
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。",
"runtimeLogs": "運行時日誌",
"runtimeLogsOpen": "打開日誌文件",
"runtimeLogsDescription": "顯示運行時的日誌記錄。"
"runtimeLogsDescription": "顯示運行時的日誌記錄。",
"signinResetPasswordHint": "請輸入用戶名/電子郵箱地址以幫助我們找到您的帳戶並重置密碼。"
}

View File

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:math' as math;
import 'package:dio/dio.dart';
@ -8,6 +7,7 @@ import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/database/database.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
@ -532,7 +532,7 @@ class ChatMessageController extends ChangeNotifier {
},
).toJson(),
));
log('[Messaging] Send read event request: $_readEventAnchor');
logging.debug('[Messaging] Send read event request: $_readEventAnchor');
}
@override

View File

@ -1,3 +1,10 @@
import 'package:talker/talker.dart';
final logging = Talker();
final logging = Talker(
settings: TalkerSettings(
enabled: true,
useHistory: true,
maxHistoryItems: 1000,
useConsoleLogs: true,
),
);

View File

@ -1,8 +1,8 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/link.dart';
@ -20,7 +20,7 @@ class SnLinkPreviewProvider {
final target = b64.encode(url);
if (_cache.containsKey(target)) return _cache[target];
log('[LinkPreview] Fetching $url ($target)');
logging.debug('[LinkPreview] Fetching $url ($target)');
try {
final resp = await _sn.client.get('/cgi/re/link/$target');
@ -28,7 +28,7 @@ class SnLinkPreviewProvider {
_cache[url] = meta;
return meta;
} catch (err) {
log('[LinkPreview] Failed to fetch $url ($target)...');
logging.warning('[LinkPreview] Failed to fetch $url ($target)...', err);
return null;
}
}

View File

@ -1,4 +1,3 @@
import 'dart:developer';
import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
@ -9,6 +8,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:local_notifier/local_notifier.dart';
import 'package:provider/provider.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
@ -48,11 +48,13 @@ class NotificationProvider extends ChangeNotifier {
var deviceUuid = await FlutterUdid.consistentUdid;
if (deviceUuid.isEmpty) {
log("Unable to active push notifications, couldn't get device uuid");
logging.warning(
'[Push Notification] Unable to active push notifications, couldn\'t get device uuid');
return;
} else {
log('Device UUID is $deviceUuid');
log('Registering device push notifications...');
logging.info('[Push Notification] Device UUID is $deviceUuid');
logging
.info('[Push Notification] Registering device push notifications...');
}
if (Platform.isIOS || Platform.isMacOS) {
@ -62,7 +64,7 @@ class NotificationProvider extends ChangeNotifier {
provider = 'fcm';
token = await FirebaseMessaging.instance.getToken();
}
log('Device Push Token is $token');
logging.info('[Push Notification] Device Push Token is $token');
await _sn.client.post(
'/cgi/id/notifications/subscription',

View File

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
@ -11,9 +10,12 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/widget.dart';
import 'package:synchronized/synchronized.dart';
import 'package:talker_dio_logger/talker_dio_logger_interceptor.dart';
import 'package:talker_dio_logger/talker_dio_logger_settings.dart';
const kNetworkServerDirectory = [
('Solar Network', 'https://api.sn.solsynth.dev'),
@ -36,6 +38,17 @@ class SnNetworkProvider {
client = Dio();
client.interceptors.add(
TalkerDioLogger(
talker: logging,
settings: const TalkerDioLoggerSettings(
printRequestHeaders: false,
printResponseHeaders: false,
printResponseMessage: true,
),
),
);
client.interceptors.add(RetryInterceptor(
dio: client,
retries: 3,
@ -69,7 +82,6 @@ class SnNetworkProvider {
_prefs = _config.prefs;
client.options.baseUrl = _config.serverUrl;
});
}
static Future<Dio> createOffContextClient() async {
@ -91,7 +103,8 @@ class SnNetworkProvider {
RequestOptions options,
RequestInterceptorHandler handler,
) async {
final atk = await _getFreshAtk(client, prefs.getString(kAtkStoreKey), prefs.getString(kRtkStoreKey), (atk, rtk) {
final atk = await _getFreshAtk(client, prefs.getString(kAtkStoreKey),
prefs.getString(kRtkStoreKey), (atk, rtk) {
prefs.setString(kAtkStoreKey, atk);
prefs.setString(kRtkStoreKey, rtk);
});
@ -103,7 +116,8 @@ class SnNetworkProvider {
},
),
);
client.options.baseUrl = prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
client.options.baseUrl =
prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
return client;
}
@ -119,7 +133,8 @@ class SnNetworkProvider {
platformInfo = 'Web; ${deviceInfo.vendor}';
} else if (Platform.isAndroid) {
final deviceInfo = await DeviceInfoPlugin().androidInfo;
platformInfo = 'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}';
platformInfo =
'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}';
} else if (Platform.isIOS) {
final deviceInfo = await DeviceInfoPlugin().iosInfo;
platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}';
@ -128,7 +143,8 @@ class SnNetworkProvider {
platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}';
} else if (Platform.isWindows) {
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
platformInfo = 'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}';
platformInfo =
'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}';
} else if (Platform.isLinux) {
final deviceInfo = await DeviceInfoPlugin().linuxInfo;
platformInfo = 'Linux; ${deviceInfo.prettyName}';
@ -148,12 +164,15 @@ class SnNetworkProvider {
final tkLock = Lock();
Future<String?> getFreshAtk() async {
return await _getFreshAtk(client, _prefs.getString(kAtkStoreKey), _prefs.getString(kRtkStoreKey), (atk, rtk) {
return await _getFreshAtk(
client, _prefs.getString(kAtkStoreKey), _prefs.getString(kRtkStoreKey),
(atk, rtk) {
setTokenPair(atk, rtk);
});
}
static Future<String?> _getFreshAtk(Dio client, String? atk, String? rtk, Function(String atk, String rtk)? onRefresh) async {
static Future<String?> _getFreshAtk(Dio client, String? atk, String? rtk,
Function(String atk, String rtk)? onRefresh) async {
if (_refreshCompleter != null) {
return await _refreshCompleter!.future;
} else {
@ -185,7 +204,8 @@ class SnNetworkProvider {
final payload = b64.decode(rawPayload);
final exp = jsonDecode(payload)['exp'];
if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) {
log('Access token need refresh, doing it at ${DateTime.now()}');
logging.debug(
'[Auth] Access token need refresh, doing it at ${DateTime.now()}');
final result = await _refreshToken(client.options.baseUrl, rtk);
if (result == null) {
atk = null;
@ -199,12 +219,12 @@ class SnNetworkProvider {
_refreshCompleter!.complete(atk);
return atk;
} else {
log('Access token refresh failed...');
logging.error('[Auth] Access token refresh failed...');
_refreshCompleter!.complete(null);
}
}
} catch (err) {
log('Failed to authenticate user: $err');
logging.error('[Auth] Failed to authenticate user...', err);
_refreshCompleter!.completeError(err);
} finally {
_refreshCompleter = null;
@ -237,7 +257,8 @@ class SnNetworkProvider {
return result.$1;
}
static Future<(String, String)?> _refreshToken(String baseUrl, String? rtk) async {
static Future<(String, String)?> _refreshToken(
String baseUrl, String? rtk) async {
if (rtk == null) return null;
final dio = Dio();

View File

@ -1,7 +1,6 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
@ -51,7 +50,7 @@ class SnStickerProvider {
return sticker;
} catch (err) {
_cache[alias] = null;
log('[Sticker] Failed to lookup sticker $alias: $err');
logging.warning('[Sticker] Failed to lookup sticker $alias', err);
}
return null;
@ -66,7 +65,7 @@ class SnStickerProvider {
_cacheSticker(sticker);
}
} catch (err) {
log('[Sticker] Failed to list stickers: $err');
logging.error('[Sticker] Failed to list stickers...', err);
rethrow;
}
}

View File

@ -1,8 +1,7 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/account.dart';
@ -30,8 +29,8 @@ class UserProvider extends ChangeNotifier {
notifyListeners();
refreshUser().then((value) async {
if (value != null) {
log('Logged in as @${value.name}');
log('Atk: ${await atk}');
logging.info('[Auth] Logged in as @${value.name}');
logging.debug('[Auth] Access token: ${await atk}');
}
});
}

View File

@ -1,9 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/websocket.dart';
@ -30,7 +30,7 @@ class WebSocketProvider extends ChangeNotifier {
if (isConnected) return;
if (!_ua.isAuthorized) return;
log('[WebSocket] Connecting to the server...');
logging.debug('[WebSocket] Connecting to the server...');
await connect();
}
@ -62,17 +62,14 @@ class WebSocketProvider extends ChangeNotifier {
await conn!.ready;
_wsStream = conn!.stream.asBroadcastStream();
listen();
log('[WebSocket] Connected to server!');
logging.info('[WebSocket] Connected to server!');
isConnected = true;
} catch (err) {
if (err is WebSocketChannelException) {
log('Failed to connect to websocket: ${(err.inner as dynamic).message}');
} else {
log('Failed to connect to websocket: $err');
}
logging.error('[WebSocket] Failed to connect to websocket...', err);
if (!noRetry) {
log('Retry connecting to websocket in 3 seconds...');
logging.warning(
'[WebSocket] Retry connecting to websocket in 3 seconds...');
return Future.delayed(
const Duration(seconds: 3),
() => connect(noRetry: true),
@ -100,7 +97,8 @@ class WebSocketProvider extends ChangeNotifier {
_wsStream!.listen(
(event) {
final packet = WebSocketPackage.fromJson(jsonDecode(event));
log('Websocket incoming message: ${packet.method} ${packet.message}');
logging.debug(
'[Websocket] Incoming message: ${packet.method} ${packet.message}');
pk.sink.add(packet);
},
onDone: () {

View File

@ -1,11 +1,14 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/logger.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:talker/talker.dart';
import 'package:talker_dio_logger/dio_logs.dart';
import 'package:talker_flutter/talker_flutter.dart';
final Map<LogLevel, IconData> kLogLevelIcons = {
LogLevel.error: Symbols.error,
@ -16,15 +19,6 @@ final Map<LogLevel, IconData> kLogLevelIcons = {
LogLevel.verbose: Symbols.info_i,
};
final Map<LogLevel, Color> kLogLevelColors = {
LogLevel.error: Colors.red,
LogLevel.critical: Colors.red,
LogLevel.warning: Colors.orange,
LogLevel.info: Colors.blue,
LogLevel.debug: Colors.green,
LogLevel.verbose: Colors.green,
};
final Map<LogLevel, bool> kLogLevelFilled = {
LogLevel.error: false,
LogLevel.critical: true,
@ -39,43 +33,132 @@ class DebugLoggingScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final talkerTheme = TalkerScreenTheme.fromTheme(Theme.of(context));
return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('debugLogging').tr(),
actions: [
IconButton(
onPressed: () {
logging.cleanHistory();
Navigator.pop(context);
},
icon: const Icon(Symbols.delete),
),
],
),
body: SelectionArea(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: logging.history.length,
itemBuilder: (context, index) {
final log = logging.history[index];
return ListTile(
leading: Icon(
kLogLevelIcons[log.logLevel ?? LogLevel.debug] ?? Symbols.help,
color: kLogLevelColors[log.logLevel ?? LogLevel.debug],
fill: (kLogLevelFilled[log.logLevel ?? LogLevel.debug] ?? false)
? 1
: 0,
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
body: ListView.builder(
reverse: true,
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
itemCount: logging.history.length,
itemBuilder: (context, index) {
final log = logging.history[index];
final color = log.getFlutterColor(talkerTheme);
return ListTile(
minTileHeight: 0,
tileColor: color.withOpacity(0.2),
leading: Icon(
kLogLevelIcons[log.logLevel ?? LogLevel.debug] ?? Symbols.help,
color: color,
fill: (kLogLevelFilled[log.logLevel ?? LogLevel.debug] ?? false)
? 1
: 0,
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (log is DioRequestLog)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${log.requestOptions.method} ${log.displayMessage}',
style: GoogleFonts.robotoMono(fontSize: 13),
),
Theme(
data: Theme.of(context).copyWith(
dividerColor: Colors.transparent,
),
child: ExpansionTile(
title: Text('Payload').fontSize(13),
minTileHeight: 0,
tilePadding: EdgeInsets.zero,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
log.requestOptions.data.toString(),
style: GoogleFonts.robotoMono(fontSize: 13),
),
],
),
),
],
)
else if (log is DioResponseLog)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${log.response.statusCode} ${log.displayMessage}',
style: GoogleFonts.robotoMono(fontSize: 13),
),
Theme(
data: Theme.of(context).copyWith(
dividerColor: Colors.transparent,
),
child: ExpansionTile(
title: Text('Payload').fontSize(13),
minTileHeight: 0,
tilePadding: EdgeInsets.zero,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
log.response.data.toString(),
style: GoogleFonts.robotoMono(fontSize: 13),
),
],
),
),
],
)
else
Text(
log.message ?? 'unknown'.tr(),
log.displayMessage,
style: GoogleFonts.robotoMono(fontSize: 13),
),
if (log.error != null)
Text(
log.error!.toString(),
style: GoogleFonts.robotoMono(fontSize: 13),
).bold(),
],
),
subtitle: Text(log.time.toString()).fontSize(11),
);
},
),
if (log.exception != null)
Text(
log.displayException,
style: GoogleFonts.robotoMono(fontSize: 13),
).bold(),
if (log.error != null)
Text(
log.displayException,
style: GoogleFonts.robotoMono(fontSize: 13),
).bold(),
if (log.stackTrace != null)
Text(
log.displayStackTrace,
style: GoogleFonts.robotoMono(fontSize: 12),
).padding(top: 4),
],
),
subtitle: Text(
'${(log.title?.replaceAll('-', ' ') ?? 'default').capitalizeEachWord()} · ${log.displayTime()}',
).fontSize(11),
onTap: () {
Clipboard.setData(
ClipboardData(
text: ['[${log.time}]', log.message, log.error?.toString()]
.where((ele) => ele != null)
.join('\n'),
),
);
},
);
},
),
);
}

View File

@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:developer';
import 'package:cross_file/cross_file.dart';
import 'package:easy_localization/easy_localization.dart';
@ -8,6 +7,7 @@ import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/logger.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:video_compress/video_compress.dart';
@ -17,10 +17,12 @@ class PendingVideoCompressDialog extends StatefulWidget {
const PendingVideoCompressDialog({super.key, required this.media});
@override
State<PendingVideoCompressDialog> createState() => _PendingVideoCompressDialogState();
State<PendingVideoCompressDialog> createState() =>
_PendingVideoCompressDialogState();
}
class _PendingVideoCompressDialogState extends State<PendingVideoCompressDialog> {
class _PendingVideoCompressDialogState
extends State<PendingVideoCompressDialog> {
VideoQuality _quality = VideoQuality.DefaultQuality;
bool _isBusy = false;
@ -50,7 +52,7 @@ class _PendingVideoCompressDialogState extends State<PendingVideoCompressDialog>
void initState() {
super.initState();
_progressSubscription = VideoCompress.compressProgress$.subscribe((event) {
log('[Compress] Progress: $event');
logging.debug('[Paperclip.VideoCompress] Progress: $event');
setState(() {
_progress = event / 100;
_isBusy = event < 100;
@ -132,7 +134,9 @@ class _PendingVideoCompressDialogState extends State<PendingVideoCompressDialog>
),
),
const Gap(8),
Text('attachmentCompressQualityHint', style: Theme.of(context).textTheme.bodySmall!).tr(),
Text('attachmentCompressQualityHint',
style: Theme.of(context).textTheme.bodySmall!)
.tr(),
if (_isBusy)
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _progress ?? 0),

View File

@ -133,7 +133,8 @@ extension AppPromptExtension on BuildContext {
),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrlString('https://kb.solsynth.dev/solar-network');
launchUrlString(
'https://kb.solsynth.dev/solar-network');
},
),
],
@ -157,7 +158,17 @@ extension ByteFormatter on int {
if (this == 0) return '0 Bytes';
const k = 1024;
final dm = decimals < 0 ? 0 : decimals;
final sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
final sizes = [
'Bytes',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB',
'EiB',
'ZiB',
'YiB'
];
final i = (math.log(this) / math.log(k)).floor().toInt();
return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
}
@ -167,4 +178,15 @@ extension StringFormatter on String {
String capitalize() {
return "${this[0].toUpperCase()}${substring(1)}";
}
String capitalizeEachWord() {
if (isEmpty) {
return this;
}
return split(' ')
.map((word) => word.isNotEmpty
? '${word[0].toUpperCase()}${word.substring(1)}'
: '')
.join(' ');
}
}