diff --git a/lib/app/data/core/platform/http_client/http_client.dart b/lib/app/data/core/platform/http_client/http_client.dart index 68631de..731313b 100644 --- a/lib/app/data/core/platform/http_client/http_client.dart +++ b/lib/app/data/core/platform/http_client/http_client.dart @@ -1,11 +1,13 @@ import 'package:dio/dio.dart'; import 'package:injectable/injectable.dart'; -// ignore: always_use_package_imports +// ignore_for_file: always_use_package_imports import 'adapters/http_stub_adapter.dart' if (dart.library.html) 'adapters/http_browser_adapter.dart' if (dart.library.io) 'adapters/http_native_adapter.dart'; +import 'http_logger.dart'; + @injectable class HttpClient with DioMixin implements Dio { HttpClient() { @@ -17,5 +19,7 @@ class HttpClient with DioMixin implements Dio { ); httpClientAdapter = httpAdapter(); + + interceptors.add(HttpLogger()); } } diff --git a/lib/app/data/core/platform/http_client/http_logger.dart b/lib/app/data/core/platform/http_client/http_logger.dart new file mode 100644 index 0000000..54ae11b --- /dev/null +++ b/lib/app/data/core/platform/http_client/http_logger.dart @@ -0,0 +1,284 @@ +// ignore_for_file: lines_longer_than_80_chars, comment_references + +import 'dart:developer'; +import 'dart:math' as math; + +import 'package:dio/dio.dart'; + +void _log(Object obj) { + log(obj.toString()); +} + +class HttpLogger extends Interceptor { + HttpLogger({ + this.request = true, + this.requestHeader = false, + this.requestBody = true, + this.responseHeader = false, + this.responseBody = true, + this.error = true, + this.maxWidth = 90, + this.compact = true, + this.logPrint = _log, + }); + + /// Print request [Options] + final bool request; + + /// Print request header [Options.headers] + final bool requestHeader; + + /// Print request data [Options.data] + final bool requestBody; + + /// Print [Response.data] + final bool responseBody; + + /// Print [Response.headers] + final bool responseHeader; + + /// Print error message + final bool error; + + /// InitialTab count to logPrint json response + static const int initialTab = 1; + + /// 1 tab length + static const String tabStep = ' '; + + /// Print compact json response + final bool compact; + + /// Width size per logPrint + final int maxWidth; + + /// Log printer; defaults logPrint log to console. + /// In flutter, you'd better use debugPrint. + /// you can also write log in a file. + void Function(Object object) logPrint; + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + if (request) { + _printRequestHeader(options); + } + if (requestHeader) { + _printMapAsTable(options.queryParameters, header: 'Query Parameters'); + final requestHeaders = {}..addAll(options.headers); + requestHeaders['contentType'] = options.contentType?.toString(); + requestHeaders['responseType'] = options.responseType.toString(); + requestHeaders['followRedirects'] = options.followRedirects; + requestHeaders['connectTimeout'] = options.connectTimeout; + requestHeaders['receiveTimeout'] = options.receiveTimeout; + _printMapAsTable(requestHeaders, header: 'Headers'); + _printMapAsTable(options.extra, header: 'Extras'); + } + if (requestBody && options.method != 'GET') { + final dynamic data = options.data; + if (data != null) { + if (data is Map) _printMapAsTable(options.data as Map?, header: 'Body'); + if (data is FormData) { + final formDataMap = {} + ..addEntries(data.fields) + ..addEntries(data.files); + _printMapAsTable(formDataMap, header: 'Form data | ${data.boundary}'); + } else { + _printBlock(data.toString()); + } + } + } + super.onRequest(options, handler); + } + + @override + void onError(DioError err, ErrorInterceptorHandler handler) { + if (error) { + if (err.type == DioErrorType.response) { + final uri = err.response?.requestOptions.uri; + _printBoxed( + header: + 'DioError ║ Status: ${err.response?.statusCode} ${err.response?.statusMessage}', + text: uri.toString(), + ); + if (err.response != null && err.response?.data != null) { + logPrint('╔ ${err.type.toString()}'); + _printResponse(err.response!); + } + _printLine('╚'); + logPrint(''); + } else { + _printBoxed(header: 'DioError ║ ${err.type}', text: err.message); + } + } + super.onError(err, handler); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + _printResponseHeader(response); + if (responseHeader) { + final responseHeaders = {}; + response.headers + .forEach((k, list) => responseHeaders[k] = list.toString()); + _printMapAsTable(responseHeaders, header: 'Headers'); + } + + if (responseBody) { + logPrint('╔ Body'); + logPrint('║'); + _printResponse(response); + logPrint('║'); + _printLine('╚'); + } + super.onResponse(response, handler); + } + + void _printBoxed({String? header, String? text}) { + logPrint(''); + logPrint('╔╣ $header'); + logPrint('║ $text'); + _printLine('╚'); + } + + void _printResponse(Response response) { + if (response.data != null) { + if (response.data is Map) { + _printPrettyMap(response.data as Map); + } else if (response.data is List) { + logPrint('║${_indent()}['); + _printList(response.data as List); + logPrint('║${_indent()}['); + } else { + _printBlock(response.data.toString()); + } + } + } + + void _printResponseHeader(Response response) { + final uri = response.requestOptions.uri; + final method = response.requestOptions.method; + _printBoxed( + header: + 'Response ║ $method ║ Status: ${response.statusCode} ${response.statusMessage}', + text: uri.toString()); + } + + void _printRequestHeader(RequestOptions options) { + final uri = options.uri; + final method = options.method; + _printBoxed(header: 'Request ║ $method ', text: uri.toString()); + } + + void _printLine([String pre = '', String suf = '╝']) => + logPrint('$pre${'═' * maxWidth}$suf'); + + void _printKV(String? key, Object? v) { + final pre = '╟ $key: '; + final msg = v.toString(); + + if (pre.length + msg.length > maxWidth) { + logPrint(pre); + _printBlock(msg); + } else { + logPrint('$pre$msg'); + } + } + + void _printBlock(String msg) { + final lines = (msg.length / maxWidth).ceil(); + for (var i = 0; i < lines; ++i) { + logPrint((i >= 0 ? '║ ' : '') + + msg.substring(i * maxWidth, + math.min(i * maxWidth + maxWidth, msg.length))); + } + } + + String _indent([int tabCount = initialTab]) => tabStep * tabCount; + + void _printPrettyMap( + Map data, { + int tabs = initialTab, + bool isListItem = false, + bool isLast = false, + }) { + var _tabs = tabs; + final isRoot = _tabs == initialTab; + final initialIndent = _indent(_tabs); + _tabs++; + + if (isRoot || isListItem) logPrint('║$initialIndent{'); + + data.keys.toList().asMap().forEach((index, dynamic key) { + final isLast = index == data.length - 1; + dynamic value = data[key]; + if (value is String) { + value = '"${value.toString().replaceAll(RegExp(r'(\r|\n)+'), " ")}"'; + } + if (value is Map) { + if (compact && _canFlattenMap(value)) { + logPrint('║${_indent(_tabs)} $key: $value${!isLast ? ',' : ''}'); + } else { + logPrint('║${_indent(_tabs)} $key: {'); + _printPrettyMap(value, tabs: _tabs); + } + } else if (value is List) { + if (compact && _canFlattenList(value)) { + logPrint('║${_indent(_tabs)} $key: ${value.toString()}'); + } else { + logPrint('║${_indent(_tabs)} $key: ['); + _printList(value, tabs: _tabs); + logPrint('║${_indent(_tabs)} ]${isLast ? '' : ','}'); + } + } else { + final msg = value.toString().replaceAll('\n', ''); + final indent = _indent(_tabs); + final linWidth = maxWidth - indent.length; + if (msg.length + indent.length > linWidth) { + final lines = (msg.length / linWidth).ceil(); + for (var i = 0; i < lines; ++i) { + logPrint( + '║${_indent(_tabs)} ${msg.substring(i * linWidth, math.min(i * linWidth + linWidth, msg.length))}'); + } + } else { + logPrint('║${_indent(_tabs)} $key: $msg${!isLast ? ',' : ''}'); + } + } + }); + + logPrint('║$initialIndent}${isListItem && !isLast ? ',' : ''}'); + } + + void _printList(List list, {int tabs = initialTab}) { + list.asMap().forEach((i, dynamic e) { + final isLast = i == list.length - 1; + if (e is Map) { + if (compact && _canFlattenMap(e)) { + logPrint('║${_indent(tabs)} $e${!isLast ? ',' : ''}'); + } else { + _printPrettyMap(e, tabs: tabs + 1, isListItem: true, isLast: isLast); + } + } else { + logPrint('║${_indent(tabs + 2)} $e${isLast ? '' : ','}'); + } + }); + } + + bool _canFlattenMap(Map map) { + return map.values + .where((dynamic val) => val is Map || val is List) + .isEmpty && + map.toString().length < maxWidth; + } + + bool _canFlattenList(List list) { + return list.length < 10 && list.toString().length < maxWidth; + } + + void _printMapAsTable(Map? map, {String? header}) { + if (map == null || map.isEmpty) return; + logPrint('╔ $header '); + map.forEach( + (dynamic key, dynamic value) => _printKV(key.toString(), value)); + _printLine('╚'); + } +} diff --git a/pubspec.lock b/pubspec.lock index 3e9451b..4f7a72d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -14,7 +14,7 @@ packages: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "1.7.1" + version: "1.7.2" archive: dependency: transitive description: @@ -35,7 +35,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.1" beamer: dependency: "direct main" description: @@ -133,7 +133,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -449,7 +449,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" mime: dependency: transitive description: @@ -720,21 +720,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.16.8" + version: "1.17.10" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.2" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.3.19" + version: "0.4.0" time: dependency: transitive description: