diff --git a/README.md b/README.md index 0d924eb..7a491ad 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,19 @@ -# flutter_remote_logging -Sending [Logger()](https://pub.dev/packages/logging) messages to loggly.com +# Dart package `remote_logging` -```dart +Sending [Logger()](https://pub.dev/packages/logging) messages to various remote logging services, +like [Loggly](https://www.loggly.com/docs/http-endpoint/), [Splunk](https://docs.splunk.com/Documentation/Splunk/latest/Data/UsetheHTTPEventCollector), etc. +```dart import 'package:remote_logging/remote_logging.dart'; void main() async { - initLogging(logglyToken, verboseLoggers: ['logger1', 'logger2'], tagsProvider: (record) { - return [ - 'tag1', - 'tag2', - ]; - }); - + final logglyCollector = LogglyCollector(logglyToken); + initRemoteLogging( + logglyCollector, + verboseLoggers: ['logger1', 'logger2'], + tagsProvider: (_) => ['tag1','tag2'], + ); + // https://pub.dev/packages/logging Logger('logger1').info('info on logger1'); Logger('logger2').info('info on logger2'); diff --git a/lib/remote_logging.dart b/lib/remote_logging.dart index 146af9b..4d49fed 100644 --- a/lib/remote_logging.dart +++ b/lib/remote_logging.dart @@ -1,10 +1,10 @@ /// Created by alex@justprodev.com on 27.05.2022. -library remote_logging; +library; export 'package:logging/logging.dart'; export 'src/model.dart'; +export 'src/remote/collectors/loggly_collector.dart'; +export 'src/remote/collectors/splunk_collector.dart'; export 'src/remote_logging.dart'; -export 'src/remote/loggly.dart' show logglyTasks; - diff --git a/lib/src/model.dart b/lib/src/model.dart index b7671af..5c47655 100644 --- a/lib/src/model.dart +++ b/lib/src/model.dart @@ -2,4 +2,10 @@ import 'package:logging/logging.dart'; /// Created by alex@justprodev.com on 27.05.2022. -typedef TagsProvider = List Function(LogRecord record); \ No newline at end of file +/// Function that provides tags for a log record +typedef TagsProvider = List Function(LogRecord record); + +/// Collects logs and sends them to some remote service +abstract class LogCollector { + Future collect(String message, {List? tags}); +} \ No newline at end of file diff --git a/lib/src/remote/collectors/http_collector.dart b/lib/src/remote/collectors/http_collector.dart new file mode 100644 index 0000000..0105a9f --- /dev/null +++ b/lib/src/remote/collectors/http_collector.dart @@ -0,0 +1,15 @@ +// Created by alex@justprodev.com on 20.12.2025. + +import 'package:http/http.dart'; +import 'package:remote_logging/src/model.dart'; + +/// Base class for HTTP log collectors +abstract class HttpCollector implements LogCollector { + late final Client client; + final Uri url; + final Map headers; + + HttpCollector({required this.url, this.headers = const {}, Client? client}) { + this.client = client ?? Client(); + } +} diff --git a/lib/src/remote/collectors/loggly_collector.dart b/lib/src/remote/collectors/loggly_collector.dart new file mode 100644 index 0000000..45a0553 --- /dev/null +++ b/lib/src/remote/collectors/loggly_collector.dart @@ -0,0 +1,30 @@ +// Created by alex@justprodev.com on 27.05.2022. + +import 'http_collector.dart'; + +/// Loggly log collector +/// See: https://www.loggly.com/docs/http-endpoint/ +class LogglyCollector extends HttpCollector { + LogglyCollector( + String token, { + host = defaultHost, + super.client, + }) : super( + url: Uri.parse('https://$host/inputs/$token'), + headers: {'Content-type': 'text/plain; charset=utf-8'}, + ); + + @override + Future collect(String message, {List? tags}) { + return client.post( + url, + body: message, + headers: { + ...headers, + 'X-LOGGLY-TAG': tags?.join(',') ?? '', + }, + ); + } + + static const defaultHost = 'logs-01.loggly.com'; +} diff --git a/lib/src/remote/collectors/splunk_collector.dart b/lib/src/remote/collectors/splunk_collector.dart new file mode 100644 index 0000000..5f1243b --- /dev/null +++ b/lib/src/remote/collectors/splunk_collector.dart @@ -0,0 +1,28 @@ +// Created by alex@justprodev.com on 20.12.2025. + +import 'dart:convert'; +import 'http_collector.dart'; + +/// Splunk log collector +/// +/// See: https://docs.splunk.com/Documentation/Splunk/latest/Data/UsetheHTTPEventCollector +class SplunkCollector extends HttpCollector { + SplunkCollector(String token, {required String host, super.client}) + : super( + url: Uri.parse('https://$host/services/collector/event'), + headers: { + 'Authorization': 'Splunk $token', + 'Content-Type': 'application/json;charset=utf-8', + }, + ); + + @override + Future collect(String message, {List? tags}) { + final data = { + 'event': message, + if (tags != null && tags.isNotEmpty) 'fields': {'tags': tags}, + }; + + return client.post(url, body: jsonEncode(data), headers: headers); + } +} diff --git a/lib/src/remote/loggly.dart b/lib/src/remote/loggly.dart deleted file mode 100644 index 4711365..0000000 --- a/lib/src/remote/loggly.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:http/http.dart'; - -/// Created by alex@justprodev.com on 27.05.2022. - -import 'web_tags.dart' if (dart.library.io) 'mobile_tags.dart'; - -/// Non-completed tasks -final Set logglyTasks = {}; - -Future loggly(Uri url, String message, {List? tags}) async { - final headers = {"Content-type": "text/plain; charset=utf-8"}; - - headers["X-LOGGLY-TAG"] = [...defaultTags, ...(tags ?? [])].join(','); - - final task = post(url, body: message, headers: headers); - - try { - logglyTasks.add(task); - await task; - } catch (e, trace) { - // ignore: avoid_print - print("Error sending message to loggly $e $trace"); - } finally { - logglyTasks.remove(task); - } -} diff --git a/lib/src/remote/mobile_tags.dart b/lib/src/remote/mobile_tags.dart deleted file mode 100644 index 17d5e22..0000000 --- a/lib/src/remote/mobile_tags.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:io' show Platform; - -/// Created by alex@justprodev.com on 23.05.2023. - -final List defaultTags = () { - final tags = []; - - if (Platform.isAndroid) { - tags.add('android'); - } else if (Platform.isIOS) { - tags.add('ios'); - } - - return tags; -}(); - - diff --git a/lib/src/remote/tags/impl/empty_tags.dart b/lib/src/remote/tags/impl/empty_tags.dart new file mode 100644 index 0000000..dc9655d --- /dev/null +++ b/lib/src/remote/tags/impl/empty_tags.dart @@ -0,0 +1,3 @@ +// Created by alex@justprodev.com on 19.12.2025. + +final List defaultTags = []; \ No newline at end of file diff --git a/lib/src/remote/tags/impl/io_tags.dart b/lib/src/remote/tags/impl/io_tags.dart new file mode 100644 index 0000000..64f5228 --- /dev/null +++ b/lib/src/remote/tags/impl/io_tags.dart @@ -0,0 +1,5 @@ +import 'dart:io' show Platform; + +/// Created by alex@justprodev.com on 23.05.2023. + +final List defaultTags = [Platform.operatingSystem]; diff --git a/lib/src/remote/web_tags.dart b/lib/src/remote/tags/impl/web_tags.dart similarity index 100% rename from lib/src/remote/web_tags.dart rename to lib/src/remote/tags/impl/web_tags.dart diff --git a/lib/src/remote/tags/tags.dart b/lib/src/remote/tags/tags.dart new file mode 100644 index 0000000..8175497 --- /dev/null +++ b/lib/src/remote/tags/tags.dart @@ -0,0 +1,6 @@ +// Created by alex@justprodev.com on 19.12.2025. + +// export by platform +export 'impl/empty_tags.dart' + if (dart.library.io) 'impl/io_tags.dart' + if (dart.library.html) 'impl/web_tags.dart'; diff --git a/lib/src/remote_logging.dart b/lib/src/remote_logging.dart index 4bdacc5..2a083da 100644 --- a/lib/src/remote_logging.dart +++ b/lib/src/remote_logging.dart @@ -1,25 +1,31 @@ // Created by alex@justprodev.com on 27.05.2022. +import 'dart:async'; + import 'package:logging/logging.dart'; import 'package:remote_logging/src/model.dart'; +import 'package:remote_logging/src/remote/tags/tags.dart'; + +import 'remote/collectors/loggly_collector.dart' show LogglyCollector; -import 'remote/loggly.dart'; +/// Non-completed tasks +final Set tasks = {}; /// -/// Watch the root [Logger] and then send messages addressed [verboseLoggers] to loggly -/// [verboseLoggers] name of loggers that will be sent to loggly verbosely - i.e. INFO messages, etc +/// Watch the root [Logger] and then send messages addressed [verboseLoggers] to [collectors] +/// [verboseLoggers] name of loggers that will be sent to [collectors] verbosely - i.e. INFO messages, etc /// [tagsProvider] tags for loggly -/// [printToConsole] print message with [debugPrint] -void initLogging( - String logglyToken, { +/// [output] passes all messages to this function (e.g., for printing to console in custom way) +/// [preProcess] pre-process message before sending to collectors (e.g., hide sensitive info) +/// [includeStackTrace] include stack trace in the message sent to collectors +void initRemoteLogging( + LogCollector collector, { List? verboseLoggers, TagsProvider? tagsProvider, - bool Function()? printToConsole, + Function(LogRecord, String)? output, String Function(String loggerName, String message)? preProcess, bool includeStackTrace = false, }) { - final logglyUrl = Uri.parse("https://logs-01.loggly.com/inputs/$logglyToken"); - processRecord(LogRecord record) { String message = preProcess != null ? preProcess(record.loggerName, record.message) : record.message; @@ -30,18 +36,25 @@ void initLogging( } } - final tags = [record.level.name, if (record.loggerName.isNotEmpty) record.loggerName]; - - if (tagsProvider != null) tags.addAll(tagsProvider.call(record)); + final tags = [ + record.level.name, + ...defaultTags, + if (record.loggerName.isNotEmpty) record.loggerName, + if (tagsProvider != null) ...tagsProvider.call(record), + ]; // SEVERE messages will be sent to loggly anyway in if (record.level == Level.SEVERE || (verboseLoggers?.contains(record.loggerName) == true)) { - loggly(logglyUrl, message, tags: tags); + final task = collector.collect(message, tags: tags); + tasks.add(task); + task.catchError((e, trace) { + // ignore: avoid_print + print("Error sending message to collector $e $trace"); + }).whenComplete(() => tasks.remove(task)); } - if (printToConsole?.call() == true) { - // ignore: avoid_print - print('${record.loggerName} $message ${record.stackTrace ?? ''}'); + if (output != null) { + output(record, '${record.loggerName} $message ${record.stackTrace ?? ''}'); } } @@ -50,3 +63,34 @@ void initLogging( Logger.root.level = Level.ALL; Logger.root.onRecord.listen(processRecord); } + +/// Wait for all logging tasks to complete +Future waitForLoggingTasks() async { + try { + await Future.wait(tasks); + } catch (_) {} +} + +/// +/// Watch the root [Logger] and then send messages addressed [verboseLoggers] to loggly +/// [verboseLoggers] name of loggers that will be sent to loggly verbosely - i.e. INFO messages, etc +/// [tagsProvider] tags for loggly +/// [printToConsole] print message with [debugPrint] +@Deprecated('Use initRemoteLogging instead') +void initLogging( + String logglyToken, { + List? verboseLoggers, + TagsProvider? tagsProvider, + bool Function()? printToConsole, + String Function(String loggerName, String message)? preProcess, + bool includeStackTrace = false, +}) { + initRemoteLogging( + LogglyCollector(logglyToken), + verboseLoggers: verboseLoggers, + tagsProvider: tagsProvider, + output: printToConsole != null ? (_, message) => print(message) : null, + preProcess: preProcess, + includeStackTrace: includeStackTrace, + ); +} diff --git a/pubspec.yaml b/pubspec.yaml index ed9a960..d54b888 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: remote_logging -description: Sending Logger() messages to loggly.com -version: 1.1.0 +description: Sending Logger() messages to various remote logging services +version: 2.0.0 homepage: https://github.com/justprodev/flutter_remote_logging.git environment: - sdk: '>=2.18.2 <3.0.0' + sdk: '>=3.0.0 <4.0.0' dependencies: http: diff --git a/test/legacy_remote_logging_test.dart b/test/legacy_remote_logging_test.dart new file mode 100644 index 0000000..8df05dc --- /dev/null +++ b/test/legacy_remote_logging_test.dart @@ -0,0 +1,104 @@ +// Created by alex@justprodev.com on 22.07.2024. + +// ignore_for_file: deprecated_member_use_from_same_package + +import 'dart:io'; + +import 'package:remote_logging/remote_logging.dart'; +import 'package:remote_logging/src/remote/tags/impl/io_tags.dart'; +import 'package:test/test.dart'; +import 'package:fake_http_client/fake_http_client.dart'; + +String? lastBody; +Map>? lastHeaders; +Uri? lastUri; + +const loggers = ['logger1', 'logger2', 'logger3']; +const tags = ['tag1', 'tag2', 'tag3']; + +class TestHttpOverrides extends HttpOverrides { + @override + HttpClient createHttpClient(_) { + return FakeHttpClient((request, client) async { + lastUri = request.uri; + lastBody = request.bodyText; + lastHeaders = {}; + request.headers.forEach((key, value) { + lastHeaders![key] = value; + }); + return FakeHttpResponse(); + }); + } +} + +void main() { + setUpAll(() { + HttpOverrides.global = TestHttpOverrides(); + }); + + setUp(() { + Logger.root.clearListeners(); + lastBody = null; + lastHeaders = null; + lastUri = null; + }); + + test('token', () async { + initLogging('token'); + Logger.root.severe('123'); + await Future.delayed(Duration.zero); + expect(lastUri, Uri.parse('https://logs-01.loggly.com/inputs/token')); + }); + + test('root severe', () async { + initLogging('token'); + Logger.root.severe('severe message to root'); + await Future.delayed(Duration.zero); + expect(lastBody, 'severe message to root'); + expect(lastHeaders?.containsKey('X-LOGGLY-TAG'), true, reason: 'X-LOGGLY-TAG header not found'); + expect(lastHeaders!['X-LOGGLY-TAG']![0], 'SEVERE,${defaultTags.join(',')}'); + }); + + group('exception', () { + test('without trace', () async { + initLogging('token'); + Logger.root.severe('severe message to root', Exception('exception'), StackTrace.current); + await Future.delayed(Duration.zero); + expect(lastBody, 'severe message to root\nException: exception'); + expect(lastHeaders?.containsKey('X-LOGGLY-TAG'), true, reason: 'X-LOGGLY-TAG header not found'); + expect(lastHeaders!['X-LOGGLY-TAG']![0], 'SEVERE,${defaultTags.join(',')}'); + }); + + test('with trace', () async { + initLogging('token', includeStackTrace: true); + final currentTrace = StackTrace.current; + Logger.root.severe('severe message to root', Exception('exception'), currentTrace); + await Future.delayed(Duration.zero); + expect(lastBody, 'severe message to root\nException: exception\n$currentTrace'); + expect(lastHeaders!['X-LOGGLY-TAG']![0], 'SEVERE,${defaultTags.join(',')}'); + }); + }); + + test('tags', () async { + initLogging('token', tagsProvider: (_) => tags); + Logger.root.severe('123'); + await Future.delayed(Duration.zero); + expect( + lastHeaders!['X-LOGGLY-TAG']![0], + 'SEVERE,${[...defaultTags, ...tags].join(',')}', + ); + }); + + test('loggers', () async { + initLogging('token', verboseLoggers: loggers); + + for (final logger in loggers) { + Logger(logger).severe('123'); + await Future.delayed(Duration.zero); + expect( + lastHeaders!['X-LOGGLY-TAG']![0], + 'SEVERE,${[...defaultTags, logger].join(',')}', + ); + } + }); +} diff --git a/test/loggly_collector_test.dart b/test/loggly_collector_test.dart new file mode 100644 index 0000000..e52f84c --- /dev/null +++ b/test/loggly_collector_test.dart @@ -0,0 +1,28 @@ +// Created by alex@justprodev.com on 20.12.2025. + +import 'dart:io'; + +import 'package:http/http.dart'; +import 'package:http/testing.dart'; +import 'package:remote_logging/remote_logging.dart'; +import 'package:test/test.dart'; + +void main() { + test('loggly collector', () async { + Request? request; + final collector = LogglyCollector( + 'token', + host: 'test', + client: MockClient((r) async { + request = r; + return Response('', HttpStatus.ok); + }), + ); + await collector.collect('Test message', tags: ['tag1', 'tag2']); + expect(request, isNotNull); + expect(request!.url, Uri.parse('https://test/inputs/token')); + expect(request!.body, 'Test message'); + expect(request!.headers.containsKey('X-LOGGLY-TAG'), true, reason: 'X-LOGGLY-TAG header not found'); + expect(request!.headers['X-LOGGLY-TAG'], 'tag1,tag2'); + }); +} diff --git a/test/loggly_test.dart b/test/loggly_test.dart deleted file mode 100644 index cd44ee4..0000000 --- a/test/loggly_test.dart +++ /dev/null @@ -1,39 +0,0 @@ -// Created by alex@justprodev.com on 22.07.2024. - -import 'dart:io'; - -import 'package:remote_logging/src/remote/loggly.dart'; -import 'package:test/test.dart'; -import 'package:fake_http_client/fake_http_client.dart'; - -class TestHttpOverrides extends HttpOverrides { - @override - HttpClient createHttpClient(_) { - return FakeHttpClient((request, client) async { - final seconds = int.parse(request.bodyText); - await Future.delayed(Duration(seconds: seconds)); - // The default response is an empty 200. - return FakeHttpResponse(); - }); - } -} - -void main() { - setUp(() { - // Overrides all HttpClients. - HttpOverrides.global = TestHttpOverrides(); - }); - - test('logglyTasks', () async { - loggly(Uri.parse('https://test.com'), '1'); - loggly(Uri.parse('https://test.com'), '2'); - loggly(Uri.parse('https://test.com'), '3'); - expect(logglyTasks.length, 3); - await Future.delayed(const Duration(milliseconds: 1100)); - expect(logglyTasks.length, 2); - await Future.delayed(const Duration(seconds: 1)); - expect(logglyTasks.length, 1); - await Future.delayed(const Duration(seconds: 1)); - expect(logglyTasks.length, 0); - }); -} diff --git a/test/remote_logging_test.dart b/test/remote_logging_test.dart index 97f0ab5..43dc86d 100644 --- a/test/remote_logging_test.dart +++ b/test/remote_logging_test.dart @@ -1,95 +1,148 @@ -// Created by alex@justprodev.com on 22.07.2024. - -import 'dart:io'; +// Created by alex@justprodev.com on 19.12.2025. import 'package:remote_logging/remote_logging.dart'; +import 'package:remote_logging/src/remote/tags/tags.dart'; import 'package:test/test.dart'; -import 'package:fake_http_client/fake_http_client.dart'; -String? lastBody; -Map>? lastHeaders; -Uri? lastUri; +void main() { + setUp(() { + Logger.root.clearListeners(); + }); -const loggers = ['logger1', 'logger2', 'logger3']; -const tags = ['tag1', 'tag2', 'tag3']; + test('collector verbose', () async { + final collector = _TestLogCollector(); + initRemoteLogging(collector, verboseLoggers: ['test']); + Logger('test').info('Test message'); + expect(collector.messages.length, 1); + expect(collector.messages.first, 'Test message'); + }); -class TestHttpOverrides extends HttpOverrides { - @override - HttpClient createHttpClient(_) { - return FakeHttpClient((request, client) async { - lastUri = request.uri; - lastBody = request.bodyText; - lastHeaders = {}; - request.headers.forEach((key, value) { - lastHeaders![key] = value; - }); - return FakeHttpResponse(); - }); - } -} + test('collector severe', () async { + final collector = _TestLogCollector(); + initRemoteLogging(collector); + Logger('test').severe('Test message'); + expect(collector.messages.length, 1); + expect(collector.messages.first, 'Test message'); + Logger.root.severe('Test message 2'); + expect(collector.messages.length, 2); + expect(collector.messages[1], 'Test message 2'); + }); -void main() { - setUpAll(() { - HttpOverrides.global = TestHttpOverrides(); + test('collector severe exception', () async { + final collector = _TestLogCollector(); + initRemoteLogging(collector); + final stacktrace = StackTrace.current; + Logger('test').severe('Test message', Exception('test exception'), stacktrace); + expect(collector.messages.first.contains('Test message'), isTrue); + expect(collector.messages.first.contains('test exception'), isTrue); }); - setUp(() { - Logger.root.clearListeners(); - lastBody = null; - lastHeaders = null; - lastUri = null; + test('collector severe stacktrace', () async { + final collector = _TestLogCollector(); + initRemoteLogging(collector, includeStackTrace: true); + final stacktrace = StackTrace.current; + Logger('test').severe('Test message', Exception('test exception'), stacktrace); + expect(collector.messages.first.contains(stacktrace.toString()), isTrue); }); - test('token', () async { - initLogging('token'); - Logger.root.severe('123'); - await Future.delayed(Duration.zero); - expect(lastUri, Uri.parse('https://logs-01.loggly.com/inputs/token')); + test('collector tags', () async { + final collector = _TestLogCollector(); + initRemoteLogging(collector, tagsProvider: (_) => ['tag1', 'tag2']); + Logger('logger').severe('Test message'); + expect(collector.messages.length, 1); + expect(collector.messages.first, 'Test message'); + expect(collector.tags.length, 1); + expect(collector.tags.first.contains('SEVERE'), isTrue); + expect(collector.tags.first.contains('logger'), isTrue); + expect(defaultTags.every((tag) => collector.tags.first.contains(tag)), isTrue); + expect(collector.tags.first.contains('tag1'), isTrue); + expect(collector.tags.first.contains('tag2'), isTrue); }); - test('root severe', () async { - initLogging('token'); - Logger.root.severe('severe message to root'); - await Future.delayed(Duration.zero); - expect(lastBody, 'severe message to root'); - expect(lastHeaders?.containsKey('X-LOGGLY-TAG'), true, reason: 'X-LOGGLY-TAG header not found'); - expect(lastHeaders!['X-LOGGLY-TAG']![0], 'SEVERE'); + test('output', () async { + final collector = _TestLogCollector(); + final outputs = []; + final records = []; + initRemoteLogging(collector, output: (record, message) { + outputs.add(message); + records.add(record); + }); + Logger('logger').info('Test message'); + expect(collector.messages.length, 0); + expect(outputs.length, 1); + expect(outputs.first.contains('logger'), isTrue); + expect(outputs.first.contains('Test message'), isTrue); + expect(records.length, 1); + expect(records.first.loggerName, 'logger'); + expect(records.first.message, 'Test message'); }); - group('exception', () { - test('without trace', () async { - initLogging('token'); - Logger.root.severe('severe message to root', Exception('exception'), StackTrace.current); - await Future.delayed(Duration.zero); - expect(lastBody, 'severe message to root\nException: exception'); - expect(lastHeaders?.containsKey('X-LOGGLY-TAG'), true, reason: 'X-LOGGLY-TAG header not found'); - expect(lastHeaders!['X-LOGGLY-TAG']![0], 'SEVERE'); + test('preProcess', () async { + final collector = _TestLogCollector(); + initRemoteLogging( + collector, + verboseLoggers: ['test'], + preProcess: (loggerName, message) => message.replaceAll(RegExp(r'password: \d+'), 'password: [HIDDEN]'), + ); + Logger('test').info('Test message with password: 12345'); + expect(collector.messages.length, 1); + expect(collector.messages.first, 'Test message with password: [HIDDEN]'); + }); + + group('tasks', () { + test('concurrency', () async { + initRemoteLogging(_DelayLogCollector(), verboseLoggers: ['test']); + final logger = Logger('test'); + + logger.info('100'); + logger.info('200'); + logger.info('300'); + + expect(tasks.length, 3, reason: 'Three tasks should be queued'); + await Future.delayed(const Duration(milliseconds: 100)); + expect(tasks.length, 2, reason: 'One task should be completed after 100ms'); + await Future.delayed(const Duration(milliseconds: 100)); + expect(tasks.length, 1, reason: 'Two tasks should be completed after 200ms'); + await Future.delayed(const Duration(milliseconds: 100)); + expect(tasks.length, 0, reason: 'All tasks should be completed after 300ms'); }); - test('with trace', () async { - initLogging('token', includeStackTrace: true); - final currentTrace = StackTrace.current; - Logger.root.severe('severe message to root', Exception('exception'), currentTrace); - await Future.delayed(Duration.zero); - expect(lastBody, 'severe message to root\nException: exception\n$currentTrace'); - expect(lastHeaders!['X-LOGGLY-TAG']![0], 'SEVERE'); + test('wait tasks', () async { + initRemoteLogging(_ErrorLogCollector()); + Logger.root.severe('Test message'); + Logger.root.severe('Test message'); + Logger.root.severe('Test message'); + expect(tasks.length, 3, reason: 'Three tasks should be queued'); + await expectLater(waitForLoggingTasks(), completes, reason: 'Tasks should complete without throwing'); + expect(tasks.length, 0, reason: 'All tasks should be completed'); }); }); +} - test('tags', () async { - initLogging('token', tagsProvider: (_) => tags); - Logger.root.severe('123'); - await Future.delayed(Duration.zero); - expect(lastHeaders!['X-LOGGLY-TAG']![0], 'SEVERE,${tags.join(',')}'); - }); +/// A log collector that delays for a number of milliseconds specified in the message +class _DelayLogCollector extends LogCollector { + @override + Future collect(String message, {List? tags}) async { + await Future.delayed(Duration(milliseconds: int.parse(message))); + } +} - test('loggers', () async { - initLogging('token', verboseLoggers: loggers); +class _ErrorLogCollector extends LogCollector { + @override + Future collect(String message, {List? tags}) async { + await Future.delayed(Duration(milliseconds: 100)); + throw Exception('Test exception'); + } +} - for(final logger in loggers) { - Logger(logger).severe('123'); - await Future.delayed(Duration.zero); - expect(lastHeaders!['X-LOGGLY-TAG']![0], 'SEVERE,$logger'); - } - }); +/// A log collector that collects messages and tags for testing +class _TestLogCollector extends LogCollector { + final tags = >[]; + final messages = []; + + @override + Future collect(String message, {List? tags}) async { + if (tags != null) this.tags.add(tags); + messages.add(message); + } } diff --git a/test/splunk_collector_test.dart b/test/splunk_collector_test.dart new file mode 100644 index 0000000..b7aa7e9 --- /dev/null +++ b/test/splunk_collector_test.dart @@ -0,0 +1,30 @@ +// Created by alex@justprodev.com on 20.12.2025. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart'; +import 'package:http/testing.dart'; +import 'package:remote_logging/remote_logging.dart'; +import 'package:test/test.dart'; + +void main() { + test('splunk collector', () async { + Request? request; + final collector = SplunkCollector( + 'token', + host: 'test', + client: MockClient((r) async { + request = r; + return Response('', HttpStatus.ok); + }), + ); + await collector.collect('Test message', tags: ['tag1', 'tag2']); + expect(request, isNotNull); + expect(request!.url, Uri.parse('https://test/services/collector/event')); + expect(request!.headers, containsPair('Authorization', 'Splunk token')); + final json = jsonDecode(request!.body); + expect(json['event'], 'Test message'); + expect(json['fields']['tags'], ['tag1', 'tag2']); + }); +}