Skip to content

Commit db2fe99

Browse files
committed
hardening + docs
1 parent 6944f70 commit db2fe99

File tree

3 files changed

+386
-7
lines changed

3 files changed

+386
-7
lines changed

README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ If you work in a legacy app and ask things like "Can we remove this?" or "Is thi
1414
- [Java Version](#java-version)
1515
- [Build](#build)
1616
- [Configure](#configure)
17+
- [Class Include/Exclude Patterns](#class-includeexclude-patterns)
18+
- [Hard-Skipped Packages (Non-Overridable)](#hard-skipped-packages-non-overridable)
1719
- [Run an Application with JCT](#run-an-application-with-jct)
1820
- [Available Processors](#available-processors)
1921
- [Stack Volume Control (All vs New Stacks)](#stack-volume-control-all-vs-new-stacks)
@@ -128,6 +130,78 @@ Notes:
128130
- Default config is loaded from `src/main/resources/META-INF/config.yaml`
129131
- If `-Djct.config=...` is set, custom config is merged with default config
130132

133+
## Class Include/Exclude Patterns
134+
135+
JCT uses Java regex patterns to decide which classes are instrumented.
136+
137+
Config keys:
138+
139+
- `classes.included`: allow list (what JCT may instrument)
140+
- `classes.excluded`: deny list (what JCT must never instrument)
141+
142+
How matching works:
143+
144+
1. JCT normalizes class names to dot notation for regex matching (example: `de.marcelsauer.sample.ClassA`).
145+
2. `excluded` is checked first. If any exclude pattern matches, the class is skipped.
146+
3. If not excluded, `included` is checked. If any include pattern matches, the class is instrumented.
147+
4. If no include pattern matches, the class is skipped.
148+
149+
Pattern behavior:
150+
151+
- Patterns are standard Java regex (`String.matches`), so anchors like `^` and `$` are recommended.
152+
- Include patterns are ORed together (`match any include`).
153+
- Exclude patterns are ORed together (`match any exclude`).
154+
- Exclude wins over include when both match the same class.
155+
156+
Example:
157+
158+
```yaml
159+
classes:
160+
included:
161+
- ^de.marcelsauer.*
162+
- ^com.example.legacy.*
163+
excluded:
164+
- ^de.marcelsauer.generated.*
165+
- ^com.example.legacy.internal.*
166+
```
167+
168+
In this example:
169+
170+
- `de.marcelsauer.service.OrderService` -> instrumented (included, not excluded)
171+
- `de.marcelsauer.generated.DtoMapper` -> skipped (excluded wins)
172+
- `org.springframework.context.ApplicationContext` -> skipped (not included)
173+
174+
Practical tips:
175+
176+
- Start narrow (one business package), then widen once you trust the output volume.
177+
- Always exclude generated/proxy-heavy areas you do not need.
178+
- Keep regex explicit; broad patterns like `.*` can produce large event volumes.
179+
180+
## Hard-Skipped Packages (Non-Overridable)
181+
182+
JCT has a built-in safety list of package prefixes that are never instrumented, even if your include regex would match them.
183+
This prevents self-instrumentation and reduces crash risk in JVM/logging/bytecode internals.
184+
185+
Current hard-skipped prefixes (JVM slash notation):
186+
187+
- `de/marcelsauer/profiler/`
188+
- `java/`
189+
- `javax/`
190+
- `jdk/`
191+
- `sun/`
192+
- `com/sun/`
193+
- `org/slf4j/`
194+
- `ch/qos/logback/`
195+
- `org/apache/log4j/`
196+
- `org/apache/logging/log4j/`
197+
- `javassist/`
198+
- `net/bytebuddy/`
199+
- `org/objectweb/asm/`
200+
- `cglib/`
201+
- `org/springframework/cglib/`
202+
203+
Source of truth: `de.marcelsauer.profiler.transformer.Transformer` (`HARD_SKIPPED_PREFIXES`).
204+
131205
## Run an Application with JCT
132206

133207
```bash

src/main/java/de/marcelsauer/profiler/transformer/Transformer.java

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,145 @@
99
import java.lang.instrument.ClassFileTransformer;
1010
import java.lang.instrument.IllegalClassFormatException;
1111
import java.security.ProtectionDomain;
12+
import java.util.Set;
13+
import java.util.concurrent.ConcurrentHashMap;
1214

1315
/**
1416
* @author msauer
1517
*/
1618
public class Transformer implements ClassFileTransformer {
1719

1820
private static final Logger logger = Logger.getLogger(Transformer.class);
21+
private static final String[] HARD_SKIPPED_PREFIXES = new String[]{
22+
"de/marcelsauer/profiler/",
23+
"java/",
24+
"javax/",
25+
"jdk/",
26+
"sun/",
27+
"com/sun/",
28+
"org/slf4j/",
29+
"ch/qos/logback/",
30+
"org/apache/log4j/",
31+
"org/apache/logging/log4j/",
32+
"javassist/",
33+
"net/bytebuddy/",
34+
"org/objectweb/asm/",
35+
"cglib/",
36+
"org/springframework/cglib/"
37+
};
38+
private static final int DEFAULT_TRANSFORM_FAILURE_THRESHOLD = 50;
39+
private static final long DEFAULT_TRANSFORM_FAILURE_WINDOW_MILLIS = 30_000L;
40+
private static final long DEFAULT_TRANSFORM_CIRCUIT_OPEN_MILLIS = 60_000L;
41+
1942
private final CombinedFilter combinedFilter;
43+
private final Set<String> permanentlySkippedClasses = ConcurrentHashMap.newKeySet();
44+
private final Object transformCircuitLock = new Object();
45+
private final int transformFailureThreshold;
46+
private final long transformFailureWindowMillis;
47+
private final long transformCircuitOpenMillis;
48+
49+
private long transformFailureWindowStartMillis;
50+
private int transformFailuresInWindow;
51+
private long transformCircuitOpenUntilMillis;
2052

2153
Instrumenter instrumenter;
2254

2355
public Transformer(Config config) {
56+
this(config,
57+
DEFAULT_TRANSFORM_FAILURE_THRESHOLD,
58+
DEFAULT_TRANSFORM_FAILURE_WINDOW_MILLIS,
59+
DEFAULT_TRANSFORM_CIRCUIT_OPEN_MILLIS);
60+
}
61+
62+
Transformer(Config config, int transformFailureThreshold, long transformFailureWindowMillis, long transformCircuitOpenMillis) {
2463
this.instrumenter = new Instrumenter(new DefaultInstrumentationCallback());
2564
this.combinedFilter = new CombinedFilter(config);
65+
this.transformFailureThreshold = Math.max(1, transformFailureThreshold);
66+
this.transformFailureWindowMillis = Math.max(1_000L, transformFailureWindowMillis);
67+
this.transformCircuitOpenMillis = Math.max(1_000L, transformCircuitOpenMillis);
2668
logger.debug("using transformer: " + Transformer.class.getName());
2769
}
2870

2971
public byte[] transform(ClassLoader loader, String className, Class klass, ProtectionDomain domain, byte[] byteCode) throws IllegalClassFormatException {
3072
try {
31-
if (loader == null) {
73+
if (loader == null || className == null) {
3274
logger.debug(String.format("[skipping bootstrap class] '%s'.", className));
3375
return byteCode;
3476
}
3577

78+
if (isHardSkipped(className) || permanentlySkippedClasses.contains(className)) {
79+
return byteCode;
80+
}
81+
82+
if (isTransformCircuitOpen()) {
83+
return byteCode;
84+
}
85+
3686
boolean shouldBeInstrumented = combinedFilter.matches(className);
37-
if (shouldBeInstrumented) {
38-
logger.debug(String.format("[instrumenting] '%s'.", className));
39-
byte[] transformed = this.instrumenter.instrument(className, loader);
40-
// null means "skip transformation" per ClassFileTransformer contract
41-
return transformed != null ? transformed : byteCode;
87+
if (!shouldBeInstrumented) {
88+
return byteCode;
4289
}
43-
return byteCode;
90+
91+
logger.debug(String.format("[instrumenting] '%s'.", className));
92+
byte[] transformed = this.instrumenter.instrument(className, loader);
93+
// null means "skip transformation" per ClassFileTransformer contract
94+
return transformed != null ? transformed : byteCode;
4495
} catch (Throwable t) {
96+
if (className != null) {
97+
permanentlySkippedClasses.add(className);
98+
}
99+
registerTransformFailure(className, t);
45100
// Never let any exception escape the transformer — the JVM JPLIS layer
46101
// turns uncaught throwables into the ugly "!errorOutstanding" native assertion.
47102
logger.warn(String.format("transform failed for '%s', returning original bytecode: %s", className, t.getMessage()));
48103
return byteCode;
49104
}
50105
}
51106

107+
private boolean isHardSkipped(String className) {
108+
for (String hardSkippedPrefix : HARD_SKIPPED_PREFIXES) {
109+
if (className.startsWith(hardSkippedPrefix)) {
110+
return true;
111+
}
112+
}
113+
return false;
114+
}
115+
116+
protected long currentTimeMillis() {
117+
return System.currentTimeMillis();
118+
}
119+
120+
private boolean isTransformCircuitOpen() {
121+
synchronized (transformCircuitLock) {
122+
return currentTimeMillis() < transformCircuitOpenUntilMillis;
123+
}
124+
}
125+
126+
private void registerTransformFailure(String className, Throwable throwable) {
127+
synchronized (transformCircuitLock) {
128+
long now = currentTimeMillis();
129+
130+
if (transformFailureWindowStartMillis == 0L
131+
|| (now - transformFailureWindowStartMillis) > transformFailureWindowMillis) {
132+
transformFailureWindowStartMillis = now;
133+
transformFailuresInWindow = 0;
134+
}
135+
136+
transformFailuresInWindow++;
137+
138+
if (transformFailuresInWindow >= transformFailureThreshold) {
139+
transformCircuitOpenUntilMillis = now + transformCircuitOpenMillis;
140+
logger.warn(String.format(
141+
"transform circuit opened for %dms after %d failures in %dms window. latest class='%s', cause='%s'",
142+
transformCircuitOpenMillis,
143+
transformFailuresInWindow,
144+
transformFailureWindowMillis,
145+
className,
146+
throwable == null ? "n/a" : throwable.getClass().getSimpleName()));
147+
transformFailureWindowStartMillis = now;
148+
transformFailuresInWindow = 0;
149+
}
150+
}
151+
}
152+
52153
}

0 commit comments

Comments
 (0)