diff --git a/packages/talker_grpc_logger/.gitignore b/packages/talker_grpc_logger/.gitignore new file mode 100644 index 00000000..3cceda55 --- /dev/null +++ b/packages/talker_grpc_logger/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/talker_grpc_logger/CHANGELOG.md b/packages/talker_grpc_logger/CHANGELOG.md new file mode 100644 index 00000000..effe43c8 --- /dev/null +++ b/packages/talker_grpc_logger/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/talker_grpc_logger/README.md b/packages/talker_grpc_logger/README.md new file mode 100644 index 00000000..1e8174b6 --- /dev/null +++ b/packages/talker_grpc_logger/README.md @@ -0,0 +1,99 @@ +# talker_grpc_logger +Lightweight and customizable [grpc](https://pub.dev/packages/grpc) client logger on [talker](https://pub.dev/packages/talker) base.
+[Talker](https://github.com/Frezyx/talker) - Advanced exception handling and logging for dart/flutter applications 🚀 + +## Preview +This is how the logs of your grpc requests will look in the console +![](preview.jpg) + + +## Note + +At the moment, only unary RPCs are supported. Streaming RPCs will probably +be added in the future. Contributions are welcome! + + +## Usage + +Create an interceptor and instrument your RPC client: + +```dart +import 'package:grpc/grpc.dart'; +import 'package:grpc/grpc_or_grpcweb.dart'; +import 'package:talker_flutter/talker_flutter.dart'; +import 'package:talker_grpc_logger/talker_grpc_logger.dart'; + +void main() { + // Define port and host as you see fit + var host = 'localhost'; + var port = 50051; + + // transportSecure needs to be true when talking to a server through TLS. + // This can be disabled for local development. + // GrpcOrGrpcWebClientChannel is a channel type compatible with web and native. There + // are other channel types available for each platform. + late final channel = GrpcOrGrpcWebClientChannel.toSingleEndpoint( + host: host, + port: port, + transportSecure: host == 'localhost' ? false : true); + + + final List interceptors = [ + TalkerGrpcLogger() + ]; + + // Generate your RPC client as usual, and use the interceptor to log the requests and responses. + late final rpcClient = YourRPCClient(channel, interceptors: interceptors); +} +``` + + +## Usage with Talker + +Very similar to the section above, just pass a Talker instance to the interceptor: + +```dart +import 'package:grpc/grpc.dart'; +import 'package:grpc/grpc_or_grpcweb.dart'; +import 'package:talker_flutter/talker_flutter.dart'; +import 'package:talker_grpc_logger/talker_grpc_logger.dart'; + +void main() { + // Not mandatory, but useful to see the grpc logs in the Talker screen + final talker = TalkerFlutter.init(); + + // Define port and host as you see fit + var host = 'localhost'; + var port = 50051; + + // transportSecure needs to be true when talking to a server through TLS. + // This can be disabled for local development. + // GrpcOrGrpcWebClientChannel is a channel type compatible with web and native. There + // are other channel types available for each platform. + late final channel = GrpcOrGrpcWebClientChannel.toSingleEndpoint( + host: host, + port: port, + transportSecure: host == 'localhost' ? false : true); + + + final List interceptors = [ + TalkerGrpcLogger(talker: talker) + ]; + + // Generate your RPC client as usual, and use the interceptor to log the requests and responses. + late final rpcClient = YourRPCClient(channel, interceptors: interceptors); +} +``` + + +## Token obfuscation + +`TalkerGrpcLogger` will obfuscate bearer tokens by default. It'll look at the +metadata of the request and obfuscate the `authorization` header. It'll look +like `Bearer [obfuscated]` in the logs. It is highly recommended to keep this +option enabled. If you want to disable it, you can pass `obfuscateToken: +false`: + +```dart +TalkerGrpcLogger(talker: talker, obfuscateToken: true) +``` diff --git a/packages/talker_grpc_logger/analysis_options.yaml b/packages/talker_grpc_logger/analysis_options.yaml new file mode 100644 index 00000000..dee8927a --- /dev/null +++ b/packages/talker_grpc_logger/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/talker_grpc_logger/example/talker_grpc_logger_example.dart b/packages/talker_grpc_logger/example/talker_grpc_logger_example.dart new file mode 100644 index 00000000..16354f9d --- /dev/null +++ b/packages/talker_grpc_logger/example/talker_grpc_logger_example.dart @@ -0,0 +1,30 @@ +import 'package:grpc/grpc.dart'; +import 'package:grpc/grpc_or_grpcweb.dart'; +import 'package:talker_flutter/talker_flutter.dart'; +import 'package:talker_grpc_logger/talker_grpc_logger.dart'; + +void main() { + // Not mandatory, but useful to see the grpc logs in the Talker screen + final talker = TalkerFlutter.init(); + + // Define port and host as you see fit + var host = 'localhost'; + var port = 50051; + + // transportSecure needs to be true when talking to a server through TLS. + // This can be disabled for local development. + // GrpcOrGrpcWebClientChannel is a channel type compatible with web and native. There + // are other channel types available for each platform. + late final channel = GrpcOrGrpcWebClientChannel.toSingleEndpoint( + host: host, + port: port, + transportSecure: host == 'localhost' ? false : true); + + + final List interceptors = [ + TalkerGrpcLogger(talker: talker) + ]; + + // Generate your RPC client as usual, and use the interceptor to log the requests and responses. + late final rpcClient = YourRPCClient(channel, interceptors: interceptors); +} diff --git a/packages/talker_grpc_logger/lib/src/grpc_logs.dart b/packages/talker_grpc_logger/lib/src/grpc_logs.dart new file mode 100644 index 00000000..030b904c --- /dev/null +++ b/packages/talker_grpc_logger/lib/src/grpc_logs.dart @@ -0,0 +1,139 @@ +import 'dart:convert'; + +import 'package:grpc/grpc.dart'; +import 'package:talker/talker.dart'; + +const encoder = JsonEncoder.withIndent(' '); + +class GrpcRequestLog extends TalkerLog { + GrpcRequestLog( + String title, { + required this.method, + required this.request, + required this.options, + this.obfuscateToken = true, + }) : super(title); + + final ClientMethod method; + final Q request; + final CallOptions options; + final bool obfuscateToken; + + @override + AnsiPen get pen => (AnsiPen()..xterm(219)); + + @override + String get title => 'grpc-request'; + + @override + String generateTextMessage() { + var time = TalkerDateTimeFormatter(DateTime.now()).timeAndSeconds; + var msg = '[$title] | $time | [${method.path}]'; + + msg += '\nRequest: ${request.toString().replaceAll("\n", " ")}'; + + // Add the headers to the log message, but obfuscate the token if + // necessary. + final Map headers = {}; + options.metadata.forEach((key, value) { + if (obfuscateToken && key.toLowerCase() == 'authorization') { + headers[key] = 'Bearer [obfuscated]'; + } else { + headers[key] = value; + } + }); + + try { + if (headers.isNotEmpty) { + final prettyHeaders = encoder.convert(headers); + msg += '\nHeaders: $prettyHeaders'; + } + } catch (_) { + // TODO: add handling can`t convert + } + return msg; + } +} + +class GrpcErrorLog extends TalkerLog { + GrpcErrorLog( + String title, { + required this.method, + required this.request, + required this.options, + required this.grpcError, + required this.durationMs, + this.obfuscateToken = true, + }) : super(title); + + final ClientMethod method; + final Q request; + final CallOptions options; + final GrpcError grpcError; + final int durationMs; + final bool obfuscateToken; + + @override + AnsiPen get pen => (AnsiPen()..red()); + + @override + String get title => 'grpc-error'; + + @override + String generateTextMessage() { + var time = TalkerDateTimeFormatter(DateTime.now()).timeAndSeconds; + var msg = '[$title] | $time | [${method.path}]'; + msg += '\nDuration: $durationMs ms'; + msg += '\nError code: ${grpcError.codeName}'; + msg += '\nError message: ${grpcError.message}'; + msg += '\nRequest: ${request.toString().replaceAll("\n", " ")}'; + + // Add the headers to the log message, but obfuscate the token if + // necessary. + final Map headers = {}; + options.metadata.forEach((key, value) { + if (obfuscateToken && key.toLowerCase() == 'authorization') { + headers[key] = 'Bearer [obfuscated]'; + } else { + headers[key] = value; + } + }); + + try { + if (headers.isNotEmpty) { + final prettyHeaders = encoder.convert(headers); + msg += '\nHeaders: $prettyHeaders'; + } + } catch (_) { + // TODO: add handling can`t convert + } + return msg; + } +} + +class GrpcResponseLog extends TalkerLog { + GrpcResponseLog( + String title, { + required this.method, + required this.response, + required this.durationMs, + }) : super(title); + + final ClientMethod method; + final R response; + final int durationMs; + + @override + AnsiPen get pen => (AnsiPen()..xterm(46)); + + @override + String get title => 'grpc-response'; + + @override + String generateTextMessage() { + var time = TalkerDateTimeFormatter(DateTime.now()).timeAndSeconds; + var msg = '[$title] | $time | [${method.path}]'; + msg += '\nDuration: $durationMs ms'; + return msg; + } +} diff --git a/packages/talker_grpc_logger/lib/src/talker_grpc_logger_base.dart b/packages/talker_grpc_logger/lib/src/talker_grpc_logger_base.dart new file mode 100644 index 00000000..bd9f7887 --- /dev/null +++ b/packages/talker_grpc_logger/lib/src/talker_grpc_logger_base.dart @@ -0,0 +1,57 @@ +/// Implements a gRPC interceptor that logs requests and responses to Talker. +/// https://pub.dev/documentation/grpc/latest/grpc/ClientInterceptor-class.html +import 'package:talker/talker.dart'; +import 'package:grpc/grpc.dart'; + +import 'grpc_logs.dart'; + + +class TalkerGrpcLogger extends ClientInterceptor { + TalkerGrpcLogger({Talker? talker, this.obfuscateToken = true}) { + _talker = talker ?? Talker(); + } + + late Talker _talker; + final bool obfuscateToken; + + @override + ResponseFuture interceptUnary(ClientMethod method, Q request, + CallOptions options, ClientUnaryInvoker invoker) { + _talker.logTyped(GrpcRequestLog(method.path, + method: method, + request: request, + options: options, + obfuscateToken: obfuscateToken)); + + DateTime startTime = DateTime.now(); + final response = invoker(method, request, options); + + response.then((r) { + Duration elapsedTime = DateTime.now().difference(startTime); + _talker.logTyped(GrpcResponseLog(method.path, + method: method, response: r, durationMs: elapsedTime.inMilliseconds)); + }).catchError((e) { + Duration elapsedTime = DateTime.now().difference(startTime); + _talker.logTyped(GrpcErrorLog(method.path, + method: method, + request: request, + options: options, + grpcError: e, + durationMs: elapsedTime.inMilliseconds, + obfuscateToken: obfuscateToken)); + }); + return response; + } + + @override + ResponseStream interceptStreaming( + ClientMethod method, + Stream requests, + CallOptions options, + ClientStreamingInvoker invoker) { + print('interceptStreaming'); + + return invoker(method, requests, options); + } +} + diff --git a/packages/talker_grpc_logger/lib/talker_grpc_logger.dart b/packages/talker_grpc_logger/lib/talker_grpc_logger.dart new file mode 100644 index 00000000..88ab651c --- /dev/null +++ b/packages/talker_grpc_logger/lib/talker_grpc_logger.dart @@ -0,0 +1,3 @@ +library talker_grpc_logger; + +export 'src/talker_grpc_logger_base.dart'; diff --git a/packages/talker_grpc_logger/preview.jpg b/packages/talker_grpc_logger/preview.jpg new file mode 100644 index 00000000..bb1bd735 Binary files /dev/null and b/packages/talker_grpc_logger/preview.jpg differ diff --git a/packages/talker_grpc_logger/pubspec.yaml b/packages/talker_grpc_logger/pubspec.yaml new file mode 100644 index 00000000..7b669376 --- /dev/null +++ b/packages/talker_grpc_logger/pubspec.yaml @@ -0,0 +1,17 @@ +name: talker_grpc_logger +description: A starting point for Dart libraries or applications. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.2.6 + +# Add regular dependencies here. +dependencies: + grpc: ^3.2.4 + talker: ^4.0.3 + # path: ^1.8.0 + +dev_dependencies: + lints: ^2.1.0 + test: ^1.24.0 diff --git a/packages/talker_grpc_logger/test/talker_grpc_logger_test.dart b/packages/talker_grpc_logger/test/talker_grpc_logger_test.dart new file mode 100644 index 00000000..418ca084 --- /dev/null +++ b/packages/talker_grpc_logger/test/talker_grpc_logger_test.dart @@ -0,0 +1,16 @@ +import 'package:talker_grpc_logger/talker_grpc_logger.dart'; +import 'package:test/test.dart'; + +void main() { + group('A group of tests', () { + final awesome = Awesome(); + + setUp(() { + // Additional setup goes here. + }); + + test('First Test', () { + expect(awesome.isAwesome, isTrue); + }); + }); +}