Skip to content

Commit 83ad220

Browse files
ejsmithclaude
andcommitted
Enforce Error Prone and NullAway; share the client thread name; fix the 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>
1 parent ce8b371 commit 83ad220

10 files changed

Lines changed: 106 additions & 19 deletions

File tree

‎.mvn/jvm.config‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
2+
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
3+
--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
4+
--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
5+
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
6+
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
7+
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
8+
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
9+
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
10+
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED

‎CONTRIBUTING.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ Test the Spring Boot starter against Spring Boot 4 with `-Pspring-boot-4`.
3333
end-to-end tests guard this.
3434
- Public API gets javadoc. Packages are `@NullMarked` (JSpecify); mark nullable types with `@Nullable`.
3535
- Formatting is google-java-format, enforced by Spotless.
36+
- On JDK 21 and newer the build also runs [Error Prone](https://errorprone.info) and
37+
[NullAway](https://github.com/uber/NullAway) on the main sources, with warnings as errors. Fix
38+
findings rather than suppressing them; when a suppression is right, say why next to it.
39+
- Write idiomatic Java 17: records for values, sealed types for closed results, `java.time` types
40+
(`Duration`, `InstantSource`) instead of numbers, immutable collections from getters, and plain
41+
JavaBeans (`void` setters) for the mutable models.
3642

3743
## Releasing
3844

‎exceptionless-core/src/main/java/com/exceptionless/ExceptionlessClient.java‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.time.Clock;
3030
import java.time.Duration;
3131
import java.time.OffsetDateTime;
32+
import java.time.ZoneId;
3233
import java.util.Collections;
3334
import java.util.IdentityHashMap;
3435
import java.util.List;
@@ -101,7 +102,8 @@ private enum State {
101102

102103
/** Creates a client. It starts automatically when first used. */
103104
public ExceptionlessClient(ExceptionlessConfiguration configuration) {
104-
this(configuration, Clock.systemDefaultZone());
105+
// Event dates carry the local UTC offset, like the other Exceptionless clients.
106+
this(configuration, Clock.system(ZoneId.systemDefault()));
105107
}
106108

107109
/** For tests: {@code clock} dates new events, in its time zone. */
@@ -204,7 +206,8 @@ private void initialize() {
204206
queue = eventQueue;
205207

206208
ScheduledThreadPoolExecutor executor =
207-
new ScheduledThreadPoolExecutor(1, new DaemonThreadFactory("exceptionless"));
209+
new ScheduledThreadPoolExecutor(
210+
1, new DaemonThreadFactory(DaemonThreadFactory.CLIENT_THREAD_NAME));
208211
executor.setRemoveOnCancelPolicy(true);
209212
executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
210213
executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
@@ -237,7 +240,7 @@ private void initialize() {
237240
shuttingDownFromHook = true;
238241
close();
239242
},
240-
"exceptionless-shutdown");
243+
DaemonThreadFactory.CLIENT_THREAD_NAME + "-shutdown");
241244
try {
242245
Runtime.getRuntime().addShutdownHook(hook);
243246
shutdownHook = hook;

‎exceptionless-core/src/main/java/com/exceptionless/internal/DaemonThreadFactory.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
/** Creates daemon threads so the client never keeps the JVM alive. */
77
public final class DaemonThreadFactory implements ThreadFactory {
8+
/** The name prefix of the client's background threads ({@code exceptionless-1}, ...). */
9+
public static final String CLIENT_THREAD_NAME = "exceptionless";
10+
811
private final String prefix;
912
private final AtomicInteger counter = new AtomicInteger();
1013

‎exceptionless-core/src/main/java/com/exceptionless/logging/LogEventSupport.java‎

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import com.exceptionless.ExceptionlessClient;
66
import com.exceptionless.ExceptionlessConfiguration;
77
import com.exceptionless.LogLevel;
8+
import com.exceptionless.internal.DaemonThreadFactory;
9+
import com.exceptionless.internal.Strings;
810
import java.time.Duration;
911
import java.util.Objects;
1012
import java.util.concurrent.atomic.AtomicBoolean;
@@ -68,7 +70,7 @@ public final class LogEventSupport implements AutoCloseable {
6870
* The client's background threads are named with this prefix ({@code exceptionless-1}, ...);
6971
* anything logged on them is never sent.
7072
*/
71-
public static final String CLIENT_THREAD_PREFIX = "exceptionless-";
73+
public static final String CLIENT_THREAD_PREFIX = DaemonThreadFactory.CLIENT_THREAD_NAME + "-";
7274

7375
/** Set while the current thread submits an event, to drop events logged meanwhile. */
7476
private static final ThreadLocal<Boolean> SUBMITTING = new ThreadLocal<>();
@@ -121,7 +123,7 @@ public String getSubmissionMethod() {
121123
* owned for it. Blank values count as unset. Changing it closes a client created for the old key.
122124
*/
123125
public void setApiKey(@Nullable String apiKey) {
124-
String value = trimToNull(apiKey);
126+
String value = Strings.stripToNull(apiKey);
125127
if (!Objects.equals(value, this.apiKey)) {
126128
this.apiKey = value;
127129
resetOwnedClient();
@@ -137,7 +139,7 @@ public void setApiKey(@Nullable String apiKey) {
137139
* unset. Changing it closes a client created for the old URL.
138140
*/
139141
public void setServerUrl(@Nullable String serverUrl) {
140-
String value = trimToNull(serverUrl);
142+
String value = Strings.stripToNull(serverUrl);
141143
if (!Objects.equals(value, this.serverUrl)) {
142144
this.serverUrl = value;
143145
resetOwnedClient();
@@ -378,12 +380,4 @@ private void closeOwned(@Nullable ExceptionlessClient owned, @Nullable Duration
378380
errorReporter.report("Unable to close the Exceptionless client", e);
379381
}
380382
}
381-
382-
private static @Nullable String trimToNull(@Nullable String value) {
383-
if (value == null) {
384-
return null;
385-
}
386-
String trimmed = value.trim();
387-
return trimmed.isEmpty() ? null : trimmed;
388-
}
389383
}

‎exceptionless-core/src/main/java/com/exceptionless/storage/FileQueueStorage.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,10 @@ public synchronized void remove(Collection<QueueEntry> entries) {
222222
public synchronized void incrementAttempts(Collection<QueueEntry> entries) {
223223
for (QueueEntry entry : entries) {
224224
Parsed parsed = Parsed.of(entry.id());
225-
Stored stored = parsed != null ? index.get(parsed.key()) : null;
225+
if (parsed == null) {
226+
continue;
227+
}
228+
Stored stored = index.get(parsed.key());
226229
if (stored == null) {
227230
continue;
228231
}

‎exceptionless-spring-boot-starter/src/main/java/com/exceptionless/spring/boot/LogbackAppenderRegistration.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public void destroy() {
8383

8484
/** Logback removed and stopped every appender: add a new one. */
8585
@Override
86+
@SuppressWarnings("ReferenceEquality") // Only events from the context we attached to.
8687
public void onReset(LoggerContext loggerContext) {
8788
synchronized (lock) {
8889
if (loggerContext == context && loggerContext.isStarted()) {
@@ -94,6 +95,7 @@ public void onReset(LoggerContext loggerContext) {
9495

9596
/** The Logback context stopped (the JVM is shutting down): nothing more to send. */
9697
@Override
98+
@SuppressWarnings("ReferenceEquality") // Only events from the context we attached to.
9799
public void onStop(LoggerContext loggerContext) {
98100
synchronized (lock) {
99101
if (loggerContext == context) {

‎pom.xml‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@
7878
<junit.version>5.13.4</junit.version>
7979
<assertj.version>3.27.7</assertj.version>
8080
<awaitility.version>4.3.0</awaitility.version>
81+
<errorprone.version>2.50.0</errorprone.version>
82+
<nullaway.version>0.14.2</nullaway.version>
8183

8284
<!-- Plugins (all pinned; the enforcer's requirePluginVersions rule checks it) -->
8385
<maven-clean-plugin.version>3.5.0</maven-clean-plugin.version>
@@ -452,5 +454,46 @@
452454
</plugins>
453455
</build>
454456
</profile>
457+
<profile>
458+
<id>errorprone</id>
459+
<activation>
460+
<jdk>[21,)</jdk>
461+
</activation>
462+
<build>
463+
<plugins>
464+
<plugin>
465+
<groupId>org.apache.maven.plugins</groupId>
466+
<artifactId>maven-compiler-plugin</artifactId>
467+
<executions>
468+
<execution>
469+
<id>default-compile</id>
470+
<configuration>
471+
<fork>false</fork>
472+
<compilerArgs combine.children="append">
473+
<arg>-XDcompilePolicy=simple</arg>
474+
<arg>-XDaddTypeAnnotationsToSymbol=true</arg>
475+
<arg>--should-stop=ifError=FLOW</arg>
476+
<arg>-Werror</arg>
477+
<arg>-Xplugin:ErrorProne -XepDisableWarningsInGeneratedCode -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:JSpecifyMode=true</arg>
478+
</compilerArgs>
479+
<annotationProcessorPaths combine.children="append">
480+
<path>
481+
<groupId>com.google.errorprone</groupId>
482+
<artifactId>error_prone_core</artifactId>
483+
<version>${errorprone.version}</version>
484+
</path>
485+
<path>
486+
<groupId>com.uber.nullaway</groupId>
487+
<artifactId>nullaway</artifactId>
488+
<version>${nullaway.version}</version>
489+
</path>
490+
</annotationProcessorPaths>
491+
</configuration>
492+
</execution>
493+
</executions>
494+
</plugin>
495+
</plugins>
496+
</build>
497+
</profile>
455498
</profiles>
456499
</project>

‎samples/console/pom.xml‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<maven.deploy.skip>true</maven.deploy.skip>
1919
<maven.javadoc.skip>true</maven.javadoc.skip>
2020
<maven.source.skip>true</maven.source.skip>
21-
<exec.mainClass>com.example.console.ConsoleApp</exec.mainClass>
21+
<sample.args></sample.args>
2222
</properties>
2323

2424
<dependencies>
@@ -31,4 +31,24 @@
3131
<artifactId>exceptionless-jul</artifactId>
3232
</dependency>
3333
</dependencies>
34+
35+
<build>
36+
<plugins>
37+
<!-- exec:exec runs the sample in its own JVM, like a real app: java.util.logging can load the
38+
handler from the class path, and shutdown hooks and uncaught exceptions behave normally. -->
39+
<plugin>
40+
<groupId>org.codehaus.mojo</groupId>
41+
<artifactId>exec-maven-plugin</artifactId>
42+
<configuration>
43+
<executable>java</executable>
44+
<arguments>
45+
<argument>-classpath</argument>
46+
<classpath/>
47+
<argument>com.example.console.ConsoleApp</argument>
48+
<argument>${sample.args}</argument>
49+
</arguments>
50+
</configuration>
51+
</plugin>
52+
</plugins>
53+
</build>
3454
</project>

‎samples/console/src/main/java/com/example/console/ConsoleApp.java‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
* <p>Run it with your API key (and server URL when self hosting):
1717
*
1818
* <pre>
19-
* EXCEPTIONLESS_API_KEY=your-key ./mvnw -pl samples/console -am -q exec:java
19+
* ./mvnw -q install -DskipTests
20+
* EXCEPTIONLESS_API_KEY=your-key ./mvnw -q -pl samples/console exec:exec
2021
* EXCEPTIONLESS_API_KEY=your-key EXCEPTIONLESS_SERVER_URL=http://localhost:5200 ./mvnw ...
22+
* EXCEPTIONLESS_API_KEY=your-key ./mvnw -q -pl samples/console exec:exec -Dsample.args=crash
2123
* </pre>
2224
*/
2325
public final class ConsoleApp {
@@ -71,7 +73,8 @@ public static void main(String[] args) throws Exception {
7173
String referenceId = Exceptionless.getLastReferenceId();
7274
System.out.println("Reported the error; tell support it was " + referenceId);
7375
Exceptionless.getDefault()
74-
.updateUserEmailAndDescription(referenceId, "user@example.com", "I clicked Order twice");
76+
.updateUserEmailAndDescription(referenceId, "user@example.com", "I clicked Order twice")
77+
.join();
7578
}
7679

7780
// JUL records reach Exceptionless through ExceptionlessHandler (see logging.properties).
@@ -99,6 +102,6 @@ private static void placeOrder(Map<String, Object> order) {
99102
}
100103

101104
private static void chargeCard(Map<String, Object> order) {
102-
throw new UnsupportedOperationException("Card declined");
105+
throw new UnsupportedOperationException("Card declined for order " + order.get("id"));
103106
}
104107
}

0 commit comments

Comments
 (0)