Skip to content
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
512c318
Initial veeeeery messing prototype of the nocode instrumentation concept
johnbley Feb 6, 2025
44bf2ae
Add and fix integral literal tests, misc other FIXME cleanups
johnbley Feb 7, 2025
3af909e
Cleanup initialization logic and immutability around lookup structure
johnbley Feb 7, 2025
ff7dcbd
Tests, FIXMEs, some debug printouts removed
johnbley Feb 7, 2025
00fe0ce
Make yml file actually configurable
johnbley Feb 7, 2025
cbedaa6
FIXME cleanup
johnbley Feb 7, 2025
a4007e9
Factor out interfaces desired by the instrumentation apparatus
johnbley Feb 7, 2025
862f833
Doc a desired test
johnbley Feb 7, 2025
90ed8bb
Make thrown exception capture work, clean up some FIXMEs, doc a few more
johnbley Feb 9, 2025
e08ae11
Start cleaning up parsing logic
johnbley Feb 10, 2025
29f5744
Clean up parsing of . separator a bit
johnbley Feb 10, 2025
39d4159
Clean up imports and package names
johnbley Feb 10, 2025
3bbe1f1
Use file i/o directly rather than slurping in a string for yaml parsing
johnbley Feb 10, 2025
a641880
Add spanKind support
johnbley Feb 10, 2025
c3d44c2
Clean up debug printouts
johnbley Feb 10, 2025
e055398
Clean up logging/error handling
johnbley Feb 10, 2025
48c5170
Doc JSPS a bit
johnbley Feb 10, 2025
6eb81e9
Use snakeyaml directly instead of through reflection
johnbley Feb 10, 2025
b8c57e3
Fallback to internal spanKind
johnbley Feb 10, 2025
16fc9b1
Build script cleanup
johnbley Feb 10, 2025
757e16e
Use superType instead of exact match to filter class names, just like…
johnbley Feb 11, 2025
00ca67c
Add tests for extractors
johnbley Feb 11, 2025
bbbc400
End-to-end integation test, some bootstrap classpath issues with the …
johnbley Feb 11, 2025
fc25038
Replace extractor tests with integration ones
johnbley Feb 12, 2025
0a8bd67
Rename instrumentation test
johnbley Feb 12, 2025
5429d18
Inline code to instrument inside the test
johnbley Feb 12, 2025
e908851
Organize imports
johnbley Feb 12, 2025
bbcab53
Fixed the open-access reflection issue for many cases
johnbley Feb 13, 2025
1ec7221
Merge branch 'signalfx:main' into nocode_prototype
johnbley Feb 13, 2025
bd4c6fd
spotlessApply
johnbley Feb 13, 2025
f52492c
Merge branch 'signalfx:main' into nocode_prototype
johnbley Feb 13, 2025
8c31160
Update bootstrap/src/main/java/com/splunk/opentelemetry/javaagent/boo…
johnbley Feb 14, 2025
5bf0a94
Update bootstrap/src/main/java/com/splunk/opentelemetry/javaagent/boo…
johnbley Feb 14, 2025
fbebfe3
Simplify global rules data structure, naming, and logic per feedback
johnbley Feb 14, 2025
a4a4ea7
Naming, docs feedback
johnbley Feb 14, 2025
58d5307
Update instrumentation/nocode/src/main/java/com/splunk/opentelemetry/…
johnbley Feb 14, 2025
52e691b
Update instrumentation/nocode/src/main/java/com/splunk/opentelemetry/…
johnbley Feb 14, 2025
2442690
Update instrumentation/nocode/src/main/java/com/splunk/opentelemetry/…
johnbley Feb 14, 2025
d354adb
Update instrumentation/nocode/src/main/java/com/splunk/opentelemetry/…
johnbley Feb 14, 2025
8e29611
Update instrumentation/nocode/src/main/java/com/splunk/opentelemetry/…
johnbley Feb 14, 2025
6e5cce2
Update instrumentation/nocode/src/main/java/com/splunk/opentelemetry/…
johnbley Feb 14, 2025
105f29a
Reduce use of public in test code
johnbley Feb 14, 2025
07ff329
Update instrumentation/nocode/src/main/java/com/splunk/opentelemetry/…
johnbley Feb 14, 2025
36e0258
Update instrumentation/nocode/src/main/java/com/splunk/opentelemetry/…
johnbley Feb 14, 2025
539c71b
Update instrumentation/nocode/src/main/java/com/splunk/opentelemetry/…
johnbley Feb 14, 2025
629e506
Update instrumentation/nocode/src/main/java/com/splunk/opentelemetry/…
johnbley Feb 14, 2025
0dded04
Make PR feedback compile
johnbley Feb 14, 2025
4faae8b
Use more kinds of whitespace in test
johnbley Feb 14, 2025
cb8f827
Clean up types, names, local variables for yaml parsing.
johnbley Feb 14, 2025
7641c24
Rename field
johnbley Feb 14, 2025
282fa2f
Comment a bit of danger for any future code editors
johnbley Feb 14, 2025
a2b789d
Clean up null checking logic
johnbley Feb 14, 2025
8468dd3
Parameterize some of the jsps tests
johnbley Feb 14, 2025
2cb3ada
Most of spotlessApply
johnbley Feb 14, 2025
2dc2675
Clean up javadoc style/use html rather than markdown for JSPS comment
johnbley Feb 14, 2025
7f8569b
spotlessApply
johnbley Feb 14, 2025
014158e
Parameterize basic jsps behavior tests
johnbley Feb 14, 2025
f22f7bb
Feedback nits
johnbley Feb 14, 2025
ea9082a
spotlessApply
johnbley Feb 14, 2025
58c71e2
Let muzzle plugin find helper classes
laurit Feb 17, 2025
0a0e20f
Merge pull request #1 from laurit/muzzle
johnbley Feb 17, 2025
43e5449
Logic and naming fixups from PR feedback
johnbley Feb 17, 2025
957fb3b
spotlessApply
johnbley Feb 17, 2025
575b0b8
Add comment about instrumentation order
johnbley Feb 18, 2025
4857123
Non-initialized singleton, better toString
johnbley Feb 18, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.splunk.opentelemetry.javaagent.bootstrap.nocode;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public final class NocodeRules {

public static final class Rule {
public final String className;
public final String methodName;
public final String spanName; // may be null - use default of "class.method"
public final String spanKind; // matches the SpanKind enum, null means default to INTERNAL
public final Map<String, String> attributes; // key name to jsps

public Rule(
String className,
String methodName,
String spanName,
String spanKind,
Map<String, String> attributes) {
this.className = className;
this.methodName = methodName;
this.spanName = spanName;
this.spanKind = spanKind;
this.attributes = Collections.unmodifiableMap(new HashMap<>(attributes));
}

public String toString() {
return className + "." + methodName + ":spanName=" + spanName + ",attrs=" + attributes;
}
}

// Using className.methodName as the key
private static final ConcurrentHashMap<String, Rule> Name2Rule = new ConcurrentHashMap<>();

public static void setGlobalRules(List<Rule> rules) {
for (Rule r : rules) {
Name2Rule.put(r.className + "." + r.methodName, r);
}
}

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

public static Rule find(String className, String methodName) {
return Name2Rule.get(className + "." + methodName);
}
}
12 changes: 12 additions & 0 deletions instrumentation/nocode/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugins {
id("splunk.instrumentation-conventions")
}

dependencies {
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-tooling")
compileOnly("org.snakeyaml:snakeyaml-engine:2.8")
}

tasks.withType<Test>().configureEach {
environment("SPLUNK_OTEL_INSTRUMENTATION_NOCODE_YML_FILE", "./src/test/config/nocode.yml")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.splunk.opentelemetry.instrumentation.nocode;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.logging.Logger;

/**
* JSPS stands for Java-like String-Producing Statement. A JSPS is essentially a single call in Java
* (as though it ends with a semicolon), with some limitations. Its purpose is to allow pieces of
* nocode instrumentation (attributes, span name) to be derived from the instrumentated context.
*
* <p>As some illustrative examples:
*
* <pre>
* this.getHeaders().get("X-Custom-Header").substring(5)
* param0.getDetails().getCustomerAccount().getAccountType()
* </pre>
*
* <p>The limitations are:
*
* <ul>
* <li>no access to variables other than 'this' and 'paramN' (N indexed at 0)
* <li>no control flow (if), no local variables, basically nothing other than a single chain of
* method calls
* <li>Method calls are limited to either 0 or 1 parameters currently
* <li>Parameters must be literals and only integral (int/long), string, and boolean literals are
* currently supported
* </ul>
*/
public final class JSPS {
private static final Logger logger = Logger.getLogger(JSPS.class.getName());
private static final Class[] NoParamTypes = new Class[0];

public static String evaluate(String jsps, Object thiz, Object[] params) {
try {
return unsafeEvaluate(jsps, thiz, params);
} catch (Throwable t) {
logger.warning("Can't evaluate {" + jsps + "}: " + t);
return null;
}
}

// FIXME Might be nice to support escaping quotes in string literals...
private static String unsafeEvaluate(String jsps, Object thiz, Object[] params) throws Exception {
jsps = jsps.trim();
int nextDot = jsps.indexOf('.');
String var = jsps.substring(0, nextDot).trim();
Object curObject = null;
if (var.equals("this")) {
curObject = thiz;
} else if (var.startsWith("param")) {
int varIndex = Integer.parseInt(var.substring("param".length()));
curObject = params[varIndex];
}
int curIndex = nextDot;
while (curIndex < jsps.length()) {
curIndex = jsps.indexOf('.', curIndex);
while (jsps.charAt(curIndex) == '.' || Character.isWhitespace(jsps.charAt(curIndex))) {
curIndex++;
}
int openParen = jsps.indexOf('(', curIndex);
String method = jsps.substring(curIndex, openParen).trim();
int closeParen = jsps.indexOf(')', openParen);
String paramString = jsps.substring(openParen + 1, closeParen).trim();
if (paramString.isEmpty()) {
Method m = findMatchingMethod(curObject, method, NoParamTypes);
curObject = m.invoke(curObject);
} else {
if (paramString.startsWith("\"") && paramString.endsWith("\"")) {
String passed = paramString.substring(1, paramString.length() - 1);
Method m = findMethodWithPossibleTypes(curObject, method, String.class, Object.class);
curObject = m.invoke(curObject, passed);
} else if (paramString.equals("true") || paramString.equals("false")) {
Method m =
findMethodWithPossibleTypes(
curObject, method, boolean.class, Boolean.class, Object.class);
curObject = m.invoke(curObject, Boolean.parseBoolean(paramString));
} else if (paramString.matches("[0-9]+")) {
try {
Method m =
findMethodWithPossibleTypes(
curObject, method, int.class, Integer.class, Object.class);
int passed = Integer.parseInt(paramString);
curObject = m.invoke(curObject, passed);
} catch (NoSuchMethodException tryLongInstead) {
Method m =
findMethodWithPossibleTypes(
curObject, method, long.class, Long.class, Object.class);
long passed = Long.parseLong(paramString);
curObject = m.invoke(curObject, passed);
}
} else {
throw new UnsupportedOperationException(
"Can't parse \"" + paramString + "\" as literal parameter");
}
}
curIndex = closeParen + 1;
}
return curObject == null ? null : curObject.toString();
}

// This sequence of methods is here because:
// - we want to try a variety of parameter types to match a literal
// e.g., someMethod(5) could match someMethod(int) or someMethod(Object)
// - module rules around reflection make some "legal" things harder and require
// looking for a public class/interface matching the method to call
// e.g., a naive reflective call through
// this.getSomeHashMap().entrySet().size()
// would fail simply because the HashMap$EntrySet implementation
// is not public, even though the interface it's being call through is.
private static Method findMatchingMethod(
Object curObject, String methodName, Class[] actualParamTypes) throws NoSuchMethodException {
Method m = findMatchingMethod(methodName, curObject.getClass(), actualParamTypes);
if (m == null) {
throw new NoSuchMethodException(
"Can't find matching method for " + methodName + " on " + curObject.getClass().getName());
}
return m;
}

// Returns null for none found
private static Method findMatchingMethod(
String methodName, Class clazz, Class[] actualParamTypes) {
if (clazz == null) {
return null;
}
if (Modifier.isPublic(clazz.getModifiers())) {
try {
return clazz.getMethod(methodName, actualParamTypes);
} catch (NoSuchMethodException nsme) {
// keep trying
}
}
// not public, try interfaces and supertype
for (Class iface : clazz.getInterfaces()) {
Method m = findMatchingMethod(methodName, iface, actualParamTypes);
if (m != null) {
return m;
}
}
return findMatchingMethod(methodName, clazz.getSuperclass(), actualParamTypes);
}

private static Method findMethodWithPossibleTypes(
Object curObject, String methodName, Class<?>... paramTypesToTryInOrder)
throws NoSuchMethodException {
Class c = curObject.getClass();
for (Class<?> paramType : paramTypesToTryInOrder) {
try {
return findMatchingMethod(curObject, methodName, new Class[] {paramType});
} catch (NoSuchMethodException e) {
// try next one
}
}
throw new NoSuchMethodException(methodName + " with single parameter matching given type");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.splunk.opentelemetry.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 java.util.Map;
import javax.annotation.Nullable;

public final class NocodeAttributesExtractor
implements AttributesExtractor<NocodeMethodInvocation, Void> {
private final AttributesExtractor<ClassAndMethod, Void> 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<String, String> attributes = mi.getRuleAttributes();
for (String key : attributes.keySet()) {
String jsps = attributes.get(key);
String value = JSPS.evaluate(jsps, mi.getThiz(), mi.getParameters());
if (value != null) {
attributesBuilder.put(key, value);
}
}
}

@Override
public void onEnd(
AttributesBuilder attributesBuilder,
Context context,
NocodeMethodInvocation nocodeMethodInvocation,
@Nullable Void unused,
@Nullable Throwable throwable) {
codeExtractor.onEnd(
attributesBuilder, context, nocodeMethodInvocation.getClassAndMethod(), unused, throwable);
}
}
Loading