@@ -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
10099signal beyond the version role itself, for example native instrumentation notes, backport
101100context, 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
108106For 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
114112return 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
163162public 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