Skip to content

Commit 6d8ac90

Browse files
authored
Code review agent: Establish AdviceScope pattern (open-telemetry#17419)
1 parent 7e5fdff commit 6d8ac90

10 files changed

Lines changed: 190 additions & 74 deletions

File tree

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,95 @@ Because `none()` matches no methods, ByteBuddy never inlines this advice into an
172172
`suppress = Throwable.class` on such a method is entirely meaningless — **do not add it**,
173173
and **do not flag its absence** during review.
174174

175+
## AdviceScope Patterns
176+
177+
`AdviceScope` usage in this repository falls into **two justified state patterns**.
178+
Review new code against these patterns instead of treating every existing variation as equally
179+
canonical.
180+
181+
### Pattern 1 — Nullable `AdviceScope` for ordinary advice
182+
183+
Use this by default when enter advice may decide not to start instrumentation.
184+
185+
- `@Advice.OnMethodEnter` returns `@Nullable AdviceScope`
186+
- The factory method returns `null` when preconditions fail or instrumentation should not start
187+
- If an `AdviceScope` is created, its `Context` and `Scope` fields should be non-null
188+
- The `end()` method should close `scope` unconditionally; the null check belongs in exit advice,
189+
not inside `AdviceScope.end()`
190+
191+
In this document, use `start()` / `end()` as the canonical `AdviceScope` naming.
192+
For new code, prefer `start()` for the factory method and `end()` for the completion method.
193+
Do not introduce one-off names such as `create()` for ordinary `AdviceScope` factories.
194+
195+
Preferred shape:
196+
197+
```java
198+
public static class AdviceScope {
199+
private final Context context;
200+
private final Scope scope;
201+
202+
private AdviceScope(Context context) {
203+
this.context = context;
204+
this.scope = context.makeCurrent();
205+
}
206+
207+
@Nullable
208+
public static AdviceScope start(Request request) {
209+
Context parentContext = Context.current();
210+
if (!instrumenter().shouldStart(parentContext, request)) {
211+
return null;
212+
}
213+
Context context = instrumenter().start(parentContext, request);
214+
return new AdviceScope(context);
215+
}
216+
217+
public void end(@Nullable Throwable throwable) {
218+
scope.close();
219+
instrumenter().end(context, request, null, throwable);
220+
}
221+
}
222+
223+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
224+
public static void onExit(
225+
@Advice.Thrown @Nullable Throwable throwable,
226+
@Advice.Enter @Nullable AdviceScope adviceScope) {
227+
if (adviceScope != null) {
228+
adviceScope.end(throwable);
229+
}
230+
}
231+
```
232+
233+
### Pattern 2 — Non-null placeholder `AdviceScope` for bookkeeping
234+
235+
Use a non-null `AdviceScope` with nullable internals only when exit advice still needs state even
236+
if no tracing scope was started, for example:
237+
238+
- `CallDepth` tracking
239+
- wrapped arguments or listeners that must be carried from enter to exit
240+
- other bookkeeping that must survive even on the "no span started" path
241+
242+
In this pattern, `AdviceScope` is the carrier object for control-flow state, not just tracing
243+
state. Nullable inner fields and exit guards are justified here.
244+
245+
### Do not use the defensive hybrid as a standard pattern
246+
247+
Avoid the hybrid shape where:
248+
249+
- `@Advice.Enter` is `@Nullable AdviceScope`
250+
- `AdviceScope` still stores a nullable `Scope`
251+
- `AdviceScope.end()` has an extra `if (scope == null)` guard
252+
253+
That double-guards the same condition and makes simple advice harder to reason about.
254+
If the helper that creates the context can return `null`, prefer returning `null` from
255+
`AdviceScope.start()` and only creating `AdviceScope` when the context is present.
256+
257+
### Async completion is not a separate `AdviceScope` state pattern
258+
259+
Async instrumentations may end spans from listeners, callbacks, or wrapped handlers instead of
260+
directly in method exit. That affects **where** the scope/span is completed, but it does not
261+
justify a third core `AdviceScope` state model. Async advice should still use either Pattern 1 or
262+
Pattern 2 as appropriate.
263+
175264
## Never Throw Exceptions in Javaagent Code
176265

177266
Javaagent instrumentations must never throw exceptions. The goal is to be invisible to the
@@ -193,3 +282,5 @@ the instrumentation automatically rather than letting it fail at runtime.
193282
- **`@Advice.OnMethodExit` method named `onEnter`** (or vice versa) — the method name should match the annotation. A mismatch is a copy-paste bug that compiles but confuses readers and may mask intent errors.
194283
- **Advice referenced in `transform()` using anything other than `getClass().getName() + "$InnerAdvice"`** — see `javaagent-module-patterns.md` for the canonical pattern. Flag `this.getClass().getName() + "$InnerAdvice"` as a redundant qualifier, and flag both `InnerAdvice.class.getName()` and `OuterInstrumentation.class.getName() + "$InnerAdvice"` because any `.class` literal in a `transform()` method triggers unwanted class loading.
195284
- **`onThrowable = Throwable.class` on return-only exit advice** — if the exit method only processes `@Advice.Return` and has no `@Advice.Enter` state to clean up, `onThrowable` should be omitted. The return value is `null`/zero on the exceptional path, and dereferencing it causes a suppressed exception for no benefit. Keep `suppress = Throwable.class` but remove `onThrowable`.
285+
- **One-off `AdviceScope` method naming** — for new ordinary advice, prefer `start()` / `end()` for `AdviceScope` methods, and avoid introducing unique names such as `create()`.
286+
- **Defensive hybrid `AdviceScope` in simple advice** — if `@Advice.Enter` is already nullable, flag simple advice that also stores a nullable inner `Scope`/`Context` and re-checks it inside `AdviceScope.end()`. Prefer returning `null` from the factory and keeping the created `AdviceScope` fully initialized.

instrumentation/influxdb-2.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/influxdb/v2_4/InfluxDbScope.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ public static InfluxDbScope start(Context parentContext, InfluxDbRequest influxD
2828
}
2929

3030
public void end(Throwable throwable) {
31-
if (scope == null) {
32-
return;
33-
}
34-
3531
scope.close();
3632

3733
instrumenter().end(context, influxDbRequest, null, throwable);

instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/DefaultRequestContextInstrumentation.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ private AdviceScope(Context context, Scope scope, Jaxrs2HandlerData handlerData)
4848
}
4949

5050
@Nullable
51-
public static AdviceScope enter(Class<?> filterClass, Method method) {
51+
public static AdviceScope start(Class<?> filterClass, Method method) {
5252

5353
Context parentContext = Context.current();
5454
Jaxrs2HandlerData handlerData = new Jaxrs2HandlerData(filterClass, method);
@@ -66,10 +66,7 @@ public static AdviceScope enter(Class<?> filterClass, Method method) {
6666
return new AdviceScope(context, context.makeCurrent(), handlerData);
6767
}
6868

69-
public void exit(Throwable throwable) {
70-
if (scope == null) {
71-
return;
72-
}
69+
public void end(@Nullable Throwable throwable) {
7370
scope.close();
7471
instrumenter().end(context, handlerData, null, throwable);
7572
}
@@ -101,7 +98,7 @@ public static AdviceScope createGenericSpan(
10198
if (method == null) {
10299
return null;
103100
}
104-
return AdviceScope.enter(filterClass, method);
101+
return AdviceScope.start(filterClass, method);
105102
}
106103

107104
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
@@ -110,7 +107,7 @@ public static void stopSpan(
110107
@Advice.Enter @Nullable AdviceScope adviceScope) {
111108

112109
if (adviceScope != null) {
113-
adviceScope.exit(throwable);
110+
adviceScope.end(throwable);
114111
}
115112
}
116113
}

instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-cxf-3.2/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/CxfRequestContextInstrumentation.java

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,26 @@ public static class AdviceScope {
6060
private final Context context;
6161
private final Scope scope;
6262

63-
public AdviceScope(
63+
private AdviceScope(Jaxrs2HandlerData handlerData, Context context) {
64+
this.handlerData = handlerData;
65+
this.context = context;
66+
scope = context.makeCurrent();
67+
}
68+
69+
@Nullable
70+
public static AdviceScope start(
6471
Class<?> resourceClass, Method method, AbstractRequestContextImpl requestContext) {
65-
handlerData = new Jaxrs2HandlerData(resourceClass, method);
66-
context =
72+
Jaxrs2HandlerData handlerData = new Jaxrs2HandlerData(resourceClass, method);
73+
Context context =
6774
Jaxrs2RequestContextHelper.createOrUpdateAbortSpan(
6875
instrumenter(), (ContainerRequestContext) requestContext, handlerData);
69-
scope = context != null ? context.makeCurrent() : null;
76+
if (context == null) {
77+
return null;
78+
}
79+
return new AdviceScope(handlerData, context);
7080
}
7181

72-
public void exit(@Nullable Throwable throwable) {
73-
if (scope == null) {
74-
return;
75-
}
82+
public void end(@Nullable Throwable throwable) {
7683
scope.close();
7784
instrumenter().end(context, handlerData, null, throwable);
7885
}
@@ -99,14 +106,15 @@ public static AdviceScope decorateAbortSpan(
99106
MethodInvocationInfo invocationInfo = resourceInfoStack.peek();
100107
Method method = invocationInfo.getMethodInfo().getMethodToInvoke();
101108
Class<?> resourceClass = invocationInfo.getRealClass();
102-
return new AdviceScope(resourceClass, method, requestContext);
109+
return AdviceScope.start(resourceClass, method, requestContext);
103110
}
104111

105112
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
106113
public static void stopSpan(
107-
@Advice.Thrown Throwable throwable, @Advice.Enter @Nullable AdviceScope adviceScope) {
114+
@Advice.Thrown @Nullable Throwable throwable,
115+
@Advice.Enter @Nullable AdviceScope adviceScope) {
108116
if (adviceScope != null) {
109-
adviceScope.exit(throwable);
117+
adviceScope.end(throwable);
110118
}
111119
}
112120
}

instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy30RequestContextInstrumentation.java

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,26 @@ public static class AdviceScope {
4141
private final Context context;
4242
private final Scope scope;
4343

44-
public AdviceScope(
44+
private AdviceScope(Jaxrs2HandlerData handlerData, Context context) {
45+
this.handlerData = handlerData;
46+
this.context = context;
47+
scope = context.makeCurrent();
48+
}
49+
50+
@Nullable
51+
public static AdviceScope start(
4552
Class<?> resourceClass, Method method, ContainerRequestContext requestContext) {
46-
handlerData = new Jaxrs2HandlerData(resourceClass, method);
47-
context =
53+
Jaxrs2HandlerData handlerData = new Jaxrs2HandlerData(resourceClass, method);
54+
Context context =
4855
Jaxrs2RequestContextHelper.createOrUpdateAbortSpan(
4956
instrumenter(), requestContext, handlerData);
50-
scope = context != null ? context.makeCurrent() : null;
57+
if (context == null) {
58+
return null;
59+
}
60+
return new AdviceScope(handlerData, context);
5161
}
5262

53-
public void exit(Throwable throwable) {
54-
if (scope == null) {
55-
return;
56-
}
63+
public void end(@Nullable Throwable throwable) {
5764
scope.close();
5865
instrumenter().end(context, handlerData, null, throwable);
5966
}
@@ -74,15 +81,15 @@ public static AdviceScope decorateAbortSpan(
7481
Method method = resourceMethodInvoker.getMethod();
7582
Class<?> resourceClass = resourceMethodInvoker.getResourceClass();
7683

77-
return new AdviceScope(resourceClass, method, requestContext);
84+
return AdviceScope.start(resourceClass, method, requestContext);
7885
}
7986

8087
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
8188
public static void stopSpan(
8289
@Advice.Thrown @Nullable Throwable throwable,
8390
@Advice.Enter @Nullable AdviceScope adviceScope) {
8491
if (adviceScope != null) {
85-
adviceScope.exit(throwable);
92+
adviceScope.end(throwable);
8693
}
8794
}
8895
}

instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v2_0/Resteasy31RequestContextInstrumentation.java

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,26 @@ public static class AdviceScope {
4141
private final Context context;
4242
private final Scope scope;
4343

44-
public AdviceScope(
44+
private AdviceScope(Jaxrs2HandlerData handlerData, Context context) {
45+
this.handlerData = handlerData;
46+
this.context = context;
47+
scope = context.makeCurrent();
48+
}
49+
50+
@Nullable
51+
public static AdviceScope start(
4552
Class<?> resourceClass, Method method, ContainerRequestContext requestContext) {
46-
handlerData = new Jaxrs2HandlerData(resourceClass, method);
47-
context =
53+
Jaxrs2HandlerData handlerData = new Jaxrs2HandlerData(resourceClass, method);
54+
Context context =
4855
Jaxrs2RequestContextHelper.createOrUpdateAbortSpan(
4956
instrumenter(), requestContext, handlerData);
50-
51-
scope = context != null ? context.makeCurrent() : null;
57+
if (context == null) {
58+
return null;
59+
}
60+
return new AdviceScope(handlerData, context);
5261
}
5362

54-
public void exit(@Nullable Throwable throwable) {
55-
if (scope == null) {
56-
return;
57-
}
63+
public void end(@Nullable Throwable throwable) {
5864
scope.close();
5965
instrumenter().end(context, handlerData, null, throwable);
6066
}
@@ -75,15 +81,15 @@ public static AdviceScope decorateAbortSpan(
7581
Method method = resourceMethodInvoker.getMethod();
7682
Class<?> resourceClass = resourceMethodInvoker.getResourceClass();
7783

78-
return new AdviceScope(resourceClass, method, requestContext);
84+
return AdviceScope.start(resourceClass, method, requestContext);
7985
}
8086

8187
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
8288
public static void stopSpan(
8389
@Advice.Thrown @Nullable Throwable throwable,
8490
@Advice.Enter @Nullable AdviceScope adviceScope) {
8591
if (adviceScope != null) {
86-
adviceScope.exit(throwable);
92+
adviceScope.end(throwable);
8793
}
8894
}
8995
}

instrumentation/jaxrs/jaxrs-3.0/jaxrs-3.0-annotations/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jaxrs/v3_0/DefaultRequestContextInstrumentation.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ private AdviceScope(Context context, Scope scope, Jaxrs3HandlerData handlerData)
4848
}
4949

5050
@Nullable
51-
public static AdviceScope enter(
52-
Class<?> filterClass, Method method, ContainerRequestContext requestContext) {
51+
public static AdviceScope start(Class<?> filterClass, Method method) {
5352
Context parentContext = Context.current();
5453
Jaxrs3HandlerData handlerData = new Jaxrs3HandlerData(filterClass, method);
5554

@@ -67,7 +66,7 @@ public static AdviceScope enter(
6766
return new AdviceScope(context, context.makeCurrent(), handlerData);
6867
}
6968

70-
public void exit(@Nullable Throwable throwable) {
69+
public void end(@Nullable Throwable throwable) {
7170
scope.close();
7271
instrumenter().end(context, handlerData, null, throwable);
7372
}
@@ -97,15 +96,16 @@ public static AdviceScope createGenericSpan(
9796
return null;
9897
}
9998

100-
return AdviceScope.enter(filterClass, method, requestContext);
99+
return AdviceScope.start(filterClass, method);
101100
}
102101

103102
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
104103
public static void stopSpan(
105-
@Advice.Thrown Throwable throwable, @Advice.Enter @Nullable AdviceScope adviceScope) {
104+
@Advice.Thrown @Nullable Throwable throwable,
105+
@Advice.Enter @Nullable AdviceScope adviceScope) {
106106

107107
if (adviceScope != null) {
108-
adviceScope.exit(throwable);
108+
adviceScope.end(throwable);
109109
}
110110
}
111111
}

0 commit comments

Comments
 (0)