Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
671c976
nocode instrumentation concept - partial port. Compiles but tests do…
johnbley Mar 25, 2025
29ac2c3
Progress - instrumentation test passing but not the jexl one yet
johnbley Mar 25, 2025
4ff583b
Jexl tests passing again, thanks to a terrible hack. Removed some de…
johnbley Mar 25, 2025
c9d16f3
spotlessApply
johnbley Mar 25, 2025
dc66bdb
Use builder pattern for rule construction
johnbley Mar 26, 2025
4ba34a7
Move nocode bootstrap code to more appropriate package name
johnbley Mar 26, 2025
852a81e
tidy build file
johnbley Mar 26, 2025
ac0ee90
spotlessApply
johnbley Mar 26, 2025
338cc30
Move jexl tests to new unittest package, fixing classpath problems
johnbley Mar 26, 2025
68dabd8
rename env/config variable to match otel specs
johnbley Mar 26, 2025
77acca4
Add a minimal README.md
johnbley Mar 26, 2025
1dfce1e
Adjust fixmes, build file cleanup, etc.
johnbley Mar 26, 2025
caa305e
generateFossaConfiguration
johnbley Mar 26, 2025
9cfe55a
checkstyle fixes
johnbley Mar 26, 2025
8e53bbd
Rework jexl dependency per build requirement
johnbley Mar 26, 2025
665be30
Got indy tests to "pass" but not sure exactly why
johnbley Mar 26, 2025
caec176
remove isIndyModule override
johnbley Mar 27, 2025
a32e63a
Merge branch 'open-telemetry:main' into nocode
johnbley Mar 27, 2025
1357911
Merge branch 'main' into nocode
johnbley Apr 1, 2025
5166f0d
Nocode instrumentation
laurit Apr 9, 2025
6e27dad
Merge pull request #1 from laurit/nocode-instrumentation
johnbley Apr 9, 2025
b722496
fix indy test
laurit Apr 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .fossa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 2 additions & 1 deletion dependencyManagement/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
47 changes: 47 additions & 0 deletions instrumentation/nocode/README.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions instrumentation/nocode/bootstrap/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
plugins {
id("otel.javaagent-bootstrap")
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<String, NocodeExpression> 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<String, NocodeExpression> attributes; // key name to jexl expression

public Rule(
String className,
String methodName,
NocodeExpression spanName,
SpanKind spanKind,
NocodeExpression spanStatus,
Map<String, NocodeExpression> 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<String, NocodeExpression> 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<Integer, Rule> ruleMap = new HashMap<>();

// Called by the NocodeInitializer
public static void setGlobalRules(List<Rule> rules) {
for (Rule r : rules) {
ruleMap.put(r.id, r);
}
}

public static Iterable<Rule> getGlobalRules() {
return ruleMap.values();
}

public static Rule find(int id) {
return ruleMap.get(id);
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
Loading
Loading