Skip to content

Commit ee0e0a9

Browse files
author
Mateusz Rzeszutek
authored
Muzzle improvements: docs, javadocs, renamings and minor refactoring (#1379)
1 parent 7ecc9bb commit ee0e0a9

File tree

20 files changed

+743
-635
lines changed

20 files changed

+743
-635
lines changed

buildSrc/src/main/groovy/MuzzlePlugin.groovy

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class MuzzlePlugin implements Plugin<Project> {
7474
project.getLogger().info('No muzzle pass directives configured. Asserting pass against instrumentation compile-time dependencies')
7575
ClassLoader userCL = createCompileDepsClassLoader(project, bootstrapProject)
7676
ClassLoader instrumentationCL = createInstrumentationClassloader(project, toolingProject)
77-
Method assertionMethod = instrumentationCL.loadClass('io.opentelemetry.javaagent.tooling.muzzle.MuzzleVersionScanPlugin')
77+
Method assertionMethod = instrumentationCL.loadClass('io.opentelemetry.javaagent.tooling.muzzle.matcher.MuzzleGradlePluginUtil')
7878
.getMethod('assertInstrumentationMuzzled', ClassLoader.class, ClassLoader.class, boolean.class)
7979
assertionMethod.invoke(null, instrumentationCL, userCL, true)
8080
}
@@ -86,7 +86,7 @@ class MuzzlePlugin implements Plugin<Project> {
8686
description = "Print references created by instrumentation muzzle"
8787
doLast {
8888
ClassLoader instrumentationCL = createInstrumentationClassloader(project, toolingProject)
89-
Method assertionMethod = instrumentationCL.loadClass('io.opentelemetry.javaagent.tooling.muzzle.MuzzleVersionScanPlugin')
89+
Method assertionMethod = instrumentationCL.loadClass('io.opentelemetry.javaagent.tooling.muzzle.matcher.MuzzleGradlePluginUtil')
9090
.getMethod('printMuzzleReferences', ClassLoader.class)
9191
assertionMethod.invoke(null, instrumentationCL)
9292
}
@@ -340,7 +340,7 @@ class MuzzlePlugin implements Plugin<Project> {
340340
ClassLoader userCL = createClassLoaderForTask(instrumentationProject, bootstrapProject, taskName)
341341
try {
342342
// find all instrumenters, get muzzle, and assert
343-
Method assertionMethod = instrumentationCL.loadClass('io.opentelemetry.javaagent.tooling.muzzle.MuzzleVersionScanPlugin')
343+
Method assertionMethod = instrumentationCL.loadClass('io.opentelemetry.javaagent.tooling.muzzle.matcher.MuzzleGradlePluginUtil')
344344
.getMethod('assertInstrumentationMuzzled', ClassLoader.class, ClassLoader.class, boolean.class)
345345
assertionMethod.invoke(null, instrumentationCL, userCL, muzzleDirective.assertPass)
346346
} finally {

docs/contributing/muzzle.md

Lines changed: 94 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,109 @@
11
# Muzzle
22

3-
Muzzle is a feature of the Java agent that ensures API compatibility
4-
between libraries/symbols on the application classpath and APIs of instrumented
5-
3rd party libraries used by the Agent. In other words the Muzzle ensures
6-
that the API symbols used by the Agent are compatible with API symbols
7-
on the application classpath. The Muzzle will prevent loading an instrumentation
8-
if the APIs do not match.
3+
Muzzle is a safety feature of the Java agent that prevents applying instrumentation when a mismatch
4+
between the instrumentation code and the instrumented application code is detected.
5+
It ensures API compatibility between symbols (classes, methods, fields) on the application classpath
6+
and references to those symbols made by instrumentation advices defined in the agent.
7+
In other words, muzzle ensures that the API symbols used by the agent are compatible with the API
8+
symbols on the application classpath.
99

10-
## How does it work
10+
Muzzle will prevent loading an instrumentation if it detects any mismatch or conflict.
1111

12-
At build time, for each instrumentation the Muzzle ByteBuddy plugin collects symbols referring to both internal
13-
and 3rd party APIs used by the currently processed instrumentation. The reference collection process starts
14-
from advice classes - values of the map returned by the `Instrumenter.Default#transformers()` method.
12+
## How it works
1513

16-
All those references are then used to create a `ReferenceMatcher` instance.
17-
The matcher is stored in the instrumentation class in method `ReferenceMatcher getInstrumentationMuzzle()`.
14+
Muzzle has two phases:
15+
* at compile time it collects references to the third-party symbols;
16+
* at runtime it compares those references to the actual API symbols on the classpath.
1817

19-
At runtime the Muzzle checks API compatibility between symbols used by the Agent
20-
and symbols in the application class loader. If the symbols do not match the instrumentation is not loaded.
21-
Because the muzzle matcher is expensive, it is only performed after a match has been made by the
22-
`SomeInstrumentation.classLoaderMatcher()` and `SomeInstrumentation.typeMatcher()` matchers.
18+
### Compile-time reference collection
19+
20+
The compile-time reference collection and code generation process is implemented using a ByteBuddy
21+
plugin (called `MuzzleCodeGenerationPlugin`).
22+
23+
For each instrumentation the ByteBuddy plugin collects symbols referring to both internal and third
24+
party APIs used by the currently processed instrumentation. The reference collection process starts
25+
from advice classes (values of the map returned by the `Instrumenter.Default#transformers()` method)
26+
and traverses the class graph until it encounters a reference to a non-instrumentation class.
27+
28+
All collected references are then used to create a `ReferenceMatcher` instance. This matcher
29+
is stored in the instrumentation class in the method `Instrumenter.Default#getMuzzleReferenceMatcher()`.
30+
The bytecode of this method (basically an array of `Reference` builder calls) is generated
31+
automatically by the ByteBuddy plugin using an ASM code visitor.
32+
33+
The source code of the compile-time plugin is located in the `javaagent-tooling` module,
34+
package `io.opentelemetry.javaagent.tooling.muzzle.collector`.
35+
36+
### Runtime reference matching
37+
38+
The runtime reference matching process is implemented as a ByteBuddy matcher in `Instrumenter.Default`.
39+
`MuzzleMatcher` uses the `getMuzzleReferenceMatcher()` method generated during the compilation phase
40+
to verify that the class loader of the instrumented type has all necessary symbols (classes,
41+
methods, fields). If the `ReferenceMatcher` finds any mismatch between collected references and the
42+
actual application classpath types the whole instrumentation is discarded.
43+
44+
It is worth noting that because the muzzle check is expensive, it is only performed after a match
45+
has been made by the `InstrumenterDefault#classLoaderMatcher()` and `Instrumenter.Default#typeMatcher()`
46+
matchers.
47+
48+
The source code of the runtime muzzle matcher is located in the `javaagent-tooling` module,
49+
in the class `Instrumenter.Default` and under the package `io.opentelemetry.javaagent.tooling.muzzle`.
2350

2451
## Muzzle gradle plugin
2552

26-
The `printReferences` task prints all API references in a given module.
53+
The muzzle gradle plugin allows to perform the runtime reference matching process against different
54+
third party library versions, when the project is built.
2755

28-
```bash
29-
./gradlew :instrumentation:google-http-client-1.19:printReferences
30-
```
56+
Muzzle gradle plugin is just an additional utility for enhanced build-time checking
57+
to alert us when there are breaking changes in the underlying third party library
58+
that will cause the instrumentation not to get applied.
59+
**Even without using it muzzle reference matching is _always_ active in runtime**,
60+
it's not an optional feature.
61+
62+
The gradle plugin defines two tasks:
63+
64+
* `muzzle` task runs the runtime muzzle verification against different library versions:
65+
```sh
66+
./gradlew :instrumentation:google-http-client-1.19:muzzle
67+
```
68+
If a new, incompatible version of the instrumented library is published it fails the build.
69+
70+
* `printReferences` task prints all API references in a given module:
71+
```sh
72+
./gradlew :instrumentation:google-http-client-1.19:printReferences
73+
```
3174

32-
The `muzzle` task downloads 3rd party libraries from maven central and checks API compatibility.
33-
If a new incompatible version is published it fails the build.
75+
The muzzle plugin needs to be configured in the module's `.gradle` file.
76+
Example:
3477
35-
```bash
36-
./gradlew :instrumentation:google-http-client-1.19:muzzle
78+
```groovy
79+
muzzle {
80+
// it is expected that muzzle fails the runtime check for this component
81+
fail {
82+
group = "commons-httpclient"
83+
module = "commons-httpclient"
84+
// versions from this range are checked
85+
versions = "[,4.0)"
86+
// this version is not checked by muzzle
87+
skipVersions += '3.1-jenkins-1'
88+
}
89+
// it is expected that muzzle passes the runtime check for this component
90+
pass {
91+
group = "org.apache.httpcomponents"
92+
module = "httpclient"
93+
versions = "[4.0,)"
94+
// verify that all other versions - [,4.0) in this case - fail the muzzle runtime check
95+
assertInverse = true
96+
}
97+
// ...
98+
}
3799
```
38100
39-
## Muzzle location
101+
* Using either `pass` or `fail` directive allows to specify whether muzzle should treat the
102+
reference check failure as expected behavior;
103+
* `versions` is a version range, where `[]` is inclusive and `()` is exclusive. It is not needed to
104+
specify the exact version to start/end, e.g. `[1.0.0,4)` would usually behave in the same way as
105+
`[1.0.0,4.0.0-Alpha)`;
106+
* `assertInverse` is basically a shortcut for adding an opposite directive for all library versions
107+
that are not included in the specified `versions` range.
40108
41-
* `buildSrc` - Muzzle Gradle plugin
42-
* `agent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/muzzle` - Muzzle ByteBuddy plugin
109+
The source code of the gradle plugin is located in the `buildSrc` directory.

gradle/instrumentation.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ byteBuddy {
1313
transformation {
1414
// Applying NoOp optimizes build by applying bytebuddy plugin to only compileJava task
1515
tasks = ['compileJava', 'compileScala', 'compileKotlin']
16-
plugin = 'io.opentelemetry.javaagent.tooling.muzzle.MuzzleGradlePlugin$NoOp'
16+
plugin = 'io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin$NoOp'
1717
}
1818
}
1919

@@ -32,7 +32,7 @@ afterEvaluate {
3232
byteBuddy {
3333
transformation {
3434
tasks = ['compileJava', 'compileScala', 'compileKotlin']
35-
plugin = 'io.opentelemetry.javaagent.tooling.muzzle.MuzzleGradlePlugin'
35+
plugin = 'io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin'
3636
classPath = project(':javaagent-tooling').configurations.instrumentationMuzzle + configurations.runtimeClasspath + sourceSets.main.output
3737
}
3838
}

instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/config/Config.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public abstract class Config {
2727
private static final Config DEFAULT = Config.create(Collections.emptyMap());
2828

2929
// INSTANCE can never be null - muzzle instantiates instrumenters when it generates
30-
// getInstrumentationMuzzle() and the Instrumenter.Default constructor uses Config
30+
// getMuzzleReferenceMatcher() and the Instrumenter.Default constructor uses Config
3131
private static volatile Config INSTANCE = DEFAULT;
3232

3333
/**

javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/Instrumenter.java

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
import io.opentelemetry.javaagent.tooling.context.FieldBackedProvider;
1818
import io.opentelemetry.javaagent.tooling.context.InstrumentationContextProvider;
1919
import io.opentelemetry.javaagent.tooling.context.NoopContextProvider;
20-
import io.opentelemetry.javaagent.tooling.muzzle.Reference;
21-
import io.opentelemetry.javaagent.tooling.muzzle.ReferenceMatcher;
20+
import io.opentelemetry.javaagent.tooling.muzzle.matcher.Mismatch;
21+
import io.opentelemetry.javaagent.tooling.muzzle.matcher.ReferenceMatcher;
2222
import java.security.ProtectionDomain;
2323
import java.util.Arrays;
2424
import java.util.Collections;
@@ -153,7 +153,11 @@ public int getOrder() {
153153
return 0;
154154
}
155155

156-
/** Matches classes for which instrumentation is not muzzled. */
156+
/**
157+
* A ByteBuddy matcher that decides whether this instrumentation should be applied. Calls
158+
* generated {@link ReferenceMatcher}: if any mismatch with the passed {@code classLoader} is
159+
* found this instrumentation is skipped.
160+
*/
157161
private class MuzzleMatcher implements AgentBuilder.RawMatcher {
158162
@Override
159163
public boolean matches(
@@ -162,49 +166,46 @@ public boolean matches(
162166
JavaModule module,
163167
Class<?> classBeingRedefined,
164168
ProtectionDomain protectionDomain) {
165-
/* Optimization: calling getInstrumentationMuzzle() inside this method
169+
/* Optimization: calling getMuzzleReferenceMatcher() inside this method
166170
* prevents unnecessary loading of muzzle references during agentBuilder
167171
* setup.
168172
*/
169-
ReferenceMatcher muzzle = getInstrumentationMuzzle();
170-
if (null != muzzle) {
173+
ReferenceMatcher muzzle = getMuzzleReferenceMatcher();
174+
if (muzzle != null) {
171175
boolean isMatch = muzzle.matches(classLoader);
172-
if (!isMatch) {
173-
if (log.isDebugEnabled()) {
174-
List<Reference.Mismatch> mismatches =
175-
muzzle.getMismatchedReferenceSources(classLoader);
176-
if (log.isDebugEnabled()) {
177-
log.debug(
178-
"Instrumentation muzzled: {} -- {} on {}",
179-
instrumentationNames,
180-
Instrumenter.Default.this.getClass().getName(),
181-
classLoader);
182-
}
183-
for (Reference.Mismatch mismatch : mismatches) {
176+
177+
if (log.isDebugEnabled()) {
178+
if (!isMatch) {
179+
log.debug(
180+
"Instrumentation skipped, mismatched references were found: {} -- {} on {}",
181+
instrumentationNames,
182+
Instrumenter.Default.this.getClass().getName(),
183+
classLoader);
184+
List<Mismatch> mismatches = muzzle.getMismatchedReferenceSources(classLoader);
185+
for (Mismatch mismatch : mismatches) {
184186
log.debug("-- {}", mismatch);
185187
}
186-
}
187-
} else {
188-
if (log.isDebugEnabled()) {
188+
} else {
189189
log.debug(
190190
"Applying instrumentation: {} -- {} on {}",
191191
instrumentationPrimaryName,
192192
Instrumenter.Default.this.getClass().getName(),
193193
classLoader);
194194
}
195195
}
196+
196197
return isMatch;
197198
}
198199
return true;
199200
}
200201
}
201202

202203
/**
203-
* This method is implemented dynamically by compile-time bytecode transformations.
204-
*
205-
* <p>{@see io.opentelemetry.javaagent.tooling.muzzle.MuzzleGradlePlugin}
204+
* The actual implementation of this method is generated automatically during compilation by the
205+
* {@link io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin}
206+
* ByteBuddy plugin.
206207
*/
207-
protected ReferenceMatcher getInstrumentationMuzzle() {
208+
protected ReferenceMatcher getMuzzleReferenceMatcher() {
208209
return null;
209210
}
210211

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,7 @@
55

66
package io.opentelemetry.javaagent.tooling.muzzle;
77

8-
/**
9-
* Defines a set of packages for which we'll create references.
10-
*
11-
* <p>For now we're hardcoding this to the instrumentation and javaagent-tooling packages so we only
12-
* create references from the method advice and helper classes.
13-
*/
14-
final class ReferenceCreationPredicate {
8+
public final class InstrumentationClassPredicate {
159
// non-shaded packages
1610
private static final String AUTO_INSTRUMENTATION_PACKAGE =
1711
"io.opentelemetry.javaagent.instrumentation.";
@@ -23,7 +17,16 @@ final class ReferenceCreationPredicate {
2317
private static final String LIBRARY_INSTRUMENTATION_PACKAGE = "io.opentelemetry.instrumentation.";
2418
private static final String INSTRUMENTATION_API_PACKAGE = "io.opentelemetry.instrumentation.api.";
2519

26-
static boolean shouldCreateReferenceFor(String className) {
20+
/**
21+
* Defines which classes are treated by muzzle as "internal", "helper" instrumentation classes.
22+
*
23+
* <p>This set of classes is defined by a package naming convention: all automatic and manual
24+
* instrumentation classes and {@code javaagent.tooling} classes are treated as "helper" classes
25+
* and are subjected to the reference collection process. All others (including {@code
26+
* instrumentation-api} and {@code javaagent-api} modules are not scanned for referenced (but
27+
* references to them are collected).
28+
*/
29+
public static boolean isInstrumentationClass(String className) {
2730
if (className.startsWith(INSTRUMENTATION_API_PACKAGE)
2831
|| className.startsWith(AUTO_INSTRUMENTATION_API_PACKAGE)) {
2932
return false;
@@ -33,5 +36,5 @@ static boolean shouldCreateReferenceFor(String className) {
3336
|| className.startsWith(LIBRARY_INSTRUMENTATION_PACKAGE);
3437
}
3538

36-
private ReferenceCreationPredicate() {}
39+
private InstrumentationClassPredicate() {}
3740
}

0 commit comments

Comments
 (0)