diff --git a/README.md b/README.md index eca16aa..9583e04 100644 --- a/README.md +++ b/README.md @@ -12,39 +12,50 @@ - **Cross-Platform Support**: With `dart_seq`, you can enjoy consistent logging capabilities across all Dart-supported platforms. It leverages the inherent cross-platform capabilities of Dart, making it easy to adopt and utilize in your applications, regardless of the target platform. - **Customizable Seq Client and Caching Implementations**: `dart_seq` provides an intuitive and flexible interface to customize your Seq client and caching implementations. This enables you to tailor the logging behavior to your specific requirements and preferences, adapting the library to various use cases and scenarios in your Dart applications. - **Batch Sending of Events**: `dart_seq` optimizes log transmission by sending events to Seq in batches. This helps minimize network overhead and improves overall logging performance, especially in high-traffic scenarios. -- **Automatic Retry Mechanism**: The library automatically retries failed requests to the Seq server, except in the case of 429 (Too Many Requests) responses. This built-in resilience ensures that log entries are reliably delivered, even in the face of intermittent network connectivity or temporary server unavailability. -- **Minimum Log Level Enforcement**: `dart_seq` keeps track of the server-side configured minimum log level and discards events that fall below this threshold. This feature helps reduce unnecessary log entries and ensures that only relevant and significant events are forwarded to the Seq server. With `dart_seq`, logging in your Dart applications becomes a breeze, ensuring that your logs are efficiently delivered to Seq servers across multiple platforms. -The library's batch sending, automatic retry, and minimum log level enforcement features enhance the logging experience and provide robustness and flexibility to your logging infrastructure. ## Getting Started -To start using `dart_seq` in your Dart application, follow these steps: +To start using `dart_seq` in your Dart/Flutter application, follow these steps: -1. Install the library using `dart pub add dart_seq` -2. Import the package: `import 'package:dart_seq/dart_seq.dart';` -3. Instantiate the `SeqLogger` class -4. Start logging +1. Install this library and the HTTP client: `dart pub add dart_seq dart_seq_http_client` +2. Instantiate client, cache and logger (see usage below) +3. Enjoy! ## Usage +> **Note** +> This library provides just the interfaces and scaffolding. +> To actually log events, you need to use a client implementation like +> [`dart_seq_http_client`](https://pub.dev/packages/dart_seq_http_client). + +After the installation, you can use the library like this: + ```dart +import 'package:dart_seq/dart_seq.dart'; +import 'package:dart_seq_http_client/dart_seq_http_client.dart'; + Future main() async { - // configure your logger - final logger = SeqLogger.http( + // Use the HTTP client implementation to create a logger + final logger = SeqHttpLogger.create( host: 'http://localhost:5341', globalContext: { - 'Environment': Platform.environment, + 'App': 'Example', }, ); - // add log events - await logger.log(SeqLogLevel.information, 'test, dart: {Dart}', null, { - 'Dart': Platform.version, - }); + // Log a message + await logger.log( + SeqLogLevel.information, + 'test, logged at: {Timestamp}', + null, + { + 'Timestamp': DateTime.now().toUtc().toIso8601String(), + }, + ); - // don't forget to flush your logs at the end! + // Flush the logger to ensure all messages are sent await logger.flush(); } ``` diff --git a/example/docker-compose.yml b/example/docker-compose.yml deleted file mode 100644 index 252a775..0000000 --- a/example/docker-compose.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: '3.9' - -services: - seq: - image: datalust/seq:latest - ports: - - '80:80' - - '5341:5341' - environment: - ACCEPT_EULA: Y diff --git a/example/main.dart b/example/main.dart deleted file mode 100644 index 13eaccc..0000000 --- a/example/main.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:dart_seq/dart_seq.dart'; - -Future main() async { - final logger = SeqLogger.http( - host: 'http://localhost:5341', - globalContext: { - 'App': 'Example', - }, - ); - - await logger.log( - SeqLogLevel.information, - 'test, logged at: {Timestamp}', - null, - { - 'Timestamp': DateTime.now().toUtc().toIso8601String(), - }, - ); - - await logger.flush(); -} diff --git a/lib/dart_seq.dart b/lib/dart_seq.dart index 6593e78..1700097 100644 --- a/lib/dart_seq.dart +++ b/lib/dart_seq.dart @@ -7,8 +7,6 @@ export 'src/seq_client.dart'; export 'src/seq_client_exception.dart'; export 'src/seq_context.dart'; export 'src/seq_event.dart'; -export 'src/seq_http_client.dart'; export 'src/seq_in_memory_cache.dart'; export 'src/seq_log_level.dart'; export 'src/seq_logger.dart'; -export 'src/seq_response.dart'; diff --git a/lib/src/seq_http_client.dart b/lib/src/seq_http_client.dart deleted file mode 100644 index d92230f..0000000 --- a/lib/src/seq_http_client.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'dart:convert'; - -import 'package:dart_seq/dart_seq.dart'; -import 'package:http/http.dart' as http; - -/// Calculates a linearly increasing duration to based on the number of [tries]. -Duration linearBackoff(int tries) => Duration(milliseconds: tries * 100); - -/// A HTTP ingestion client for Seq. Implements the [SeqClient] interface. -class SeqHttpClient implements SeqClient { - SeqHttpClient({ - required String host, - String? apiKey, - int maxRetries = 5, - Duration Function(int tries)? backoff, - }) : assert(host.isNotEmpty, 'host must not be empty'), - assert(host.startsWith('http'), 'the host must contain a scheme'), - assert(null == apiKey || apiKey.isNotEmpty, 'apiKey must not be empty'), - assert(maxRetries >= 0, 'maxRetries must be >= 0'), - _headers = { - 'Content-Type': 'application/vnd.serilog.clef', - if (apiKey != null) 'X-Seq-ApiKey': apiKey, - }, - _maxRetries = maxRetries, - _endpoint = Uri.parse('$host/api/events/raw'), - _backoff = backoff ?? linearBackoff; - - final Map _headers; - final Duration Function(int tries) _backoff; - final int _maxRetries; - final Uri _endpoint; - - String? _minimumLevelAccepted; - - @override - String? get minimumLevelAccepted => _minimumLevelAccepted; - - @override - Future sendEvents(List events) async { - final body = _collapseEvents(events); - if (body.isEmpty) { - SeqLogger.diagnosticLog(SeqLogLevel.verbose, 'No events to send.'); - - return; - } - - http.Response response; - try { - response = await _sendRequest(body); - } catch (e, stack) { - throw SeqClientException('Failed to send request', e, stack); - } - - await _handleResponse(response); - } - - String _collapseEvents(List events) => - events.reversed.map(jsonEncode).join('\n'); - - Future _sendRequest(String body) async { - http.Response? response; - Exception? lastException; - - var tries = 0; - - const noRetryStatusCodes = [201, 400, 401, 403, 413, 429, 500]; - do { - try { - response = await http.post( - _endpoint, - headers: _headers, - body: body, - ); - } on Exception catch (e) { - lastException = e; - - final backoffDuration = _backoff(tries); - - SeqLogger.diagnosticLog( - SeqLogLevel.error, - 'Error when sending request. Backing off by {BackoffDuration}s', - e, - {'BackoffDuration': backoffDuration.inSeconds}, - ); - - await Future.delayed(backoffDuration); - } - } while (!noRetryStatusCodes.contains(response?.statusCode) && - ++tries < _maxRetries); - - if (lastException != null) { - throw lastException; - } - - return response!; - } - - Future _handleResponse(http.Response response) async { - final json = jsonDecode(response.body); - if (json is! Map) { - throw SeqClientException('The response body was not a JSON object'); - } - - final seqResponse = SeqResponse.fromJson(json); - - if (response.statusCode == 201) { - if (seqResponse.minimumLevelAccepted != _minimumLevelAccepted) { - _minimumLevelAccepted = seqResponse.minimumLevelAccepted; - } - - return; - } - - final problem = seqResponse.error ?? 'no problem details known'; - - throw switch (response.statusCode) { - 400 => SeqClientException('The request was malformed: $problem'), - 401 => SeqClientException('Authorization is required: $problem'), - 403 => SeqClientException( - "The provided credentials don't have ingestion permission: $problem", - ), - 413 => SeqClientException( - 'The payload itself exceeds the configured maximum size: $problem', - ), - 429 => SeqClientException('Too many requests'), - 500 => SeqClientException( - "An internal error prevented the events from being ingested; check Seq's diagnostic log for more information: $problem", - ), - 503 => SeqClientException( - "The Seq server is starting up and can't currently service the request, or, free storage space has fallen below the minimum required threshold; this status code may also be returned by HTTP proxies and other network infrastructure when Seq is unreachable: $problem", - ), - _ => SeqClientException( - 'Unexpected status code (${response.statusCode}). Error: $problem', - ), - }; - } -} diff --git a/lib/src/seq_logger.dart b/lib/src/seq_logger.dart index bcb20d0..de951fa 100644 --- a/lib/src/seq_logger.dart +++ b/lib/src/seq_logger.dart @@ -12,44 +12,6 @@ class SeqLogger { this.autoFlush = true, }) : assert(backlogLimit >= 0, 'backlogLimit must be >= 0'); - /// Creates a new instance of [SeqLogger] that logs to a Seq server over HTTP. - /// - /// This method is a factory for creating a new instance of [SeqLogger] that - /// logs to a Seq server over HTTP using the [SeqHttpClient]. - /// - /// By default, a new instance of [SeqInMemoryCache] is used for caching the - /// events. If you want to use a different cache, you can pass it as the - /// [cache] parameter. - factory SeqLogger.http({ - required String host, - String? apiKey, - int maxRetries = 5, - SeqCache? cache, - int backlogLimit = 50, - SeqContext? globalContext, - String? minimumLogLevel, - bool autoFlush = true, - Duration Function(int tries)? httpBackoff, - }) { - final httpClient = SeqHttpClient( - host: host, - apiKey: apiKey, - maxRetries: maxRetries, - backoff: httpBackoff, - ); - - final actualCache = cache ?? SeqInMemoryCache(); - - return SeqLogger( - client: httpClient, - cache: actualCache, - backlogLimit: backlogLimit, - globalContext: globalContext, - minimumLogLevel: minimumLogLevel, - autoFlush: autoFlush, - ); - } - /// Compares two log levels. static int compareLevels(String? a, String? b) { if (a == null && b == null) { diff --git a/lib/src/seq_response.dart b/lib/src/seq_response.dart deleted file mode 100644 index 97062d0..0000000 --- a/lib/src/seq_response.dart +++ /dev/null @@ -1,19 +0,0 @@ -/// This class is used to parse the response from the Seq server. -class SeqResponse { - /// Creates a new instance of [SeqResponse]. - SeqResponse(this.minimumLevelAccepted, this.error); - - /// Creates a new instance of [SeqResponse] from the given [json] object. - factory SeqResponse.fromJson(Map json) { - return SeqResponse( - json['MinimumLevelAccepted'] as String?, - json['Error'] as String?, - ); - } - - /// The minimum level accepted by the Seq server, if any. - final String? minimumLevelAccepted; - - /// The error message from the Seq server, if any. - final String? error; -} diff --git a/pubspec.yaml b/pubspec.yaml index 97a68ed..34e2f06 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,9 +17,6 @@ screenshots: environment: sdk: '^3.0.0' -dependencies: - http: '>=0.13.3 <2.0.0' - dev_dependencies: build_runner: ^2.4.8 mockito: ^5.4.4 diff --git a/test/seq_response_test.dart b/test/seq_response_test.dart deleted file mode 100644 index a887b08..0000000 --- a/test/seq_response_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:convert'; - -import 'package:dart_seq/dart_seq.dart'; -import 'package:test/test.dart'; - -void main() { - test('Seq Response decoding', () { - const json = - '{"MinimumLevelAccepted": "Information", "Error": "test error"}'; - final map = jsonDecode(json) as Map; - - final response = SeqResponse.fromJson(map); - - expect(response.minimumLevelAccepted, 'Information'); - expect(response.error, 'test error'); - }); -}