This project follows the Google Java Style Guide.
One of the CI checks will fail if source code is not formatted according to Google Java Style.
Run the following command to reformat all files:
./gradlew spotlessApplyIn addition to Google Java Style formatting, spotless applies
custom static importing rules
(e.g. rewriting Objects.requireNonNull to a static import).
To completely delegate code style formatting to the machine, there is a pre-commit hook setup to verify formatting before committing. It can be activated with this command:
git config core.hooksPath .githooksThe build uses checkstyle to verify some parts of the Google Java Style Guide that cannot be handled by auto-formatting.
To run these checks locally:
./gradlew checkstyleMain checkstyleTest
Follow the principle of minimal necessary visibility. Use the most restrictive access modifier that still allows the code to function correctly.
Classes in .internal packages are not considered public API and may change without notice. These
packages contain implementation details that should not be used by external consumers.
Use .internal packages for implementation classes that need to be public within the module but
should not be used externally
Prefer this order:
- Static fields (final before non-final)
- Static initializer
- Instance fields (final before non-final)
- Constructors
- Methods
- Nested classes
Method ordering: Place calling methods above the methods they call. For example, place private methods below the non-private methods that use them.
Exception — static field initialization: When a static final field is initialized by a
private static method or a static {} block, it is acceptable to place the method or block
immediately after the field to keep initialization logic co-located, even when this contradicts
the general method ordering above.
Static utility classes: Place the private constructor (used to prevent instantiation) after all methods.
Public non-internal non-test classes should be declared final where possible.
"Internal" here includes .internal packages and javaagent/src/main/ classes — javaagent
instrumentation code is not public API.
"Test" here includes src/test/ directories and any module whose directory name starts or ends
with testing or tests (e.g., testing/, testing-common/, testing-apps/,
quarkus-2.0-testing/, smoke-tests/).
Methods should only be declared final if they are in public non-internal non-test non-final classes.
Fields should be declared final where possible.
Method parameters and local variables should never be declared final.
Prefer value == null and value != null over left-hand null comparisons such as
null == value and null != value.
This applies throughout the codebase, including Java, Kotlin, and Scala sources.
Use uppercase (SCREAMING_SNAKE_CASE) for constant-like fields whose value is treated as a stable
identifier, immutable descriptor, or immutable value constant.
Examples that may remain uppercase include:
- literal strings, numbers, and booleans that behave like module constants
- immutable value objects that are treated as fixed constants after initialization, such as
Durationtimeouts, intervals, or deadlines - semantic keys and handles such as
AttributeKey,ContextKey,VirtualField,MethodHandle, andPattern - canonical singleton or sentinel fields named
INSTANCE,EMPTY, orNOOP
Do not use uppercase solely because a field is static final.
Use lower camel case for runtime-created collaborator objects even when they are static final,
for example loggers, instrumenters, helpers, sanitizers, mappers, caches, and similar service
objects.
When deciding between uppercase and lower camel case, distinguish immutable value constants from
collaborators. A private static final Duration FLUSH_TIMEOUT = ...; field may remain uppercase
when it is used as a fixed timeout constant, even if its value is computed from configuration at
startup. In contrast, runtime-created service objects such as instrumenters, tracers, loggers, or
helpers should use lower camel case.
Note: This section is aspirational and may not reflect the current codebase.
Annotate all parameters and fields that can be null with @Nullable
(specifically javax.annotation.Nullable, which is included by the
otel.java-conventions Gradle plugin as a compileOnly dependency).
@NonNull is unnecessary as it is the default.
Test code: Do not add @Nullable annotations in test code.
Defensive programming: Public APIs should still check for null parameters even if not
annotated with @Nullable. Internal APIs do not need these checks.
To help enforce @Nullable annotation usage, the otel.nullaway-conventions Gradle plugin
should be used in all modules to perform basic nullable usage validation:
plugins {
id("otel.nullaway-conventions")
}Following the reasoning from
Writing a Java library with better experience (slide 12),
java.util.Optional usage is kept to a minimum.
Guidelines:
Optionalshouldn't appear in public API signatures- Avoid
Optionalon the hot path (instrumentation code), unless the instrumented library uses it
Library instrumentation: Copy semantic convention constants directly into library instrumentation classes rather than depending on the semconv artifact. Library instrumentation is used by end users, and this avoids exposing a dependency on the semconv artifact (which may change across versions). For example:
// copied from MessagingIncubatingAttributes
private static final AttributeKey<String> MESSAGING_SYSTEM =
AttributeKey.stringKey("messaging.system");Javaagent instrumentation: Use the semconv constants from the semconv artifact directly. The javaagent bundles its own dependencies, so there is no risk of version conflicts for end users.
Tests: Use the semconv constants from the semconv artifact directly. Test dependencies do not affect end users.
Prefer AssertJ assertions over JUnit assertions (assertEquals, assertTrue, etc.) for better error messages.
Test classes and test methods should generally be package-protected (no explicit visibility
modifier) rather than public. This follows the principle of minimal necessary visibility and is
sufficient for JUnit to discover and execute tests.
Avoid allocations on the hot path (instrumentation code) whenever possible. This includes Iterator
allocations from collections; note that for (SomeType t : plainJavaArray) does not allocate an
iterator object.
Non-allocating Stream API usage on the hot path is acceptable but may not fit the surrounding code
style; this is a judgment call. Some Stream APIs make efficient allocation difficult (e.g.,
collect with pre-sized sink data structures involves convoluted Supplier code, or lambdas passed
to forEach may be capturing/allocating lambdas).