Repository navigation
Conversation
Rewrite the client as a multi-module Java 17 library that matches the current Exceptionless server contract, with integrations for Spring Boot, Servlet, Logback, Log4j 2 and java.util.logging. - exceptionless-core: zero runtime dependencies, snake_case wire format, full cause/suppressed chains, server settings (@@log, @@error, @@DataExclusions, bot patterns), sessions and heartbeats, status-aware queue with optional file persistence, shutdown flush and uncaught exception capture on a single daemon thread - logging appenders, servlet filter and Spring Boot 3/4 starter - BOM, Maven wrapper, Spotless, JaCoCo, Central Portal release workflow - 368 unit/integration tests plus a Testcontainers e2e suite against exceptionless/exceptionless:8.10.0 - new README, MIGRATION.md, CONTRIBUTING.md, samples and agent skill Closes #91 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f80879f3ea
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| String prefix = apiKey.length() > 8 ? apiKey.substring(0, 8) : apiKey; | ||
| return prefix.replaceAll("[^A-Za-z0-9_-]", "_"); |
There was a problem hiding this comment.
Derive storage directories from the entire API key
When two API keys share their first eight characters, they resolve to the same queue and state directory. If the same storage root is reused by those clients—such as after switching projects—the second client can load and submit the first project's persisted events with its own credentials, while also inheriting its settings and install ID. Use a collision-resistant hash of the complete API key instead of the truncated prefix.
Useful? React with 👍 / 👎.
| public ExceptionlessConfiguration removePlugin(String name) { | ||
| plugins.removeIf(p -> p.getName().equals(name)); | ||
| return this; |
There was a problem hiding this comment.
Suspend plugins before removing or replacing them
After a plugin has run once, removing it only deletes it from the pipeline and never invokes suspend. Stateful plugins such as HeartbeatPlugin therefore retain their scheduled task and continue issuing heartbeat requests after removePlugin, and replacing one through addPlugin leaves the old instance running as well; the removed instance is also absent from the close-time suspension loop. Suspend started plugins and clear their lifecycle bookkeeping when they are removed.
Useful? React with 👍 / 👎.
- Models are plain JavaBeans (void setters); Event tags go through add/remove/hasTag - ModuleInfo is an immutable record; record map components are copied - SettingsResponse is a sealed interface; SubmissionResponse.notSent factory - SubmissionClient no longer takes the configuration per call - EventPlugin.of factory, EventsPostedListener, suspendProcessing(Duration) - Configuration naming fixes (userIdentity, isReferenceIdsEnabled, getProxy) - SettingsManager and settings parsing moved to internal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- One InstantSource-based clock across the queue, storage and plugins - FileQueueStorage keeps an in-memory index instead of listing the directory per enqueue - Precompiled wildcard exclusions cached by the configuration - Strict RFC 8259 JsonParser; JsonWriter and DataNormalizer share simple-value rules - Holder-class lazy init, ClassValue accessor cache, strip() over trim() - Test cleanups: latches instead of sleeps, parameterized tables, @tempdir Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add com.exceptionless.logging.LogEventSupport (exported) and use it from the Logback, Log4j 2 and JUL integrations: one client resolution/owned-client lifecycle, internal-logger/thread filter, recursion guard and event factory; only the first failure after start is reported. - Log4j 2: honour ignoreExceptions, stop(timeout), MapMessage fields. - Servlet filter: extend HttpFilter, resolve the started default client per request, register the request plugin once per client, void setter. - Starter: @ConditionalOnBooleanProperty, LogLevel/Path properties, Logback registration re-attaches after a context reset, WebFlux query params from getQueryParams(), class-presence tests, StepVerifier. - Tests: share FakeServer/TestClients via the core test-jar, Awaitility, parameterized level mapping tests. - Build/CI: requirePluginVersions, JaCoCo 0.8.15, central Automatic-Module-Name, SHA-pinned actions, release job permissions and concurrency, timeouts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he console sample - errorprone profile (JDK 21+): Error Prone + NullAway (JSpecify mode) with -Werror - LogEventSupport uses the client's thread-name constant and Strings helpers - Console sample runs in a forked JVM (exec:exec) so the JUL handler loads and crashes are reported Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 83ad2206fc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| Object normalized = | ||
| value instanceof JsonSerializable | ||
| ? value | ||
| : DataNormalizer.normalize(value, maxDepth, excluded); | ||
| if (normalized != null) { | ||
| event.getData().put(name, normalized); |
There was a problem hiding this comment.
Reapply exclusions after loading persisted settings
When a lazily started client has persisted @@DataExclusions, the normal fluent sequence createEvent().setProperty(...).submit() normalizes and stores the property here before submit() starts the client and loads those settings. The pipeline never filters existing custom event data again, so the first event after a restart can transmit a property such as password despite the saved server exclusion. Apply exclusions after startup or scrub event data during submission.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Thanks for the work on this. I traced this at 83ad220: ConfigurationDefaultsPlugin skips existing custom-data keys, and the remaining default plugins don't scrub them before DefaultEventQueue serializes the event. I haven't reproduced it locally yet.
Could we add a regression using synthetic persisted @@DataExclusions and a lazily constructed client, then createEvent().setProperty(...).submit()? Capturing the serialized payload with a fake submission client would let us check that excluded top-level and nested fields are absent while allowed fields remain. Loading saved settings before normalization or applying a final custom-data scrub both seem worth considering, whichever fits the lifecycle best.
| if (isOurUncaughtExceptionHandlerInstalled()) { | ||
| Thread.setDefaultUncaughtExceptionHandler(previousHandler); | ||
| } |
There was a problem hiding this comment.
Unlink closed clients from the uncaught-handler chain
With two clients using the default captureUncaughtExceptions=true, starting A and then B makes B's previous handler point to A. If A is closed before B, this identity check leaves A in the chain; when B later closes, it restores A's now-closed handler as the JVM default. Subsequent uncaught exceptions therefore pass through a closed client and are no longer reported by an active Exceptionless client. Closing a client needs to remove its handler from the chain even when another client is currently installed.
Useful? React with 👍 / 👎.
| Path temp = directory.resolve(stored.fileName() + ".tmp"); | ||
| try { | ||
| Files.writeString(temp, json, StandardCharsets.UTF_8); | ||
| Files.move(temp, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); |
There was a problem hiding this comment.
Fall back when atomic moves are unavailable
When file persistence is configured on a filesystem provider that does not support ATOMIC_MOVE (for example, some mounted or network-backed paths), every enqueue reaches this move and throws AtomicMoveNotSupportedException; the catch deletes the temporary file and the event is never indexed, so persistence silently drops all events. Retry the rename without ATOMIC_MOVE when that specific exception occurs, as durability is still preferable to losing the queue entirely.
Useful? React with 👍 / 👎.
FakeServer and TestClients move to exceptionless-core/src/testFixtures/java. Modules that need them add that directory as a test source through build-helper-maven-plugin, so exceptionless-core no longer attaches a tests jar that would be deployed to Maven Central. Spotless now formats every src/*/java directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 564255f577
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!markProcessed(exception)) { | ||
| context.cancel(); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Defer deduplication until the event is accepted
When the same throwable is first logged and later escapes a servlet or framework boundary, this records it before EventExclusionPlugin and submitting handlers have decided whether the first event will be queued. If that logged event is filtered by a source-specific setting or cancelled by a handler, the later unhandled submission is nevertheless cancelled as already processed, so the exception is never reported. Record the throwable only after the event is accepted, or roll back the marker when downstream processing cancels it.
Useful? React with 👍 / 👎.
| /** Exceptions already reported. Weak, so reported exceptions can still be collected. */ | ||
| private final Set<Throwable> processed = Collections.newSetFromMap(new WeakHashMap<>()); |
There was a problem hiding this comment.
Use identity semantics when tracking reported throwables
For a custom Throwable that overrides equals/hashCode, this WeakHashMap-backed set treats distinct exception instances as the same key. After one such exception is submitted, a later independent occurrence that compares equal is cancelled indefinitely rather than merely deduplicated within the configured duplicate window. The processed set needs weak identity semantics, consistent with the stated intent to track exception instances.
Useful? React with 👍 / 👎.
| } catch (ReflectiveOperationException | RuntimeException e) { | ||
| LOG.debug(() -> "Unable to read record " + value.getClass().getName() + ": " + e); | ||
| return safeToString(value); |
There was a problem hiding this comment.
Preserve exclusions when record reflection fails
When a custom property is a non-public record in a named module whose package is not open, setAccessible throws here; an overridden accessor can trigger the same path. Falling back to the record's generated toString() includes every component, including components such as password that were skipped by the exclusion predicate, so @@DataExclusions can be bypassed and secrets transmitted. Omit the value or retain only the successfully filtered components instead of serializing the whole record as text.
Useful? React with 👍 / 👎.
Match the other Exceptionless builds: MinVer computes the version from v* tags and every job builds with it (-Drevision). A v* tag publishes that version to Maven Central after the tests, end-to-end tests and the Spring Boot 4 job pass; other pushes publish a preview build to GitHub Packages; pull requests only build and test. release.yml is folded into build.yml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When MvcIntegrationTest runs first, its cached application context keeps its own appender on the shared root logger, so asserting that no appender named EXCEPTIONLESS remains failed depending on test-class order (CI vs local). Assert only on the appenders this test's context added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: decda840b7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return; // already processing | ||
| } | ||
| try { | ||
| for (int run = 0; run < MAX_BATCHES_PER_RUN; run++) { |
There was a problem hiding this comment.
Drain beyond the 100-batch cap during shutdown
When the queue contains more than 100 batches—for example, submissionBatchSize(1) with 101 queued events—this hard cap returns while items remain even if every post succeeds. ExceptionlessClient.close() invokes process() only once before shutting down the executor, so remaining in-memory events are lost and file-backed events are deferred until a future restart. Make the shutdown path repeat capped processing until the queue is empty or its deadline expires.
Useful? React with 👍 / 👎.
| Merged existing = merged.get(hash); | ||
| if (existing != null) { | ||
| existing.count += countOf(event); | ||
| context.cancel(); |
There was a problem hiding this comment.
Compare full duplicate signatures instead of integer hashes
When distinct errors have the same 32-bit hash within the duplicate window, this lookup treats them as one signature; for example, otherwise identical exceptions with Java-colliding messages such as Aa and BB collide. Once a merged entry exists, subsequent occurrences of either error are cancelled and counted against that one event, corrupting the reported message/count distribution. Store and compare the complete signature, using its hash only for lookup.
Useful? React with 👍 / 👎.
There has only been a 0.1.0 release, so the rewrite ships as 0.9 (first release v0.9.0-beta.1) ahead of 1.0. CI pre-release builds use alpha instead of preview: Maven orders unknown qualifiers after the release, so 1.0.0-preview.1 would count as newer than 1.0.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 926d71c89f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * <p>Set {@code exceptionless.enabled=false} to turn all of it off. | ||
| */ | ||
| @AutoConfiguration | ||
| @ConditionalOnBooleanProperty(name = "exceptionless.enabled", matchIfMissing = true) |
There was a problem hiding this comment.
Use a condition available throughout Spring Boot 3
Applications using Spring Boot 3.0–3.4 do not provide this annotation—Spring Boot's API documents ConditionalOnBooleanProperty as introduced in 3.5.0—so the starter's auto-configuration cannot load reliably on part of the advertised “Spring Boot 3” range. Use the older ConditionalOnProperty equivalent or explicitly require Spring Boot 3.5+.
Useful? React with 👍 / 👎.
| if (!config.isEnabled()) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Keep background processing available when re-enabling
When a valid-key client starts with enabled(false) and is later changed to enabled(true), this return means the queue and settings tasks were never scheduled. The setter remains usable after startup, so subsequent submissions are accepted into the queue but are never sent automatically; either schedule the disabled-aware tasks unconditionally or reject/reinitialize post-start enable transitions.
Useful? React with 👍 / 👎.
| int excess = index.size() - maxItems + 1; | ||
| for (int i = 0; i < excess; i++) { | ||
| Map.Entry<Key, Stored> oldest = index.pollFirstEntry(); | ||
| deleteQuietly(directory.resolve(oldest.getValue().fileName())); |
There was a problem hiding this comment.
Evict the oldest file only after persisting its replacement
When the persistent queue is full, the oldest entry is deleted before the new event has been written and moved into place. If that write or move then fails—for example because the filesystem is temporarily full—the catch also discards the temporary event, losing both the existing event and the new one; commit the replacement successfully before finalizing the eviction, or restore the evicted entry on failure.
Useful? React with 👍 / 👎.
Implements #91. A ground-up rewrite, released as 0.9 on the way to 1.0, that matches the current Exceptionless server contract and brings the Java client in line with the .NET and JavaScript clients.
What's included
exceptionless-corecom.exceptionless.core. Snake_case wire format, full cause/suppressed chains, request/environment/module info, privacy settings and data exclusions. Server settings (@@log,@@error,@@<type>:<source>,@@DataExclusions,@@IncludePrivateInformation,@@UserAgentBotPatterns). Status-aware queue (429/503/402/401/403/400/404/413 handled like .NET), gzip, optional file persistence, sessions and heartbeats, reference ids and user descriptions. One daemon thread; queued events are flushed on shutdown and uncaught exceptions are reported.exceptionless-logback,-log4j2,-jul@@logsettings override the local minimum level.exceptionless-servletexceptionless-spring-boot-starterexceptionless.*properties, MVC + WebFlux capture, Logback appender wired automatically. Spring Boot 3.5 and 4.1.exceptionless-bomexceptionless-e2e-testsexceptionless/exceptionless:8.10.0(opt-in:-De2e=true).Also: new README,
MIGRATION.md,CONTRIBUTING.md, console and Spring Boot samples, an agent skill, Maven wrapper, Spotless (GJF), JaCoCo, Dependabot, and a single tag-versioned Build workflow (MinVer, like the other Exceptionless builds):v*tags publish to Maven Central, other pushes publishalphapre-release builds to GitHub Packages. The first release will bev0.9.0-beta.1.Breaking: new package
com.exceptionless, new artifacts (exceptionless-coreetc.), Java 17 baseline. SeeMIGRATION.md.Testing
./mvnw verify), on JDK 17, 21 and 25.-Pspring-boot-4), including back-off when Logback, Reactor or the servlet filter are absent.close()and the shutdown hook still sends queued events.Before releasing
CENTRAL_USERNAME,CENTRAL_PASSWORD(a Central Portal user token),GPG_PRIVATE_KEYandGPG_PASSPHRASEsecrets; none exist yet.exceptionless-client0.1.0 (2021) was published by hand through the now-retired OSSRH, so confirm thecom.exceptionlessnamespace appears in the Central Portal account. Releases are cut by pushing av*tag (or creating a GitHub release); the tag name is the version.Server-side follow-ups (not in this PR)
java.*frames are treated as user code in stacking signatures, so JDK frames (e.g.NumberFormatException.forInputString) become the stack signature.$in nested class names isn't normalized in stacking signatures.Closes #91
🤖 Generated with Claude Code