Skip to content

Add SEQ properties, fix cache loop and other improvements - #16

Merged
ricardoboss merged 16 commits into
ricardoboss:mainfrom
petrnymsa:feat/per-event-error-isolation
Mar 6, 2026
Merged

ricardoboss merged 16 commits into
ricardoboss:mainfrom
petrnymsa:feat/per-event-error-isolation

Conversation

@petrnymsa

@petrnymsa petrnymsa commented Mar 4, 2026 •

Copy link
Copy Markdown
Contributor

First of all, thank you for this package.

We used dart_seq heavily for last year in our big projects (over 1M+ Month users) and found several issues.

Main issue is loop-hole where event can be potentially malformed which is refused by SEQ server (Bad Request 400). There was no way to tell that this happened and next time, same event was read from cache - thus creating infinite loophole of failure. From this point, no more logs were written to SEQ.

Originally I wanted to create multiple several, focused PRs but I've decided (And yes with help of AI agents) to tackle multiple issues at once.

I am more than happy to fine-tune it before we merge it, but we will definitely use "new" version.

Note that there are is PR for dart_seq_http_client too. I will update version constraints after we agree that this PR makes sense.


Motivation

The original dart_seq had several pain points when used in production:

  1. Logging methods could crash the app (#13, #14) — SeqClient.sendEvents() returned Future<void> with no error isolation. A network error or server rejection during flush would throw an unhandled exception, potentially crashing the host app. Users had no way to choose between "fire and forget" and "audit" logging.

  2. Oversized payloads caused infinite retry loops (#12) — when the Seq server rejected a batch (e.g. HTTP 413 Payload Too Large), the events stayed in cache and were re-sent on every flush. This blocked all subsequent logging indefinitely, because the same oversized batch was retried forever.

  3. No support for OpenTelemetry / distributed tracing fields (#10) — Seq supports @tr, @sp, @ps, @st, @sc, @ra, @sk CLEF fields for distributed tracing, but the library had no way to set them. Passing them via context caused double-escaping (@tr became @@tr), making them unusable.

  4. No timer-based auto-flush (#15) — events that didn't reach the backlogLimit threshold would sit in cache indefinitely until the next batch filled up. There was no way to ensure logs were sent after a period of inactivity.

  5. Positional parameters made the API hard to extend (#10) — adding new fields to log() required breaking positional parameter order. Named parameters are needed for a maintainable API surface.

This PR addresses all of the above.

Closes Issues

  • #10 — Add support for other SEQ properties (@tr, @sp, @ps, @st, @sc, @ra, @sk)
  • #11 — Formatting of exception property (Error.safeToString → toString())
  • #12 — Dart body message max size (non-retryable errors no longer cause infinite retry)
  • #13 — Add info about SeqClientException to README
  • #14 — Add ability to toggle audit logging (throwOnError flag)
  • #15 — Auto flush duration timer (flushInterval)

Breaking Changes

  • SeqLogger.log() — exception and context changed from positional to named parameters
  • SeqLogger convenience methods (verbose, debug, info, warning, error, fatal) — same positional-to-named migration
  • SeqEvent constructor — all parameters except timestamp are now named
  • SeqClient.sendEvents() — return type changed from Future<void> to Future<List<SeqEventSentResult>> (implementations must return per-event results)

Changes

Per-event error isolation

Why: Previously, flush() treated all events as a single unit — either everything succeeded or everything failed. A single malformed event in a batch would cause the entire batch to fail and be retried forever, blocking all logging.

What:

  • New SeqEventSentResult class — carries isSuccess, error, and isPermanent per event
  • flush() Path A now processes per-event results: permanent failures (e.g. malformed events rejected with HTTP 400) are dropped, transient failures are re-queued
  • Diagnostic warning logged for each permanently dropped event (previously these were silently lost)

Non-retryable error handling (Path B)

Why: When sendEvents throws (total failure), all events stayed in cache unconditionally. For non-retryable errors like 413 (Payload Too Large), this meant the same oversized batch was re-sent on every flush, blocking the entire logging pipeline indefinitely (#12).

What:

  • SeqClientException.isRetryable getter (default: true) — allows subclasses to signal non-retryable errors
  • When sendEvents throws a non-retryable SeqClientException:
    • Adaptive batch sizing: _nextFlushBatchSize halves on each failure, converging to single-event testing
    • Single non-retryable event is dropped with diagnostic warning
    • Batch size resets to backlogLimit after a successful flush
  • Retryable errors (network, auth) and plain Exceptions retain existing behavior (events stay in cache)

onFlushError callback

Why: The built-in defaults (drop permanent, re-queue transient) cover the common cases, but some apps need custom behavior — e.g. logging individual failures, applying retry limits, or reporting to crash analytics (#13).

What:

  • Optional FlushErrorHandler on SeqLogger — receives per-event results and error, returns events to re-queue
  • When set, overrides all default Path A and Path B handling
  • When null, built-in defaults apply

throwOnError flag (#14)

Why: The original library had no way to choose between "fire and forget" and "audit" logging. Some apps want logging to never crash the app; others want to know immediately when logging fails.

What:

  • When false (default): flush errors are caught and reported via onDiagnosticLog — safe for production
  • When true: exceptions propagate to caller — useful for debugging or when logging failures must be handled

flushInterval timer (#15)

Why: Events that didn't reach the backlogLimit threshold sat in cache indefinitely. In low-traffic apps, important logs (like a single error) could wait minutes or hours before being flushed.

What:

  • Optional Duration that triggers automatic flush after inactivity
  • Timer resets on each send() call
  • dispose() method cancels the timer

Exception formatting fix (#11)

Why: The @x (exception) CLEF field was serialized using Error.safeToString(), which wraps non-String objects in Instance of 'Foo' and escapes newlines as literal \n text. This made stack traces and custom exception messages unreadable in Seq.

What:

  • Replaced Error.safeToString(exception) with a _safeToString() helper that tries toString() first and falls back to Error.safeToString() if it throws
  • Custom exception toString() overrides now render properly in Seq
  • Stack traces display with actual line breaks instead of literal \n text
  • Still safe against broken toString() implementations — no risk of crashing the serializer

OpenTelemetry / distributed tracing fields (#10)

Why: Seq supports W3C trace context fields for distributed tracing, but the library had no native support. Passing them via context caused double-escaping (@tr → @@tr), making Seq's tracing tools unusable.

What:

  • 7 new CLEF fields on SeqEvent: traceId (@tr), spanId (@sp), parentSpanId (@ps), spanStart (@st), scope (@sc), resourceAttributes (@ra), spanKind (@sk)
  • Known Seq CLEF keys are no longer double-escaped in context
  • Named parameters on SeqLogger.log() for all tracing fields

Files Changed

File Change
lib/dart_seq.dart Export SeqEventSentResult
lib/src/seq_client.dart Return type Future<List<SeqEventSentResult>>, expanded DartDoc
lib/src/seq_client_exception.dart Added isRetryable getter
lib/src/seq_event.dart Named params, OTEL fields, fromMap(), withAddedContext()
lib/src/seq_event_sent_result.dart New — per-event result model
lib/src/seq_logger.dart onFlushError, throwOnError, flushInterval, dispose(), Path A/B rewrite, _nextFlushBatchSize, diagnostic logging for dropped events
pubspec.yaml Added fake_async dev dependency
CHANGELOG.md v3.0.0 entry
README.md Flush behavior docs, configuration table, onFlushError example

Test Coverage

  • 157 tests (up from ~30 on main)
  • New test files: seq_event_sent_result_test.dart, seq_client_exception_test.dart
  • Expanded: seq_logger_test.dart (+1295 lines), seq_event_test.dart (+681 lines), seq_in_memory_cache_test.dart (+165 lines)
  • Key new test scenarios:
    • Non-retryable exception halves batch size across flushes
    • Single non-retryable event is dropped with diagnostic
    • Batch size resets after successful smaller flush
    • Retryable exceptions keep events in cache
    • Permanent failures in Path A are logged as warnings
    • onFlushError callback integration (total and partial failure)
    • Concurrent flush guard

@petrnymsa
petrnymsa requested a review from ricardoboss as a code owner March 4, 2026 14:18
@ricardoboss

ricardoboss commented Mar 4, 2026 •

Copy link
Copy Markdown
Owner

Hi! Thanks for opening a PR! I will need some time to review everything. Meanwhile, you can make sure you reviewed the CONTRIBUTING.md (particularly formatting and writing tests).

@ricardoboss ricardoboss left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, thanks for opening a PR!

This applies here as well: I'm not a big fan of such big PRs, but I am happy you put in the effort.

I have a few suggestions and questions. Also, please review the comment on seq_logger_test.dart.

Comment thread lib/src/seq_event_sent_result.dart Outdated
Comment thread lib/src/seq_logger.dart Outdated
Comment thread lib/src/seq_logger.dart Outdated
Comment thread lib/src/seq_logger.dart Outdated
Comment thread lib/src/seq_logger.dart
Comment thread lib/src/seq_event_sent_result.dart Outdated
Comment thread lib/src/seq_client.dart Outdated
Comment thread lib/src/seq_client.dart Outdated
Comment thread lib/src/seq_client.dart Outdated
Comment thread lib/src/seq_client.dart Outdated
@ricardoboss

Copy link
Copy Markdown
Owner

It seems you need to run dart format (check CI failure)

@ricardoboss

ricardoboss commented Mar 5, 2026 •

Copy link
Copy Markdown
Owner

@petrnymsa you need to rebase, I fixed a small issue on main

petrnymsa added 11 commits March 5, 2026 13:06
Convert SeqEvent constructor to named parameters.
Add OpenTelemetry distributed tracing fields (@tr, @sp, @ps, @st,
@sc, @ra, @sk) to SeqEvent and SeqLogger.log().
Prevent double-escaping of known CLEF keys in context.
Add tests for SeqEvent, SeqLogLevel, SeqClientException,
and SeqInMemoryCache.
Per-event success/failure tracking with isPermanent flag
to distinguish malformed events from transient errors.
Change SeqClient.sendEvents() return type from Future<void>
to Future<List<SeqEventSentResult>>. Throws on total failure,
returns mixed results on partial failure.
Rewrite flush() with two paths: partial failure (per-event
results) and total failure (exception). Add throwOnError,
flushInterval, dispose(). Change FlushErrorHandler typedef
to receive List<SeqEventSentResult>. Default behavior drops
permanent failures and re-queues transient ones.

# Conflicts:
#	lib/src/seq_logger.dart
Update mock clients to return List<SeqEventSentResult>.
Add tests for: partial failure with isPermanent, transient
re-queue, total failure cache retention, onFlushError with
synthetic results, and isPermanent flag on SeqEventSentResult.
Document flush paths, isPermanent flag, flush triggers,
configuration flags, and recommended onFlushError usage.
Update CHANGELOG with all v3.0.0 breaking changes and features.
@petrnymsa

Copy link
Copy Markdown
Contributor Author

@ricardoboss Since we doing breaking change, are you ok with bumping Dart SDK version? We could use null-aware-elements to simplify code

@ricardoboss

Copy link
Copy Markdown
Owner

@ricardoboss Since we doing breaking change, are you ok with bumping Dart SDK version? We could use null-aware-elements to simplify code

Yeah, that's ok.

@petrnymsa
petrnymsa force-pushed the feat/per-event-error-isolation branch from 682bf03 to d049185 Compare March 5, 2026 12:44
@petrnymsa

Copy link
Copy Markdown
Contributor Author

@ricardoboss I've tried to simplify code, removed non-sense tests. Also dropped flushTimer "feature"

@petrnymsa
petrnymsa requested a review from ricardoboss March 5, 2026 12:48

@ricardoboss ricardoboss left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're almost there. Thanks for your continued efforts on this!

Comment thread lib/src/seq_client.dart Outdated
Comment thread test/seq_event_result_test.dart
Comment thread lib/src/seq_logger.dart Outdated

@ricardoboss ricardoboss left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice! Thanks for making the changes

@ricardoboss
ricardoboss enabled auto-merge March 6, 2026 09:03
@ricardoboss
ricardoboss merged commit 093013f into ricardoboss:main Mar 6, 2026
1 check passed
@ricardoboss

Copy link
Copy Markdown
Owner

@petrnymsa both dart_seq and dart_seq_http_client have been released! Thanks for all your work on this and I hope we can collaborate again in the future

@petrnymsa

Copy link
Copy Markdown
Contributor Author

Thank you for quick review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants