diff --git a/.fossa.yml b/.fossa.yml index fb2c373fe00e..547d290bf513 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -196,6 +196,12 @@ targets: - type: gradle path: ./ target: ':instrumentation:mybatis-3.2:javaagent' + - type: gradle + path: ./ + target: ':instrumentation:nocode:bootstrap' + - type: gradle + path: ./ + target: ':instrumentation:nocode:javaagent' - type: gradle path: ./ target: ':instrumentation:opentelemetry-extension-annotations-1.0:javaagent' diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index c7352a1ca8b7..8a6eee17356d 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -119,7 +119,8 @@ val DEPENDENCIES = listOf( "org.objenesis:objenesis:3.4", "javax.validation:validation-api:2.0.1.Final", "org.snakeyaml:snakeyaml-engine:2.9", - "org.elasticmq:elasticmq-rest-sqs_2.13:1.6.12" + "org.elasticmq:elasticmq-rest-sqs_2.13:1.6.12", + "org.apache.commons:commons-jexl3:3.4.0" ) javaPlatform { diff --git a/instrumentation/nocode/README.md b/instrumentation/nocode/README.md new file mode 100644 index 000000000000..86edd0347065 --- /dev/null +++ b/instrumentation/nocode/README.md @@ -0,0 +1,47 @@ +# "nocode" instrumentation + +Sometimes, you need to apply custom instrumentation to code you don't control/can't edit +(e.g., for a third-party app). This module provides a way to do that, controlling many +behaviors of instrumentation available through the trace api. + +# Usage + +Set `OTEL_JAVA_INSTRUMENTATION_NOCODE_YML_FILE=/path/to/your.yml`, where the yml describes +what methods you want to instrument and how: + +``` +- class: myapp.BusinessObject + method: update + spanName: this.getName() + attributes: + - key: "business.context" + value: this.getDetails().get("context") + +- class: mycustom.SpecialClient + method: doRequest + spanKind: CLIENT + spanStatus: 'returnValue.code() > 3 ? "OK" : "ERROR"' + attributes: + - key: "special.header" + value: 'param0.headers().get("special-header").substring(5)' +``` + +Expressions are written in [JEXL](https://commons.apache.org/proper/commons-jexl/reference/syntax.html) and may use +the following variables: + - `this` - which may be null for a static method + - `param0` through `paramN` where 0 indexes the first parameter to the method + - `returnValue` which is only defined for `spanStatus` and may be null (if an exception is thrown or the method returns void) + - `error` which is only defined for `spanStatus` and is the `Throwable` thrown by the method invocation (or null if a normal return) + +# See also + +If you don't need this much control over span creation, you might find +[methods instrumentation](../methods/README.md) a simpler way to get started. + +# Safety + +Please be aware of all side effects of statements you write in "nocode" instrumentation. +Avoid calling methods that permute state, interact with threads or locks, or might +have signigicant performance impact. Additionally, be aware that code under +active development might have its class or method names change, breaking instrumentation +created in this way. diff --git a/instrumentation/nocode/bootstrap/build.gradle.kts b/instrumentation/nocode/bootstrap/build.gradle.kts new file mode 100644 index 000000000000..072a96df450f --- /dev/null +++ b/instrumentation/nocode/bootstrap/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("otel.javaagent-bootstrap") +} diff --git a/instrumentation/nocode/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/nocode/NocodeExpression.java b/instrumentation/nocode/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/nocode/NocodeExpression.java new file mode 100644 index 000000000000..68cfd0ce7b91 --- /dev/null +++ b/instrumentation/nocode/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/nocode/NocodeExpression.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap.nocode; + +public interface NocodeExpression { + + Object evaluate(Object thiz, Object[] params); + + Object evaluateAtEnd(Object thiz, Object[] params, Object returnValue, Throwable error); +} diff --git a/instrumentation/nocode/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/nocode/NocodeInstrumentationRules.java b/instrumentation/nocode/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/nocode/NocodeInstrumentationRules.java new file mode 100644 index 000000000000..efa1b162458a --- /dev/null +++ b/instrumentation/nocode/bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/nocode/NocodeInstrumentationRules.java @@ -0,0 +1,159 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap.nocode; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.trace.SpanKind; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +public final class NocodeInstrumentationRules { + + public static final class Builder { + private String className; + private String methodName; + private NocodeExpression spanName; + private SpanKind spanKind; + private NocodeExpression spanStatus; + private final Map attributes = new HashMap<>(); + + @CanIgnoreReturnValue + public Builder className(String className) { + this.className = className; + return this; + } + + @CanIgnoreReturnValue + public Builder methodName(String methodName) { + this.methodName = methodName; + return this; + } + + @CanIgnoreReturnValue + public Builder spanName(NocodeExpression spanName) { + this.spanName = spanName; + return this; + } + + @CanIgnoreReturnValue + public Builder spanKind(SpanKind spanKind) { + this.spanKind = spanKind; + return this; + } + + @CanIgnoreReturnValue + public Builder spanStatus(NocodeExpression spanStatus) { + this.spanStatus = spanStatus; + return this; + } + + @CanIgnoreReturnValue + public Builder attribute(String key, NocodeExpression valueExpression) { + attributes.put(key, valueExpression); + return this; + } + + public Rule build() { + return new Rule(className, methodName, spanName, spanKind, spanStatus, attributes); + } + } + + public static final class Rule { + private static final AtomicInteger counter = new AtomicInteger(); + + private final int id = counter.incrementAndGet(); + private final String className; + private final String methodName; + private final NocodeExpression spanName; // may be null - use default of "class.method" + private final SpanKind spanKind; // may be null + private final NocodeExpression spanStatus; // may be null, should return string from StatusCodes + private final Map attributes; // key name to jexl expression + + public Rule( + String className, + String methodName, + NocodeExpression spanName, + SpanKind spanKind, + NocodeExpression spanStatus, + Map attributes) { + this.className = className; + this.methodName = methodName; + this.spanName = spanName; + this.spanKind = spanKind; + this.spanStatus = spanStatus; + this.attributes = Collections.unmodifiableMap(new HashMap<>(attributes)); + } + + public int getId() { + return id; + } + + public Map getAttributes() { + return attributes; + } + + public String getClassName() { + return className; + } + + public String getMethodName() { + return methodName; + } + + public NocodeExpression getSpanName() { + return spanName; + } + + public SpanKind getSpanKind() { + return spanKind; + } + + public NocodeExpression getSpanStatus() { + return spanStatus; + } + + @Override + public String toString() { + return "Nocode rule: " + + className + + "." + + methodName + + ":spanName=" + + spanName + + ":spanKind=" + + spanKind + + ":spanStatus=" + + spanStatus + + ",attrs=" + + attributes; + } + } + + private NocodeInstrumentationRules() {} + + // FUTURE setting the global and lookup could go away if the instrumentation could be + // parameterized with the Rule + + private static final HashMap ruleMap = new HashMap<>(); + + // Called by the NocodeInitializer + public static void setGlobalRules(List rules) { + for (Rule r : rules) { + ruleMap.put(r.id, r); + } + } + + public static Iterable getGlobalRules() { + return ruleMap.values(); + } + + public static Rule find(int id) { + return ruleMap.get(id); + } +} diff --git a/instrumentation/nocode/javaagent-unit-tests/build.gradle.kts b/instrumentation/nocode/javaagent-unit-tests/build.gradle.kts new file mode 100644 index 000000000000..d15743100378 --- /dev/null +++ b/instrumentation/nocode/javaagent-unit-tests/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + testImplementation(project(":instrumentation:nocode:javaagent")) + testImplementation(project(":instrumentation:nocode:bootstrap")) + testImplementation("org.apache.commons:commons-jexl3") +} diff --git a/instrumentation/nocode/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/nocode/JexlTest.java b/instrumentation/nocode/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/nocode/JexlTest.java new file mode 100644 index 000000000000..8b6d4c707e4b --- /dev/null +++ b/instrumentation/nocode/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/nocode/JexlTest.java @@ -0,0 +1,213 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nocode; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; +import org.apache.commons.jexl3.JexlExpression; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class JexlTest { + private static final Map thiz = new HashMap<>(); + private static final Set param0 = new HashSet<>(); + private static final Map param1 = new HashMap<>(); + + static { + thiz.put("key", "value"); + param0.add("present"); + param1.put("float", 7.0f); + } + + private JexlTest() {} + + private static Object evalJexl(String jexl, Object thiz, Object[] params) { + JexlEvaluator evaluator = new JexlEvaluator(); + JexlExpression expression = evaluator.createExpression(jexl); + if (expression == null) { + return null; + } + return evaluator.evaluate(expression, thiz, params); + } + + static Stream jexlToExpected() { + return Stream.of( + arguments("this", "{key=value}"), + arguments("this.toString()", "{key=value}"), + arguments("this.toString().length()", 11), + arguments("this.get(\"key\")", "value"), + arguments("this.get(\"key\").substring(1)", "alue"), + arguments("param0.isEmpty()", false), + arguments("param0.contains(\"present\")", true), + arguments("\"prefix: \"+this.toString()+\" (suffix)\"", "prefix: {key=value} (suffix)"), + arguments("param1.get(\"float\")", 7.0f), + arguments("this.entrySet().size()", 1)); + } + + @ParameterizedTest + @MethodSource("jexlToExpected") + void testBasicBehavior(String jexl, Object expected) { + Object result = evalJexl(jexl, thiz, new Object[] {param0, param1}); + if (expected instanceof String) { + result = result == null ? null : result.toString(); + } + assertEquals(expected, result, jexl); + } + + @ParameterizedTest + @ValueSource( + strings = { + "nosuchvar", + "nosuchvar.toString()", + "this .", + "this . ", + "this.noSuchMethod()", + "this.toString()extrastuffatend", + "this.toString()toString()", + "param1.toString()", // out of bounds + "param999.toString()", + "this.get(\"noclosequote)", + "this.get(\"nocloseparan\"", + "this.noparens", + "this.noparens.anotherMethod()", + "this.wrongOrder)(", + "this.get(NotALiteralParameter);", + "this.get(12.2)", + "this.get(this)", + "this.get(\"NoSuchKey\")", // evals completely but returns null + "param1.toString()", // no such param + }) + void testInvalidJexlReturnNull(String invalid) { + Object answer = evalJexl(invalid, thiz, new Object[] {param0}); + assertNull(answer, "Expected null for \"" + invalid + "\" but was \"" + answer + "\""); + } + + @Test + void testIntegerLiteralLongerThanOneDigit() { + Map o = new HashMap<>(); + o.put("key", "really long value"); + String jexl = "this.get(\"key\").substring(10)"; + assertEquals("g value", evalJexl(jexl, o, new Object[0])); + } + + public static class TakeString { + public String take(String s) { + return s; + } + } + + public static class TakeObject { + public String take(Object o) { + return o.toString(); + } + } + + public static class TakeBooleanPrimitive { + public String take(boolean param) { + return Boolean.toString(param); + } + } + + public static class TakeBoolean { + public String take(Boolean param) { + return param.toString(); + } + } + + public static class TakeIntegerPrimitive { + public String take(int param) { + return Integer.toString(param); + } + } + + public static class TakeInteger { + public String take(Integer param) { + return param.toString(); + } + } + + public static class TakeLongPrimitize { + public String take(long param) { + return Long.toString(param); + } + } + + public static class TakeLong { + public String take(Long param) { + return param.toString(); + } + } + + @Test + void testBooleanLiteralParamTypes() { + assertEquals("true", evalJexl("this.take(true)", new TakeBooleanPrimitive(), new Object[0])); + assertEquals("false", evalJexl("this.take(false)", new TakeBoolean(), new Object[0])); + assertEquals("true", evalJexl("this.take(true)", new TakeObject(), new Object[0])); + } + + @Test + void testStringLiteralParamTypes() { + assertEquals("a", evalJexl("this.take(\"a\")", new TakeString(), new Object[0])); + assertEquals("a", evalJexl("this.take(\"a\")", new TakeObject(), new Object[0])); + } + + @Test + public void testIntegerLiteralParamTypes() { + assertEquals("13", evalJexl("this.take(13)", new TakeIntegerPrimitive(), new Object[0])); + assertEquals("13", evalJexl("this.take(13)", new TakeInteger(), new Object[0])); + assertEquals("13", evalJexl("this.take(13)", new TakeLongPrimitize(), new Object[0])); + // Just documenting some oddness here with the long treatment + assertEquals("13", evalJexl("this.take(13L)", new TakeLong(), new Object[0])); + assertEquals("13", evalJexl("this.take(13)", new TakeObject(), new Object[0])); + } + + @ParameterizedTest + @ValueSource( + strings = { + "this.get(\"key\").substring(1)", + " this.get(\"key\").substring(1)", + "this .get(\"key\").substring(1)", + "this. get(\"key\").substring(1)", + "this.get (\"key\").substring(1)", + "this.get( \"key\").substring(1)", + "this.get(\"key\" ).substring(1)", + "this.get(\"key\")\t.substring(1)", + "this.get(\"key\").\nsubstring(1)", + "this.get(\"key\").substring\r(1)", + "this.get(\"key\").substring( 1)", + "this.get(\"key\").substring(1 )", + }) + void testWhitespace(String test) { + assertEquals("alue", evalJexl(test, thiz, new Object[] {param0}), test); + } + + @Test + void testManyParams() { + Object[] params = new Object[13]; + Arrays.fill(params, new Object()); + assertEquals( + "java.lang.Object", evalJexl("param12.getClass().getName()", new Object(), params)); + } + + @Test + void testTernaryLogic() { + assertEquals( + "potato", evalJexl("this.size() > 0 ? \"potato\" : \"banana\"", thiz, new Object[] {})); + assertEquals( + "banana", evalJexl("this.size() < 0 ? \"potato\" : \"banana\"", thiz, new Object[] {})); + } +} diff --git a/instrumentation/nocode/javaagent/build.gradle.kts b/instrumentation/nocode/javaagent/build.gradle.kts new file mode 100644 index 000000000000..6435a8da8cfd --- /dev/null +++ b/instrumentation/nocode/javaagent/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + coreJdk() + } +} + +sourceSets { + test { + runtimeClasspath += files("${project.layout.buildDirectory}/classes/java/main") + } +} + +dependencies { + bootstrap(project(":instrumentation:nocode:bootstrap")) + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + compileOnly(project(":instrumentation-annotations-support")) + implementation(project(":sdk-autoconfigure-support")) + + compileOnly("org.snakeyaml:snakeyaml-engine:2.9") + + implementation("org.apache.commons:commons-jexl3") + + add("codegen", project(":instrumentation:nocode:bootstrap")) +} + +tasks.withType().configureEach { + environment("OTEL_JAVA_INSTRUMENTATION_NOCODE_YML_FILE", "./src/test/config/nocode.yml") +} diff --git a/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/JexlEvaluator.java b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/JexlEvaluator.java new file mode 100644 index 000000000000..c3f0b4dc2c2c --- /dev/null +++ b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/JexlEvaluator.java @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nocode; + +import java.util.logging.Logger; +import org.apache.commons.jexl3.JexlBuilder; +import org.apache.commons.jexl3.JexlContext; +import org.apache.commons.jexl3.JexlEngine; +import org.apache.commons.jexl3.JexlExpression; +import org.apache.commons.jexl3.JexlFeatures; +import org.apache.commons.jexl3.MapContext; +import org.apache.commons.jexl3.introspection.JexlPermissions; + +class JexlEvaluator { + private static final Logger logger = Logger.getLogger(JexlEvaluator.class.getName()); + + private final JexlEngine jexl; + + JexlEvaluator() { + JexlFeatures features = + new JexlFeatures() + .register(false) // don't support #register syntax + .comparatorNames(false); // don't support 'lt' as an alternative to '<' + this.jexl = + new JexlBuilder() + .features(features) + // "unrestricted" means "can introspect on custom classes" + .permissions(JexlPermissions.UNRESTRICTED) + // don't support ant syntax + .antish(false) + // This api is terribly named but false means "null deref throws exception rather than + // log warning" + .safe(false) + // We will catch our own exceptions + .silent(false) + // Don't assume unknown methods/variables mean "null" + .strict(true) + .create(); + } + + private static void setBeginningVariables(JexlContext context, Object thiz, Object[] params) { + context.set("this", thiz); + for (int i = 0; i < params.length; i++) { + context.set("param" + i, params[i]); + } + } + + private static Object evaluateExpression(JexlExpression expression, JexlContext context) { + try { + return expression.evaluate(context); + } catch (Throwable t) { + logger.warning("Can't evaluate {" + expression + "}: " + t); + return null; + } + } + + JexlExpression createExpression(String expression) { + try { + return jexl.createExpression(expression); + } catch (Throwable t) { + logger.warning("Invalid expression {" + expression + "}: " + t); + return null; + } + } + + private static void setEndingVariables(JexlContext context, Object returnValue, Throwable error) { + context.set("returnValue", returnValue); + context.set("error", error); + } + + Object evaluate(JexlExpression expression, Object thiz, Object[] params) { + JexlContext context = new MapContext(); + setBeginningVariables(context, thiz, params); + return evaluateExpression(expression, context); + } + + Object evaluateAtEnd( + JexlExpression expression, + Object thiz, + Object[] params, + Object returnValue, + Throwable error) { + JexlContext context = new MapContext(); + setBeginningVariables(context, thiz, params); + setEndingVariables(context, returnValue, error); + return evaluateExpression(expression, context); + } +} diff --git a/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeAttributesExtractor.java b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeAttributesExtractor.java new file mode 100644 index 000000000000..fd6680f05333 --- /dev/null +++ b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeAttributesExtractor.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nocode; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.javaagent.bootstrap.nocode.NocodeExpression; +import java.util.Map; +import javax.annotation.Nullable; + +class NocodeAttributesExtractor implements AttributesExtractor { + private final AttributesExtractor codeExtractor; + + public NocodeAttributesExtractor() { + codeExtractor = CodeAttributesExtractor.create(ClassAndMethod.codeAttributesGetter()); + } + + @Override + public void onStart( + AttributesBuilder attributesBuilder, Context context, NocodeMethodInvocation mi) { + codeExtractor.onStart(attributesBuilder, context, mi.getClassAndMethod()); + + Map attributes = mi.getRuleAttributes(); + for (String key : attributes.keySet()) { + NocodeExpression expression = attributes.get(key); + Object value = mi.evaluate(expression); + if (value instanceof Long + || value instanceof Integer + || value instanceof Short + || value instanceof Byte) { + attributesBuilder.put(key, ((Number) value).longValue()); + } else if (value instanceof Float || value instanceof Double) { + attributesBuilder.put(key, ((Number) value).doubleValue()); + } else if (value instanceof Boolean) { + attributesBuilder.put(key, (Boolean) value); + } else if (value != null) { + attributesBuilder.put(key, value.toString()); + } + } + } + + @Override + public void onEnd( + AttributesBuilder attributesBuilder, + Context context, + NocodeMethodInvocation nocodeMethodInvocation, + @Nullable Object unused, + @Nullable Throwable throwable) { + codeExtractor.onEnd( + attributesBuilder, context, nocodeMethodInvocation.getClassAndMethod(), unused, throwable); + } +} diff --git a/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeInitializer.java b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeInitializer.java new file mode 100644 index 000000000000..64e5086dd401 --- /dev/null +++ b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeInitializer.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nocode; + +import static io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil.getConfig; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.bootstrap.nocode.NocodeInstrumentationRules; +import io.opentelemetry.javaagent.tooling.BeforeAgentListener; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; + +@AutoService(BeforeAgentListener.class) +public class NocodeInitializer implements BeforeAgentListener { + + @Override + public void beforeAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) { + ConfigProperties config = getConfig(autoConfiguredOpenTelemetrySdk); + NocodeRulesParser parser = new NocodeRulesParser(config); + NocodeInstrumentationRules.setGlobalRules(parser.getInstrumentationRules()); + } +} diff --git a/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeInstrumentation.java b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeInstrumentation.java new file mode 100644 index 000000000000..7cfa7237bc3e --- /dev/null +++ b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeInstrumentation.java @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nocode; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; +import static io.opentelemetry.javaagent.instrumentation.nocode.NocodeSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.none; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndSupport; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +import io.opentelemetry.javaagent.bootstrap.nocode.NocodeInstrumentationRules; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.utility.JavaConstant; + +public final class NocodeInstrumentation implements TypeInstrumentation { + private final NocodeInstrumentationRules.Rule rule; + + public NocodeInstrumentation(NocodeInstrumentationRules.Rule rule) { + this.rule = rule; + } + + @Override + public ElementMatcher typeMatcher() { + // null rule is used when no rules are configured, this ensures that muzzle can collect helper + // classes + if (rule == null) { + return none(); + } + // methods instrumentation also uses hasSuperType + return hasSuperType(named(rule.getClassName())); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + rule != null ? named(rule.getMethodName()) : none(), + mapping -> + mapping.bind( + RuleId.class, JavaConstant.Simple.ofLoaded(rule != null ? rule.getId() : -1)), + this.getClass().getName() + "$NocodeAdvice"); + } + + @interface RuleId {} + + @SuppressWarnings("unused") + public static class NocodeAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @RuleId int ruleId, + @Advice.Origin("#t") Class declaringClass, + @Advice.Origin("#m") String methodName, + @Advice.Local("otelInvocation") NocodeMethodInvocation otelInvocation, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.This Object thiz, + @Advice.AllArguments Object[] methodParams) { + NocodeInstrumentationRules.Rule rule = NocodeInstrumentationRules.find(ruleId); + otelInvocation = + new NocodeMethodInvocation( + rule, ClassAndMethod.create(declaringClass, methodName), thiz, methodParams); + Context parentContext = currentContext(); + + if (!instrumenter().shouldStart(parentContext, otelInvocation)) { + return; + } + context = instrumenter().start(parentContext, otelInvocation); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Origin Method method, + @Advice.Local("otelInvocation") NocodeMethodInvocation otelInvocation, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Return(typing = Assigner.Typing.DYNAMIC, readOnly = false) Object returnValue, + @Advice.Thrown Throwable error) { + if (scope == null) { + return; + } + scope.close(); + + returnValue = + AsyncOperationEndSupport.create(instrumenter(), Object.class, method.getReturnType()) + .asyncEnd(context, otelInvocation, returnValue, error); + } + } +} diff --git a/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeInstrumentationModule.java b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeInstrumentationModule.java new file mode 100644 index 000000000000..76c54ef9b3e8 --- /dev/null +++ b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeInstrumentationModule.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nocode; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.bootstrap.nocode.NocodeInstrumentationRules; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.ArrayList; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public final class NocodeInstrumentationModule extends InstrumentationModule { + + public NocodeInstrumentationModule() { + super("nocode"); + } + + @Override + public List typeInstrumentations() { + List answer = new ArrayList<>(); + for (NocodeInstrumentationRules.Rule rule : NocodeInstrumentationRules.getGlobalRules()) { + answer.add(new NocodeInstrumentation(rule)); + } + // ensure that there is at least one instrumentation so that muzzle reference collection could + // work + if (answer.isEmpty()) { + answer.add(new NocodeInstrumentation(null)); + } + return answer; + } + + // If nocode instrumentation is added to something with existing auto-instrumentation, + // it would generally be better to run the nocode bits after the "regular" bits. + // E.g., if we want to add nocode to a servlet call, then we want to make sure that + // the "standard" servlet instrumentation runs first to handle context propagation, etc. + @Override + public int order() { + return Integer.MAX_VALUE; + } +} diff --git a/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeMethodInvocation.java b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeMethodInvocation.java new file mode 100644 index 000000000000..cfc03d78ec4e --- /dev/null +++ b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeMethodInvocation.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nocode; + +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +import io.opentelemetry.javaagent.bootstrap.nocode.NocodeExpression; +import io.opentelemetry.javaagent.bootstrap.nocode.NocodeInstrumentationRules; +import java.util.Collections; +import java.util.Map; + +public final class NocodeMethodInvocation { + private final NocodeInstrumentationRules.Rule rule; + private final ClassAndMethod classAndMethod; + private final Object thiz; + private final Object[] parameters; + + public NocodeMethodInvocation( + NocodeInstrumentationRules.Rule rule, ClassAndMethod cm, Object thiz, Object[] parameters) { + this.rule = rule; + this.classAndMethod = cm; + this.thiz = thiz; + this.parameters = parameters; + } + + public NocodeInstrumentationRules.Rule getRule() { + return rule; + } + + public ClassAndMethod getClassAndMethod() { + return classAndMethod; + } + + public Map getRuleAttributes() { + return rule == null ? Collections.emptyMap() : rule.getAttributes(); + } + + public Object evaluate(NocodeExpression expression) { + return expression.evaluate(thiz, parameters); + } + + public Object evaluateAtEnd(NocodeExpression expression, Object returnValue, Throwable error) { + return expression.evaluateAtEnd(thiz, parameters, returnValue, error); + } +} diff --git a/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeRulesParser.java b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeRulesParser.java new file mode 100644 index 000000000000..0fff001c151b --- /dev/null +++ b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeRulesParser.java @@ -0,0 +1,133 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nocode; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.javaagent.bootstrap.nocode.NocodeExpression; +import io.opentelemetry.javaagent.bootstrap.nocode.NocodeInstrumentationRules; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.commons.jexl3.JexlExpression; +import org.snakeyaml.engine.v2.api.Load; +import org.snakeyaml.engine.v2.api.LoadSettings; + +class NocodeRulesParser { + private static final String NOCODE_YMLFILE = "otel.java.instrumentation.nocode.yml.file"; + + private static final Logger logger = Logger.getLogger(NocodeRulesParser.class.getName()); + + private final List instrumentationRules; + private JexlEvaluator evaluator; + + public NocodeRulesParser(ConfigProperties config) { + instrumentationRules = Collections.unmodifiableList(new ArrayList<>(load(config))); + } + + public List getInstrumentationRules() { + return instrumentationRules; + } + + private List load(ConfigProperties config) { + String yamlFileName = config.getString(NOCODE_YMLFILE); + if (yamlFileName == null || yamlFileName.trim().isEmpty()) { + return Collections.emptyList(); + } + + try { + return loadUnsafe(yamlFileName); + } catch (Exception e) { + logger.log(Level.SEVERE, "Can't load configured nocode yaml.", e); + return Collections.emptyList(); + } + } + + @SuppressWarnings("unchecked") + private List loadUnsafe(String yamlFileName) throws Exception { + List answer = new ArrayList<>(); + try (InputStream inputStream = Files.newInputStream(Paths.get(yamlFileName.trim()))) { + Load load = new Load(LoadSettings.builder().build()); + Iterable parsedYaml = load.loadAllFromInputStream(inputStream); + for (Object yamlBit : parsedYaml) { + List> rulesMap = (List>) yamlBit; + for (Map yamlRule : rulesMap) { + NocodeInstrumentationRules.Builder builder = new NocodeInstrumentationRules.Builder(); + + // FUTURE support more complex class selection (inherits-from, wildcards, etc.) + builder.className(yamlRule.get("class").toString()); + // FUTURE support more complex method (specific overrides, wildcards, etc.) + builder.methodName(yamlRule.get("method").toString()); + builder.spanName(toExpression(yamlRule.get("spanName"))); + if (yamlRule.get("spanKind") != null) { + String spanKind = yamlRule.get("spanKind").toString(); + try { + builder.spanKind(SpanKind.valueOf(spanKind.toUpperCase(Locale.ROOT))); + } catch (IllegalArgumentException exception) { + logger.warning("Invalid span kind " + spanKind); + } + } + builder.spanStatus(toExpression(yamlRule.get("spanStatus"))); + + List> attrs = (List>) yamlRule.get("attributes"); + if (attrs != null) { + for (Map attr : attrs) { + builder.attribute(attr.get("key").toString(), toExpression(attr.get("value"))); + } + } + answer.add(builder.build()); + } + } + } + + return answer; + } + + private NocodeExpression toExpression(Object ruleNode) { + if (ruleNode == null) { + return null; + } + + String expressionText = ruleNode.toString(); + if (expressionText == null) { + return null; + } + + if (evaluator == null) { + evaluator = new JexlEvaluator(); + } + + JexlExpression jexlExpression = evaluator.createExpression(expressionText); + if (jexlExpression == null) { + return null; + } + + return new NocodeExpression() { + @Override + public Object evaluate(Object thiz, Object[] params) { + return evaluator.evaluate(jexlExpression, thiz, params); + } + + @Override + public Object evaluateAtEnd( + Object thiz, Object[] params, Object returnValue, Throwable error) { + return evaluator.evaluateAtEnd(jexlExpression, thiz, params, returnValue, error); + } + + @Override + public String toString() { + return expressionText; + } + }; + } +} diff --git a/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeSingletons.java b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeSingletons.java new file mode 100644 index 000000000000..8897cb26248a --- /dev/null +++ b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeSingletons.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nocode; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + +public final class NocodeSingletons { + private static final Instrumenter INSTRUMENTER; + + static { + INSTRUMENTER = + Instrumenter.builder( + GlobalOpenTelemetry.get(), "io.opentelemetry.nocode", new NocodeSpanNameExtractor()) + .addAttributesExtractor(new NocodeAttributesExtractor()) + .setSpanStatusExtractor(new NocodeSpanStatusExtractor()) + .buildInstrumenter(new NocodeSpanKindExtractor()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private NocodeSingletons() {} +} diff --git a/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeSpanKindExtractor.java b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeSpanKindExtractor.java new file mode 100644 index 000000000000..19849056362f --- /dev/null +++ b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeSpanKindExtractor.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nocode; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; + +class NocodeSpanKindExtractor implements SpanKindExtractor { + @Override + public SpanKind extract(NocodeMethodInvocation mi) { + if (mi.getRule() == null || mi.getRule().getSpanKind() == null) { + return SpanKind.INTERNAL; + } + return mi.getRule().getSpanKind(); + } +} diff --git a/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeSpanNameExtractor.java b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeSpanNameExtractor.java new file mode 100644 index 000000000000..ba4270464915 --- /dev/null +++ b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeSpanNameExtractor.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nocode; + +import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.util.ClassAndMethod; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.javaagent.bootstrap.nocode.NocodeInstrumentationRules; + +class NocodeSpanNameExtractor implements SpanNameExtractor { + private final SpanNameExtractor defaultNamer; + + public NocodeSpanNameExtractor() { + this.defaultNamer = CodeSpanNameExtractor.create(ClassAndMethod.codeAttributesGetter()); + } + + @Override + public String extract(NocodeMethodInvocation mi) { + NocodeInstrumentationRules.Rule rule = mi.getRule(); + if (rule != null && rule.getSpanName() != null) { + Object name = mi.evaluate(rule.getSpanName()); + if (name != null) { + return name.toString(); + } + } + return defaultNamer.extract(mi.getClassAndMethod()); + } +} diff --git a/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeSpanStatusExtractor.java b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeSpanStatusExtractor.java new file mode 100644 index 000000000000..28d34e2bd7be --- /dev/null +++ b/instrumentation/nocode/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeSpanStatusExtractor.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nocode; + +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import java.util.Locale; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +class NocodeSpanStatusExtractor implements SpanStatusExtractor { + private static final Logger logger = Logger.getLogger(NocodeSpanStatusExtractor.class.getName()); + + @Override + public void extract( + SpanStatusBuilder spanStatusBuilder, + NocodeMethodInvocation mi, + @Nullable Object returnValue, + @Nullable Throwable error) { + + if (mi.getRule() == null || mi.getRule().getSpanStatus() == null) { + if (error != null) { + SpanStatusExtractor.getDefault().extract(spanStatusBuilder, mi, returnValue, error); + } + return; + } + Object status = mi.evaluateAtEnd(mi.getRule().getSpanStatus(), returnValue, error); + if (status != null) { + try { + StatusCode code = StatusCode.valueOf(status.toString().toUpperCase(Locale.ROOT)); + spanStatusBuilder.setStatus(code); + } catch (IllegalArgumentException noMatchingValue) { + // nop, should remain UNSET + logger.fine("Invalid span status ignored: " + status); + } + } + } +} diff --git a/instrumentation/nocode/javaagent/src/test/config/nocode.yml b/instrumentation/nocode/javaagent/src/test/config/nocode.yml new file mode 100644 index 000000000000..483122ab08a4 --- /dev/null +++ b/instrumentation/nocode/javaagent/src/test/config/nocode.yml @@ -0,0 +1,42 @@ +- class: io.opentelemetry.javaagent.instrumentation.nocode.NocodeInstrumentationTest$SampleClass + method: doSomething + spanName: "this.getName()" + attributes: + - key: "details" + value: this.getDetails() + - key: "map.size" + value: this.getMap().entrySet().size() + - key: "map.isEmpty" + value: this.getMap().isEmpty() + - key: getFloat + value: this.getFloat() + - key: getDouble + value: this.getDouble() + - key: getLong + value: this.getLong() + - key: getShort + value: this.getShort() + - key: getByte + value: this.getByte() + +- class: io.opentelemetry.javaagent.instrumentation.nocode.NocodeInstrumentationTest$SampleClass + method: throwException + spanKind: SERVER + attributes: + - key: "five" + value: param0.toString().substring(0) + +- class: io.opentelemetry.javaagent.instrumentation.nocode.NocodeInstrumentationTest$SampleClass + method: echo + spanStatus: 'returnValue.booleanValue() ? "ERROR" : "OK"' + + +- class: io.opentelemetry.javaagent.instrumentation.nocode.NocodeInstrumentationTest$SampleClass + method: doInvalidRule + spanName: "this.thereIsNoSuchMethod()" + spanKind: INVALID + spanStatus: invalid jexl that does not parse + attributes: + - key: "notpresent" + value: "invalid.noSuchStatement()" + diff --git a/instrumentation/nocode/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeInstrumentationTest.java b/instrumentation/nocode/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeInstrumentationTest.java new file mode 100644 index 000000000000..d29733ac639b --- /dev/null +++ b/instrumentation/nocode/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/nocode/NocodeInstrumentationTest.java @@ -0,0 +1,149 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nocode; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.StatusDataAssert; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +// This test has "test/config/nocode.yml" applied to it by the gradle environment setting +class NocodeInstrumentationTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void testBasicMethod() { + new SampleClass().doSomething(); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("name") + .hasKind(SpanKind.INTERNAL) + .hasStatus(StatusData.unset()) + .hasAttributesSatisfying( + equalTo(AttributeKey.doubleKey("getFloat"), 7.0), + equalTo(AttributeKey.doubleKey("getDouble"), 8.0), + equalTo(AttributeKey.longKey("getLong"), 9), + equalTo(AttributeKey.longKey("getShort"), 10), + equalTo(AttributeKey.longKey("getByte"), 11), + equalTo(AttributeKey.booleanKey("map.isEmpty"), false), + equalTo(AttributeKey.longKey("map.size"), 2), + equalTo(AttributeKey.stringKey("details"), "details")))); + } + + @Test + void testRuleWithManyInvalidFields() { + new SampleClass().doInvalidRule(); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SampleClass.doInvalidRule") + .hasKind(SpanKind.INTERNAL) + .hasStatus(StatusData.unset()) + .hasTotalAttributeCount( + 2))); // two code. attribute but nothing from the invalid rule + } + + @Test + void testThrowException() { + try { + new SampleClass().throwException(5); + } catch (UnsupportedOperationException expected) { + // nop + } + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SampleClass.throwException") + .hasKind(SpanKind.SERVER) + .hasEventsSatisfyingExactly(event -> event.hasName("exception")) + .hasStatusSatisfying(StatusDataAssert::isError) + .hasAttributesSatisfying(equalTo(AttributeKey.stringKey("five"), "5")))); + } + + @Test + void testEchoTrueIsError() { + new SampleClass().echo(true); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SampleClass.echo") + .hasStatusSatisfying(StatusDataAssert::isError))); + } + + @Test + void testEchoFalseIsOk() { + new SampleClass().echo(false); + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("SampleClass.echo").hasStatusSatisfying(StatusDataAssert::isOk))); + } + + public static class SampleClass { + public String getName() { + return "name"; + } + + public String getDetails() { + return "details"; + } + + public float getFloat() { + return 7.0f; + } + + public double getDouble() { + return 8.0; + } + + public long getLong() { + return 9L; + } + + public short getShort() { + return 10; + } + + public byte getByte() { + return 11; + } + + public Map getMap() { + HashMap answer = new HashMap<>(); + answer.put("key", "value"); + answer.put("key2", "value2"); + return answer; + } + + public void throwException(int parameter) { + throw new UnsupportedOperationException("oh no"); + } + + public void doSomething() {} + + public void doInvalidRule() {} + + public boolean echo(boolean b) { + return b; + } + } +} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/TypeTransformer.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/TypeTransformer.java index 52d51312e0d0..7438e4b18f40 100644 --- a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/TypeTransformer.java +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/TypeTransformer.java @@ -5,7 +5,9 @@ package io.opentelemetry.javaagent.extension.instrumentation; +import java.util.function.Function; import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -17,12 +19,23 @@ * will provide the implementation of all transformations described here. */ public interface TypeTransformer { + /** + * Apply the advice class named {@code adviceClassName} to the instrumented type methods that + * match {@code methodMatcher}. + */ + default void applyAdviceToMethod( + ElementMatcher methodMatcher, String adviceClassName) { + applyAdviceToMethod(methodMatcher, Function.identity(), adviceClassName); + } + /** * Apply the advice class named {@code adviceClassName} to the instrumented type methods that * match {@code methodMatcher}. */ void applyAdviceToMethod( - ElementMatcher methodMatcher, String adviceClassName); + ElementMatcher methodMatcher, + Function mappingCustomizer, + String adviceClassName); /** * Apply a custom ByteBuddy {@link AgentBuilder.Transformer} to the instrumented type. Note that diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/TypeTransformerImpl.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/TypeTransformerImpl.java index e3b6285d334b..79dddf511b45 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/TypeTransformerImpl.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/TypeTransformerImpl.java @@ -9,6 +9,7 @@ import io.opentelemetry.javaagent.tooling.Utils; import io.opentelemetry.javaagent.tooling.bytebuddy.ExceptionHandlers; import io.opentelemetry.javaagent.tooling.instrumentation.indy.ForceDynamicallyTypedAssignReturnedFactory; +import java.util.function.Function; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.method.MethodDescription; @@ -29,10 +30,12 @@ final class TypeTransformerImpl implements TypeTransformer { @Override public void applyAdviceToMethod( - ElementMatcher methodMatcher, String adviceClassName) { + ElementMatcher methodMatcher, + Function mappingCustomizer, + String adviceClassName) { agentBuilder = agentBuilder.transform( - new AgentBuilder.Transformer.ForAdvice(adviceMapping) + new AgentBuilder.Transformer.ForAdvice(mappingCustomizer.apply(adviceMapping)) .include( Utils.getBootstrapProxy(), Utils.getAgentClassLoader(), diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/AdviceTransformer.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/AdviceTransformer.java index aab45a387921..9b56dc6fe0c3 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/AdviceTransformer.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/AdviceTransformer.java @@ -154,12 +154,14 @@ private static List getWritableArguments(MethodNode source) { if (source.visibleParameterAnnotations != null) { int i = 0; for (List list : source.visibleParameterAnnotations) { - for (AnnotationNode annotationNode : list) { - Type annotationType = Type.getType(annotationNode.desc); - if (ADVICE_ARGUMENT.equals(annotationType) && isWriteable(annotationNode)) { - Object value = getAnnotationValue(annotationNode); - if (value instanceof Integer) { - result.add(new OutputArgument(i, (Integer) value)); + if (list != null) { + for (AnnotationNode annotationNode : list) { + Type annotationType = Type.getType(annotationNode.desc); + if (ADVICE_ARGUMENT.equals(annotationType) && isWriteable(annotationNode)) { + Object value = getAnnotationValue(annotationNode); + if (value instanceof Integer) { + result.add(new OutputArgument(i, (Integer) value)); + } } } } @@ -177,10 +179,12 @@ private static OutputArgument getWritableReturnValue(MethodNode source) { if (source.visibleParameterAnnotations != null) { int i = 0; for (List list : source.visibleParameterAnnotations) { - for (AnnotationNode annotationNode : list) { - Type annotationType = Type.getType(annotationNode.desc); - if (ADVICE_RETURN.equals(annotationType) && isWriteable(annotationNode)) { - return new OutputArgument(i, -1); + if (list != null) { + for (AnnotationNode annotationNode : list) { + Type annotationType = Type.getType(annotationNode.desc); + if (ADVICE_RETURN.equals(annotationType) && isWriteable(annotationNode)) { + return new OutputArgument(i, -1); + } } } i++; @@ -198,11 +202,13 @@ private static OutputArgument getEnterArgument(MethodNode source) { if (source.visibleParameterAnnotations != null) { int i = 0; for (List list : source.visibleParameterAnnotations) { - for (AnnotationNode annotationNode : list) { - Type annotationType = Type.getType(annotationNode.desc); - if (ADVICE_ENTER.equals(annotationType) - && argumentTypes[i].getDescriptor().length() > 1) { - return new OutputArgument(i, -1); + if (list != null) { + for (AnnotationNode annotationNode : list) { + Type annotationType = Type.getType(annotationNode.desc); + if (ADVICE_ENTER.equals(annotationType) + && argumentTypes[i].getDescriptor().length() > 1) { + return new OutputArgument(i, -1); + } } } i++; @@ -220,12 +226,14 @@ private static List getLocals(MethodNode source) { if (source.visibleParameterAnnotations != null) { int i = 0; for (List list : source.visibleParameterAnnotations) { - for (AnnotationNode annotationNode : list) { - Type annotationType = Type.getType(annotationNode.desc); - if (ADVICE_LOCAL.equals(annotationType)) { - Object value = getAnnotationValue(annotationNode); - if (value instanceof String) { - result.add(new AdviceLocal(i, (String) value)); + if (list != null) { + for (AnnotationNode annotationNode : list) { + Type annotationType = Type.getType(annotationNode.desc); + if (ADVICE_LOCAL.equals(annotationType)) { + Object value = getAnnotationValue(annotationNode); + if (value instanceof String) { + result.add(new AdviceLocal(i, (String) value)); + } } } } diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyTypeTransformerImpl.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyTypeTransformerImpl.java index 94fa337a5a06..ae65d50da1b2 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyTypeTransformerImpl.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyTypeTransformerImpl.java @@ -10,6 +10,7 @@ import io.opentelemetry.javaagent.tooling.bytebuddy.ExceptionHandlers; import java.io.FileOutputStream; import java.io.IOException; +import java.util.function.Function; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.method.MethodDescription; @@ -43,10 +44,12 @@ public IndyTypeTransformerImpl( @Override public void applyAdviceToMethod( - ElementMatcher methodMatcher, String adviceClassName) { + ElementMatcher methodMatcher, + Function mappingCustomizer, + String adviceClassName) { agentBuilder = agentBuilder.transform( - new AgentBuilder.Transformer.ForAdvice(adviceMapping) + new AgentBuilder.Transformer.ForAdvice(mappingCustomizer.apply(adviceMapping)) .advice(methodMatcher, adviceClassName) .include(getAdviceLocator(instrumentationModule.getClass().getClassLoader())) .withExceptionHandler(ExceptionHandlers.defaultExceptionHandler())); diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/InstrumentationModuleClassLoader.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/InstrumentationModuleClassLoader.java index 5d5a868e21b4..ef1dfeea6227 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/InstrumentationModuleClassLoader.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/InstrumentationModuleClassLoader.java @@ -25,9 +25,11 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nullable; import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.StringMatcher; @@ -181,7 +183,9 @@ private static Set getModuleAdviceNames(InstrumentationModule module) { new TypeTransformer() { @Override public void applyAdviceToMethod( - ElementMatcher methodMatcher, String adviceClassName) { + ElementMatcher methodMatcher, + Function mappingCustomizer, + String adviceClassName) { adviceNames.add(adviceClassName); } diff --git a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/generation/AdviceClassNameCollector.java b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/generation/AdviceClassNameCollector.java index 22a3d831d0c3..86896b0bb4d4 100644 --- a/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/generation/AdviceClassNameCollector.java +++ b/muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/generation/AdviceClassNameCollector.java @@ -8,7 +8,9 @@ import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import java.util.HashSet; import java.util.Set; +import java.util.function.Function; import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -17,7 +19,9 @@ final class AdviceClassNameCollector implements TypeTransformer { @Override public void applyAdviceToMethod( - ElementMatcher methodMatcher, String adviceClassName) { + ElementMatcher methodMatcher, + Function mappingCustomizer, + String adviceClassName) { adviceClassNames.add(adviceClassName); } diff --git a/settings.gradle.kts b/settings.gradle.kts index 9c01461bac4f..2ea59a15aed0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -414,6 +414,9 @@ include(":instrumentation:netty:netty-4.1:testing") include(":instrumentation:netty:netty-common-4.0:javaagent") include(":instrumentation:netty:netty-common-4.0:library") include(":instrumentation:netty:netty-common:library") +include(":instrumentation:nocode:bootstrap") +include(":instrumentation:nocode:javaagent") +include(":instrumentation:nocode:javaagent-unit-tests") include(":instrumentation:okhttp:okhttp-2.2:javaagent") include(":instrumentation:okhttp:okhttp-3.0:javaagent") include(":instrumentation:okhttp:okhttp-3.0:library")