Skip to content

Commit ddff7fe

Browse files
authored
Add ability to set span status through nocode expressions (#2231)
* Initial work on setting span status with returnValue/error variables * Add some barebones documentation * spotlessApply (runs on .md too, huh) * Tweak example in docs to be a tiny bit more realistic * Log an edge case and document that this is not stable yet * spotlessApply
1 parent 14e7759 commit ddff7fe

File tree

11 files changed

+185
-14
lines changed

11 files changed

+185
-14
lines changed

bootstrap/src/main/java/com/splunk/opentelemetry/javaagent/bootstrap/nocode/NocodeEvaluation.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ public class NocodeEvaluation {
2222

2323
public interface Evaluator {
2424
Object evaluate(String expression, Object thiz, Object[] params);
25+
26+
Object evaluateAtEnd(
27+
String expression, Object thiz, Object[] params, Object returnValue, Throwable error);
2528
}
2629

2730
private static final AtomicReference<Evaluator> globalEvaluator = new AtomicReference<>();
@@ -35,5 +38,11 @@ public static Object evaluate(String expression, Object thiz, Object[] params) {
3538
return e == null ? null : e.evaluate(expression, thiz, params);
3639
}
3740

41+
public static Object evaluateAtEnd(
42+
String expression, Object thiz, Object[] params, Object returnValue, Throwable error) {
43+
Evaluator e = globalEvaluator.get();
44+
return e == null ? null : e.evaluateAtEnd(expression, thiz, params, returnValue, error);
45+
}
46+
3847
private NocodeEvaluation() {}
3948
}

bootstrap/src/main/java/com/splunk/opentelemetry/javaagent/bootstrap/nocode/NocodeRules.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,22 @@ public static final class Rule {
2828
public final String methodName;
2929
public final String spanName; // may be null - use default of "class.method"
3030
public final String spanKind; // matches the SpanKind enum, null means default to INTERNAL
31+
public final String spanStatus; // may be null, should return string from StatusCodes
32+
3133
public final Map<String, String> attributes; // key name to jexl expression
3234

3335
public Rule(
3436
String className,
3537
String methodName,
3638
String spanName,
3739
String spanKind,
40+
String spanStatus,
3841
Map<String, String> attributes) {
3942
this.className = className;
4043
this.methodName = methodName;
4144
this.spanName = spanName;
4245
this.spanKind = spanKind;
46+
this.spanStatus = spanStatus;
4347
this.attributes = Collections.unmodifiableMap(new HashMap<>(attributes));
4448
}
4549

@@ -50,6 +54,10 @@ public String toString() {
5054
+ methodName
5155
+ ":spanName="
5256
+ spanName
57+
+ ":spanKind="
58+
+ spanKind
59+
+ ":spanStatus="
60+
+ spanStatus
5361
+ ",attrs="
5462
+ attributes;
5563
}

instrumentation/nocode/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# "nocode" instrumentation
2+
3+
Stability: under active development; breaking changes can occur
4+
5+
Please don't use this if you have the ability to edit the code being instrumented.
6+
7+
Set `SPLUNK_OTEL_INSTRUMENTATION_NOCODE_YML_FILE=/path/to/some.yml`
8+
9+
Where the yml looks like
10+
```
11+
- class: foo.Foo
12+
method: foo
13+
spanName: this.getName()
14+
attributes:
15+
- key: "business.context"
16+
value: this.getDetails().get("context")
17+
18+
- class: foo.Foo
19+
method: doStuff
20+
spanKind: CLIENT
21+
spanStatus: 'returnValue.code() > 3 ? "OK" : "ERROR"`
22+
attributes:
23+
- key: "special.header"
24+
value: 'param0.headers().get("special-header").substring(5)"'
25+
```
26+
27+
Expressions are written in [JEXL](https://commons.apache.org/proper/commons-jexl/reference/syntax.html) and may use
28+
the following variables:
29+
- `this` - which may be null for a static method
30+
- `param0` through `paramN` where 0 indexes the first parameter to the method
31+
- `returnValue` which is only defined for `spanStatus` and may be null (if an exception is thrown or the method returns void)
32+
- `error` which is only defined for `spanStatus` and is the `Throwable` thrown by the method invocation (or null if a normal return)

instrumentation/nocode/src/main/java/com/splunk/opentelemetry/instrumentation/nocode/JexlEvaluator.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,14 @@ public JexlEvaluator() {
5252
.create();
5353
}
5454

55-
@Override
56-
public Object evaluate(String expression, Object thiz, Object[] params) {
57-
JexlContext context = new MapContext();
55+
private static void setBeginningVariables(JexlContext context, Object thiz, Object[] params) {
5856
context.set("this", thiz);
5957
for (int i = 0; i < params.length; i++) {
6058
context.set("param" + i, params[i]);
6159
}
60+
}
61+
62+
private Object evaluateExpression(String expression, JexlContext context) {
6263
try {
6364
// could cache the Expression in the Rule if desired
6465
return jexl.createExpression(expression).evaluate(context);
@@ -67,4 +68,25 @@ public Object evaluate(String expression, Object thiz, Object[] params) {
6768
return null;
6869
}
6970
}
71+
72+
private void setEndingVariables(JexlContext context, Object returnValue, Throwable error) {
73+
context.set("returnValue", returnValue);
74+
context.set("error", error);
75+
}
76+
77+
@Override
78+
public Object evaluate(String expression, Object thiz, Object[] params) {
79+
JexlContext context = new MapContext();
80+
setBeginningVariables(context, thiz, params);
81+
return evaluateExpression(expression, context);
82+
}
83+
84+
@Override
85+
public Object evaluateAtEnd(
86+
String expression, Object thiz, Object[] params, Object returnValue, Throwable error) {
87+
JexlContext context = new MapContext();
88+
setBeginningVariables(context, thiz, params);
89+
setEndingVariables(context, returnValue, error);
90+
return evaluateExpression(expression, context);
91+
}
7092
}

instrumentation/nocode/src/main/java/com/splunk/opentelemetry/instrumentation/nocode/NocodeAttributesExtractor.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
import javax.annotation.Nullable;
2727

2828
public final class NocodeAttributesExtractor
29-
implements AttributesExtractor<NocodeMethodInvocation, Void> {
30-
private final AttributesExtractor<ClassAndMethod, Void> codeExtractor;
29+
implements AttributesExtractor<NocodeMethodInvocation, Object> {
30+
private final AttributesExtractor<ClassAndMethod, Object> codeExtractor;
3131

3232
public NocodeAttributesExtractor() {
3333
codeExtractor = CodeAttributesExtractor.create(ClassAndMethod.codeAttributesGetter());
@@ -62,7 +62,7 @@ public void onEnd(
6262
AttributesBuilder attributesBuilder,
6363
Context context,
6464
NocodeMethodInvocation nocodeMethodInvocation,
65-
@Nullable Void unused,
65+
@Nullable Object unused,
6666
@Nullable Throwable throwable) {
6767
codeExtractor.onEnd(
6868
attributesBuilder, context, nocodeMethodInvocation.getClassAndMethod(), unused, throwable);

instrumentation/nocode/src/main/java/com/splunk/opentelemetry/instrumentation/nocode/NocodeInstrumentation.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.lang.reflect.Method;
3131
import net.bytebuddy.asm.Advice;
3232
import net.bytebuddy.description.type.TypeDescription;
33+
import net.bytebuddy.implementation.bytecode.assign.Assigner;
3334
import net.bytebuddy.matcher.ElementMatcher;
3435

3536
public final class NocodeInstrumentation implements TypeInstrumentation {
@@ -85,6 +86,7 @@ public static void stopSpan(
8586
@Advice.Local("otelInvocation") NocodeMethodInvocation otelInvocation,
8687
@Advice.Local("otelContext") Context context,
8788
@Advice.Local("otelScope") Scope scope,
89+
@Advice.Return(typing = Assigner.Typing.DYNAMIC) Object returnValue,
8890
@Advice.Thrown Throwable error) {
8991
if (scope == null) {
9092
return;
@@ -93,7 +95,7 @@ public static void stopSpan(
9395
// This is heavily based on the "methods" instrumentation from upstream, but
9496
// for now we're not supporting modifying return types/async result codes, etc.
9597
// This could be expanded in the future.
96-
instrumenter().end(context, otelInvocation, null, error);
98+
instrumenter().end(context, otelInvocation, returnValue, error);
9799
}
98100
}
99101
}

instrumentation/nocode/src/main/java/com/splunk/opentelemetry/instrumentation/nocode/NocodeSingletons.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,18 @@
2020
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
2121

2222
public class NocodeSingletons {
23-
private static final Instrumenter<NocodeMethodInvocation, Void> INSTRUMENTER;
23+
private static final Instrumenter<NocodeMethodInvocation, Object> INSTRUMENTER;
2424

2525
static {
2626
INSTRUMENTER =
27-
Instrumenter.<NocodeMethodInvocation, Void>builder(
27+
Instrumenter.<NocodeMethodInvocation, Object>builder(
2828
GlobalOpenTelemetry.get(), "com.splunk.nocode", new NocodeSpanNameExtractor())
2929
.addAttributesExtractor(new NocodeAttributesExtractor())
30+
.setSpanStatusExtractor(new NocodeSpanStatusExtractor())
3031
.buildInstrumenter(new NocodeSpanKindExtractor());
3132
}
3233

33-
public static Instrumenter<NocodeMethodInvocation, Void> instrumenter() {
34+
public static Instrumenter<NocodeMethodInvocation, Object> instrumenter() {
3435
return INSTRUMENTER;
3536
}
3637
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright Splunk Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.splunk.opentelemetry.instrumentation.nocode;
18+
19+
import com.splunk.opentelemetry.javaagent.bootstrap.nocode.NocodeEvaluation;
20+
import io.opentelemetry.api.trace.StatusCode;
21+
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusBuilder;
22+
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
23+
import java.util.Locale;
24+
import java.util.logging.Logger;
25+
import javax.annotation.Nullable;
26+
27+
public class NocodeSpanStatusExtractor
28+
implements SpanStatusExtractor<NocodeMethodInvocation, Object> {
29+
private static final Logger logger = Logger.getLogger(NocodeSpanStatusExtractor.class.getName());
30+
31+
@Override
32+
public void extract(
33+
SpanStatusBuilder spanStatusBuilder,
34+
NocodeMethodInvocation mi,
35+
@Nullable Object returnValue,
36+
@Nullable Throwable error) {
37+
38+
if (mi.getRule() == null || mi.getRule().spanStatus == null) {
39+
40+
// FIXME would love to use a DefaultSpanStatusExtractor as a fallback but it is not public
41+
// so here is a copy of its (admittedly simple) guts
42+
if (error != null) {
43+
spanStatusBuilder.setStatus(StatusCode.ERROR);
44+
}
45+
return;
46+
}
47+
Object status =
48+
NocodeEvaluation.evaluateAtEnd(
49+
mi.getRule().spanStatus, mi.getThiz(), mi.getParameters(), returnValue, error);
50+
if (status != null) {
51+
try {
52+
StatusCode code = StatusCode.valueOf(status.toString().toUpperCase(Locale.ROOT));
53+
spanStatusBuilder.setStatus(code);
54+
} catch (IllegalArgumentException noMatchingValue) {
55+
// nop, should remain UNSET
56+
logger.fine("Invalid span status ignored: " + status);
57+
}
58+
}
59+
}
60+
}

instrumentation/nocode/src/main/java/com/splunk/opentelemetry/instrumentation/nocode/YamlParser.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,19 @@ private static List<NocodeRules.Rule> loadUnsafe(String yamlFileName) throws Exc
7575
yamlRule.get("spanName") == null ? null : yamlRule.get("spanName").toString();
7676
String spanKind =
7777
yamlRule.get("spanKind") == null ? null : yamlRule.get("spanKind").toString();
78-
List<Map<String, Object>> attrs = (List<Map<String, Object>>) yamlRule.get("attributes");
78+
String spanStatus =
79+
yamlRule.get("spanStatus") == null ? null : yamlRule.get("spanStatus").toString();
80+
7981
Map<String, String> ruleAttributes = new HashMap<>();
80-
for (Map<String, Object> attr : attrs) {
81-
ruleAttributes.put(attr.get("key").toString(), attr.get("value").toString());
82+
List<Map<String, Object>> attrs = (List<Map<String, Object>>) yamlRule.get("attributes");
83+
if (attrs != null) {
84+
for (Map<String, Object> attr : attrs) {
85+
ruleAttributes.put(attr.get("key").toString(), attr.get("value").toString());
86+
}
8287
}
8388
answer.add(
84-
new NocodeRules.Rule(className, methodName, spanName, spanKind, ruleAttributes));
89+
new NocodeRules.Rule(
90+
className, methodName, spanName, spanKind, spanStatus, ruleAttributes));
8591
}
8692
}
8793
}

instrumentation/nocode/src/test/config/nocode.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,16 @@
2626
- key: "five"
2727
value: param0.toString().substring(0)
2828

29+
- class: com.splunk.opentelemetry.instrumentation.nocode.NocodeInstrumentationTest$SampleClass
30+
method: echo
31+
spanStatus: 'returnValue.booleanValue() ? "ERROR" : "OK"'
32+
33+
2934
- class: com.splunk.opentelemetry.instrumentation.nocode.NocodeInstrumentationTest$SampleClass
3035
method: doInvalidRule
3136
spanName: "this.thereIsNoSuchMethod()"
3237
spanKind: INVALID
38+
spanStatus: invalid jexl that does not parse
3339
attributes:
3440
- key: "notpresent"
3541
value: "invalid.noSuchStatement()"

instrumentation/nocode/src/test/java/com/splunk/opentelemetry/instrumentation/nocode/NocodeInstrumentationTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,27 @@ void testThrowException() {
8888
.hasAttributesSatisfying(equalTo(AttributeKey.stringKey("five"), "5"))));
8989
}
9090

91+
@Test
92+
void testEchoTrueIsError() {
93+
new SampleClass().echo(true);
94+
testing.waitAndAssertTraces(
95+
trace ->
96+
trace.hasSpansSatisfyingExactly(
97+
span ->
98+
span.hasName("SampleClass.echo")
99+
.hasStatusSatisfying(StatusDataAssert::isError)));
100+
}
101+
102+
@Test
103+
void testEchoFalseIsOK() {
104+
new SampleClass().echo(false);
105+
testing.waitAndAssertTraces(
106+
trace ->
107+
trace.hasSpansSatisfyingExactly(
108+
span ->
109+
span.hasName("SampleClass.echo").hasStatusSatisfying(StatusDataAssert::isOk)));
110+
}
111+
91112
public static class SampleClass {
92113
public String getName() {
93114
return "name";
@@ -131,5 +152,9 @@ public void throwException(int parameter) {
131152
public void doSomething() {}
132153

133154
public void doInvalidRule() {}
155+
156+
public boolean echo(boolean b) {
157+
return b;
158+
}
134159
}
135160
}

0 commit comments

Comments
 (0)