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);
+ });
+ });
+}