Skip to content

Commit a9f8b91

Browse files
committed
copyedit
1 parent 084453f commit a9f8b91

1 file changed

Lines changed: 99 additions & 106 deletions

File tree

.github/agents/knowledge/javaagent-module-patterns.md

Lines changed: 99 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,31 @@ public class MyLibrary10InstrumentationModule extends InstrumentationModule {
4141

4242
### `classLoaderMatcher()` — Version-Boundary Detection
4343

44-
Override `classLoaderMatcher()` when the instrumentation targets a specific library version
45-
range and muzzle alone cannot distinguish it (e.g., a class was added or removed between
46-
versions but isn't referenced by helper classes).
44+
Override `classLoaderMatcher()` only when the instrumentation targets a specific library
45+
version range and muzzle cannot distinguish that range on its own. Typical cases are classes
46+
that were added, removed, renamed, or introduced by a library's own native OpenTelemetry
47+
instrumentation, but are not referenced by the helper classes muzzle inspects.
4748

48-
**Execution order**: `classLoaderMatcher()` runs **first** (fast), then `typeMatcher()`, then
49-
muzzle (expensive, cached). Use `classLoaderMatcher()` to pre-filter before muzzle runs.
49+
**Execution order**: `classLoaderMatcher()` runs **first** and should be very cheap. It is
50+
followed by `typeMatcher()`, then muzzle. Use `classLoaderMatcher()` to reject obvious
51+
non-matches before the more expensive checks run.
5052

51-
**When to override** (otherwise let muzzle handle it):
53+
**Override it when**:
5254

53-
- A landmark class was **added** in the target version (exclude older versions).
54-
- A landmark class was **removed** in a newer version (exclude newer versions).
55-
- A newer version has **native OpenTelemetry instrumentation** — avoid double-instrumentation.
55+
- A landmark class was **added** in the target version and excludes older versions.
56+
- A landmark class was **removed** in a newer version and excludes newer versions.
57+
- A newer version ships **native OpenTelemetry instrumentation** and the agent should back off.
5658

57-
**When NOT to override** (muzzle catches these automatically):
59+
**Do not override it for**:
5860

59-
- Method signature changes, parameter type changes, field removals, package relocations.
61+
- Method signature changes.
62+
- Parameter type changes.
63+
- Field removals.
64+
- Package relocations that muzzle already sees.
6065

61-
#### Pattern A: Check for a class added in the target version (most common)
66+
#### Pattern A: Single Landmark Class
67+
68+
This is the most common case: one class cleanly identifies the lower bound.
6269

6370
```java
6471
@Override
@@ -68,66 +75,58 @@ public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
6875
}
6976
```
7077

71-
#### Pattern B: Multiple landmark classes (AND logic)
72-
73-
`hasClassesNamed()` with multiple arguments requires **ALL** classes to be present. A single
74-
class suffices for a simple version boundary — use multiple classes only when:
75-
76-
- **One class is ambiguous** — e.g., it was backported to an older branch, so it doesn't
77-
uniquely identify the target version. Adding a second class narrows the match.
78-
- **Floor + ceiling pinning** — one class proves "at least version X", another proves
79-
"not yet version Y" (e.g., Hibernate 3.3 checks a class absent before 3.3 AND a class
80-
absent in 4.0+, pinning to exactly 3.3.x–3.x).
81-
- **Cross-JAR dependency** — the classes come from different JARs that may be independently
82-
present (e.g., Jersey checks both the JAX-RS spec API and the Jersey implementation class;
83-
AWS SDK Lambda checks both the Lambda module and JSON protocol core).
84-
85-
For multi-class checks or negated exclusions, place a comment **above each class name string**
86-
stating its version role and matcher shape:
87-
88-
- **Positive floor class** in `hasClassesNamed(...)` (proves "at least version X"):
89-
use `// added in X.Y`, optionally followed by brief parenthetical context only when it adds
90-
interesting, non-duplicative signal.
91-
- **Positive ceiling class** in `hasClassesNamed(...)` (proves "not yet version Y"):
92-
use `// removed in Y.Z`, optionally followed by brief parenthetical context only when it
93-
adds interesting, non-duplicative signal.
78+
#### Pattern B: Multiple Landmark Classes
79+
80+
`hasClassesNamed()` with multiple arguments requires **all** listed classes to be present.
81+
Use multiple classes only when one class is not enough.
82+
83+
- **Ambiguous floor**: one class was backported and no longer uniquely identifies the target
84+
version.
85+
- **Floor + ceiling pinning**: one class proves "at least X", another proves "not yet Y".
86+
- **Cross-JAR dependency**: the version boundary depends on classes from separate artifacts,
87+
such as a spec API plus a library implementation.
88+
89+
For multi-class checks, and for negated exclusions, add a version comment **above each class
90+
name string**.
91+
92+
- **Positive floor class** in `hasClassesNamed(...)`: use `// added in X.Y`.
93+
- **Positive ceiling class** in `hasClassesNamed(...)`: use `// removed in Y.Z`.
9494
- **Negated exclusion class** in `not(hasClassesNamed(...))` or
95-
`.and(not(hasClassesNamed(...)))`: use `// added in Y.Z`, optionally followed by brief
96-
parenthetical context only when it adds interesting, non-duplicative signal, because the
97-
class's presence starts excluding `Y.Z+`.
95+
`.and(not(hasClassesNamed(...)))`: use `// added in Y.Z`, because that class's first
96+
appearance is what starts excluding `Y.Z+`.
9897

99-
Short parenthetical details are allowed only when they add interesting, non-duplicative
98+
You may add brief parenthetical context, but only when it adds interesting, non-duplicative
10099
signal beyond the version role itself, for example native instrumentation notes, backport
101100
context, or namespace rename notes.
102101

103-
A single **positive** landmark class can sometimes provide **both** bounds: its presence
104-
proves the module is at least version `X.Y`, and the same class disappears in `Y.Z`, so
105-
its presence also excludes `Y.Z+`. In that case, a combined comment such as
106-
`// added in X.Y, removed in Y.Z` is appropriate.
102+
One positive landmark class can sometimes provide **both** bounds: its presence proves the
103+
module is at least version `X.Y`, and the same class disappears in `Y.Z`, so it also excludes
104+
`Y.Z+`. In that case, use a combined comment such as `// added in X.Y, removed in Y.Z`.
107105

108106
For a single-item `hasClassesNamed(...)` check, prefer the compact form with the version
109-
comment above the statement or expression that uses it, instead of splitting the sole string
110-
onto its own line. For example:
107+
comment above the statement or expression that uses the matcher, instead of splitting the
108+
single string onto its own line:
111109

112110
```java
113111
// added in 3.0
114112
return hasClassesNamed("jakarta.faces.context.FacesContext");
115113
```
116114

117-
And for a single negated exclusion:
115+
The same compact style applies to single negated exclusions:
118116

119117
```java
120118
// added in 8.10 (native OTel support)
121119
.and(not(hasClassesNamed("co.elastic.clients.transport.instrumentation.Instrumentation")));
122120
```
123121

124-
**How to identify ceiling classes**: check whether a **newer sibling module** exists for
125-
the same library (e.g., `mongo-4.0` next to `mongo-3.7`). If the newer module's
126-
`classLoaderMatcher()` checks a different variant of the same class (e.g.,
127-
`com.mongodb.internal.async.SingleResultCallback` vs `com.mongodb.async.SingleResultCallback`),
128-
the old variant was likely removed in the newer version and serves as a ceiling. A ceiling
129-
class may have been *introduced* much earlier than the module's target version — the relevant
130-
fact is when it was **removed**, not when it was added.
122+
**How to identify ceiling classes**: look for a **newer sibling module** for the same
123+
library, such as `mongo-4.0` next to `mongo-3.7`. If the newer module's
124+
`classLoaderMatcher()` checks a different variant of the same class, such as
125+
`com.mongodb.internal.async.SingleResultCallback` instead of
126+
`com.mongodb.async.SingleResultCallback`, the older variant was likely removed in the newer
127+
version and can serve as the ceiling. A ceiling class may have been introduced much earlier
128+
than the module's target version; the important fact is when it was **removed**, not when it
129+
first appeared.
131130

132131
```java
133132
@Override
@@ -140,10 +139,10 @@ public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
140139
}
141140
```
142141

143-
Here `ConfigurationStrSubstitutor` alone would also match 2.12.3 (due to back-port),
144-
so `DefaultArbiter` (added in 2.15) is required to tighten the match to 2.17+.
142+
Here `ConfigurationStrSubstitutor` alone would also match 2.12.3 because of the backport, so
143+
`DefaultArbiter` is needed to tighten the match to 2.17+.
145144

146-
Another common shape floor + ceiling:
145+
Another common shape is floor + ceiling pinning:
147146

148147
```java
149148
@Override
@@ -156,27 +155,27 @@ public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
156155
}
157156
```
158157

159-
Another floor + ceiling example mongo 3.7 pins to 3.7.x–3.x:
158+
Another example pins mongo 3.7 to 3.7.x–3.x:
160159

161160
```java
162161
@Override
163162
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
164163
return hasClassesNamed(
165164
// added in 3.7
166165
"com.mongodb.MongoClientSettings$Builder",
167-
// removed in 4.0 replaced by com.mongodb.internal.async.SingleResultCallback
166+
// removed in 4.0 (replaced by com.mongodb.internal.async.SingleResultCallback)
168167
"com.mongodb.async.SingleResultCallback");
169168
}
170169
```
171170

172-
Here `SingleResultCallback` was introduced in driver 3.0 but **removed in 4.0**. Its
173-
presence ensures the 3.7 module does not activate on 4.0+ classloaders. The comment must
174-
say `// removed in 4.0`, not `// added in 3.0` — the purpose is the upper bound.
171+
`SingleResultCallback` was introduced in driver 3.0 but **removed in 4.0**. Its presence keeps
172+
the 3.7 module from activating on 4.0+ classloaders. That is why the comment must say
173+
`// removed in 4.0`, not `// added in 3.0`.
175174

176-
#### Pattern C: Exclude newer versions with native instrumentation
175+
#### Pattern C: Exclude Newer Versions with Native Instrumentation
177176

178-
Use `.and(not(hasClassesNamed(...)))` to opt out when the library ships its own
179-
OpenTelemetry instrumentation:
177+
Use `.and(not(hasClassesNamed(...)))` when the newer library version ships its own
178+
OpenTelemetry instrumentation and the agent should opt out.
180179

181180
```java
182181
@Override
@@ -189,7 +188,7 @@ public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
189188
}
190189
```
191190

192-
#### Pattern D: Exclude a version range (upper bound)
191+
#### Pattern D: Exclude an Upper Range
193192

194193
```java
195194
@Override
@@ -205,49 +204,43 @@ public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
205204

206205
- **Do NOT add `classLoaderMatcher()` for optimization.** The "skip when library is absent"
207206
optimization belongs on `TypeInstrumentation.classLoaderOptimization()`, not here.
208-
`classLoaderMatcher()` is only for **version-boundary detection** — cases where muzzle
209-
alone cannot distinguish the target version range. Most modules do not need it.
210-
- **Do NOT flag modules that lack a `classLoaderMatcher()` override.** The default (`any()`)
211-
is correct when muzzle can detect version boundaries on its own (the common case). Only
212-
flag a *missing* override when the module targets a specific version range and the
213-
versioning signal (added/removed landmark class) is not part of the classes muzzle inspects.
214-
- **Version comments on landmark classes.** For multi-class checks or cases where the
215-
landmark version differs from the module's base version, each `hasClassesNamed()` call
216-
must have a `//` comment stating the class's **role**. For a single-item
217-
`hasClassesNamed(...)` check, including a negated exclusion in `.and(not(...))`, prefer
218-
the compact form with the comment above the statement or expression instead of splitting
219-
the sole string onto its own line:
220-
- Positive floor class → `// added in X.Y` (optionally with brief, non-duplicative
221-
parenthetical context such as `renamed from javax` or `backported to 2.12.3`).
222-
- Positive ceiling class → `// removed in Y.Z` (optionally with brief, non-duplicative
223-
parenthetical context such as `replaced by jakarta variant` or `moved to internal.async`).
224-
- Negated exclusion class in `not(hasClassesNamed(...))``// added in Y.Z`
225-
(optionally with brief, non-duplicative parenthetical context such as `native OTel
226-
support` or `shaded into core module`).
227-
- Single positive class that serves as both floor and ceiling → include both boundaries,
228-
e.g. `// added in X.Y, removed in Y.Z`.
229-
Do not use `// added in` for a **positive** ceiling class — that states when the class
230-
first appeared, which is irrelevant and misleading; the purpose is the upper bound.
231-
Conversely, do use `// added in` for a **negated** exclusion class, because the class's
232-
first appearance is exactly what begins the excluded version range.
233-
To identify ceiling classes, check if a newer sibling module's `classLoaderMatcher()`
234-
checks a different variant or replacement class.
235-
- **Single-class landmark comments are required.** When a single-class
236-
`hasClassesNamed(...)` check establishes the module's lower bound, or when a single-class
237-
negated check establishes an excluded upper range, prefer the compact form with the
238-
version comment immediately above the statement or expression that uses the matcher.
239-
Parenthetical context is fine only when it adds interesting, non-duplicative signal. If
240-
the same positive class also establishes the upper bound because it is removed or replaced
241-
in a newer sibling version, include that fact too, e.g. `// added in X.Y, removed in Y.Z`.
242-
First validate each stated boundary from repository or upstream evidence; do not infer
243-
versions from the module name alone. Flag a missing boundary comment in this case, and do
244-
not flag an existing dual-role comment for removal unless it is demonstrably wrong.
245-
- Prefer **one landmark class** per version boundary — choose the most stable/specific class.
207+
`classLoaderMatcher()` is only for **version-boundary detection**. Most modules do not need
208+
it.
209+
- **Do NOT flag modules that omit `classLoaderMatcher()`.** The default (`any()`) is correct
210+
when muzzle can detect the version boundary on its own. Only flag a missing override when
211+
the module truly depends on an added or removed landmark class that muzzle does not inspect.
212+
- **Version comments are required on landmark classes.** For multi-class checks, or whenever
213+
the landmark version differs from the module's base version, every `hasClassesNamed()` call
214+
needs a role comment. For a single-item `hasClassesNamed(...)` check, including a negated
215+
exclusion in `.and(not(...))`, prefer the compact form with the comment above the statement
216+
or expression instead of placing the only string on its own line.
217+
- Positive floor class → `// added in X.Y`, optionally with brief, non-duplicative context
218+
such as `renamed from javax` or `backported to 2.12.3`.
219+
- Positive ceiling class → `// removed in Y.Z`, optionally with brief, non-duplicative
220+
context such as `replaced by jakarta variant` or `moved to internal.async`.
221+
- Negated exclusion class → `// added in Y.Z`, optionally with brief, non-duplicative
222+
context such as `native OTel support` or `shaded into core module`.
223+
- Single positive class serving as both floor and ceiling → include both boundaries, for
224+
example `// added in X.Y, removed in Y.Z`.
225+
Do not use `// added in` for a **positive** ceiling class, because the upper bound depends on
226+
when that class disappeared, not when it first appeared. Conversely, do use `// added in`
227+
for a **negated** exclusion class, because its first appearance is what starts the excluded
228+
range.
229+
- **Single-class landmark comments are required.** When a single-class `hasClassesNamed(...)`
230+
check establishes the module's lower bound, or when a single-class negated check establishes
231+
an excluded upper range, use the compact form with the version comment immediately above the
232+
statement or expression that uses the matcher. Parenthetical context is fine only when it
233+
adds interesting, non-duplicative signal. If the same positive class also establishes the
234+
upper bound because it is removed or replaced in a newer sibling version, include that too,
235+
for example `// added in X.Y, removed in Y.Z`. First validate the stated boundaries from
236+
repository or upstream evidence; do not infer them from the module name alone.
237+
- Prefer **one landmark class** per version boundary whenever possible.
246238
- Pair with **muzzle config** (`assertInverse.set(true)`) for full coverage.
247-
- Use `hasClassesNamed(...)` (from `AgentElementMatchers`) — not raw ByteBuddy matchers.
239+
- Use `hasClassesNamed(...)` from `AgentElementMatchers`, not raw ByteBuddy matchers.
248240
- `classLoaderMatcher()` runs against **every class loader** in the JVM before type matching
249-
begins. It must be fast: use only `hasClassesNamed(...)` (a simple class-presence check).
250-
Never use hierarchy matchers (`extendsClass(...)`, `implementsInterface(...)`) here.
241+
begins, so it must stay fast. Use only `hasClassesNamed(...)`, which is a simple
242+
class-presence check. Never use hierarchy matchers such as `extendsClass(...)` or
243+
`implementsInterface(...)` here.
251244

252245
## TypeInstrumentation
253246

0 commit comments

Comments
 (0)