Third-party exporter loading framework
MODIFY
agent-bootstrap/src/main/java/io/opentelemetry/auto/config/Config.java
agent-bootstrap/src/main/java/io/opentelemetry/auto/config/Config.java
@@ -37,6 +37,7 @@ public class Config {
private static final Pattern ENV_REPLACEMENT = Pattern.compile("[^a-zA-Z0-9_]");
public static final String EXPORTER = "exporter";
+ public static final String EXPORTER_JAR = "exporter.jar";
public static final String SERVICE = "service";
public static final String CONFIGURATION_FILE = "trace.config";
public static final String TRACE_ENABLED = "trace.enabled";
@@ -83,6 +84,7 @@ public class Config {
private static final String DEFAULT_TRACE_METHODS = null;
@Getter private final String exporter;
+ @Getter private final String exporterJar;
@Getter private final String serviceName;
@Getter private final boolean traceEnabled;
@Getter private final boolean integrationsEnabled;
@@ -114,6 +116,7 @@ public class Config {
propertiesFromConfigFile = loadConfigurationFile();
exporter = getSettingFromEnvironment(EXPORTER, null);
+ exporterJar = getSettingFromEnvironment(EXPORTER_JAR, null);
serviceName = getSettingFromEnvironment(SERVICE, "(unknown)");
traceEnabled = getBooleanSettingFromEnvironment(TRACE_ENABLED, DEFAULT_TRACE_ENABLED);
integrationsEnabled =
@@ -170,6 +173,7 @@ public class Config {
// Read order: Properties -> Parent
private Config(final Properties properties, final Config parent) {
exporter = properties.getProperty(EXPORTER, parent.exporter);
+ exporterJar = properties.getProperty(EXPORTER_JAR, parent.exporterJar);
serviceName = properties.getProperty(SERVICE, parent.serviceName);
traceEnabled = getPropertyBooleanValue(properties, TRACE_ENABLED, parent.traceEnabled);

MODIFY
agent-tooling/agent-tooling.gradle
agent-tooling/agent-tooling.gradle
@@ -23,6 +23,9 @@ dependencies {
compile deps.opentelemetrySdk
compileOnly deps.opentelemetryJaeger
+ // TODO: This might have to live in opentelemetry-java
+ compile project(':exporter-support')
+
// Needed for Jaeger exporter
compileOnly group: 'io.grpc', name: 'grpc-api', version: '1.24.0'
@@ -33,6 +36,7 @@ dependencies {
implementation deps.autoservice
testCompile project(':testing')
+ testCompile project(':dummy-exporter')
instrumentationMuzzle sourceSets.main.output
instrumentationMuzzle configurations.compile

ADD
agent-tooling/src/main/java/io/opentelemetry/auto/tooling/DefaultConfigProvider.java
+package io.opentelemetry.auto.tooling;
+
+import io.opentelemetry.auto.config.Config;
+import io.opentelemetry.auto.exportersupport.ConfigProvider;
+
+public class DefaultConfigProvider implements ConfigProvider {
+ private final String prefix;
+
+ public DefaultConfigProvider(final String prefix) {
+ this.prefix = prefix;
+ }
+
+ @Override
+ public String getString(final String key, final String defaultValue) {
+ return Config.getSettingFromEnvironment(prefix + "." + key, defaultValue);
+ }
+
+ @Override
+ public int getInt(final String key, final int defaultValue) {
+ final String s = Config.getSettingFromEnvironment(prefix + "." + key, null);
+ if (s == null) {
+ return defaultValue;
+ }
+ return Integer.parseInt(s); // TODO: Handle format errors gracefully?
+ }
+
+ @Override
+ public long getLong(final String key, final long defaultValue) {
+ final String s = Config.getSettingFromEnvironment(prefix + "." + key, null);
+ if (s == null) {
+ return defaultValue;
+ }
+ return Long.parseLong(s); // TODO: Handle format errors gracefully?
+ }
+
+ @Override
+ public boolean getBoolean(final String key, final boolean defaultValue) {
+ final String s = Config.getSettingFromEnvironment(prefix + "." + key, null);
+ if (s == null) {
+ return defaultValue;
+ }
+ return Boolean.parseBoolean(s); // TODO: Handle format errors gracefully?
+ }
+
+ @Override
+ public double getDouble(final String key, final double defaultValue) {
+ final String s = Config.getSettingFromEnvironment(prefix + "." + key, null);
+ if (s == null) {
+ return defaultValue;
+ }
+ return Double.parseDouble(s); // TODO: Handle format errors gracefully?
+ }
+}

ADD
agent-tooling/src/main/java/io/opentelemetry/auto/tooling/ExporterClassLoader.java
+package io.opentelemetry.auto.tooling;
+
+import static io.opentelemetry.auto.tooling.ShadingRemapper.rule;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import net.bytebuddy.jar.asm.ClassReader;
+import net.bytebuddy.jar.asm.ClassWriter;
+import net.bytebuddy.jar.asm.commons.ClassRemapper;
+
+public class ExporterClassLoader extends URLClassLoader {
+ // We need to prefix the names to prevent the gradle shadowJar relocation rules from touching
+ // them. It's possible to do this by excluding this class from shading, but it may cause issue
+ // with transitive dependencies down the line.
+ private final ShadingRemapper remapper =
+ new ShadingRemapper(
+ rule(
+ "#io.opentelemetry.OpenTelemetry",
+ "#io.opentelemetry.auto.shaded.io.opentelemetry.OpenTelemetry"),
+ rule(
+ "#io.opentelemetry.context",
+ "#io.opentelemetry.auto.shaded.io.opentelemetry.context"),
+ rule(
+ "#io.opentelemetry.distributedcontext",
+ "#io.opentelemetry.auto.shaded.io.opentelemetry.distributedcontext"),
+ rule(
+ "#io.opentelemetry.internal",
+ "#io.opentelemetry.auto.shaded.io.opentelemetry.internal"),
+ rule(
+ "#io.opentelemetry.metrics",
+ "#io.opentelemetry.auto.shaded.io.opentelemetry.metrics"),
+ rule("#io.opentelemetry.trace", "#io.opentelemetry.auto.shaded.io.opentelemetry.trace"));
+
+ public ExporterClassLoader(final URL[] urls, final ClassLoader parent) {
+ super(urls, parent);
+ }
+
+ @Override
+ protected Class<?> findClass(final String name) throws ClassNotFoundException {
+
+ // Use resource loading to get the class as a stream of bytes, then use ASM to transform it.
+ try (final InputStream in = getResourceAsStream(name.replace('.', '/') + ".class")) {
+ final ClassWriter cw = new ClassWriter(0);
+ final ClassReader cr = new ClassReader(in);
+ cr.accept(new ClassRemapper(cw, remapper), ClassReader.EXPAND_FRAMES);
+ final byte[] bytes = cw.toByteArray();
+ return defineClass(name, bytes, 0, bytes.length);
+ } catch (final IOException e) {
+ throw new ClassNotFoundException(name);
+ }
+ }
+}

ADD
agent-tooling/src/main/java/io/opentelemetry/auto/tooling/ShadingRemapper.java
+package io.opentelemetry.auto.tooling;
+
+import java.util.Map;
+import java.util.TreeMap;
+import net.bytebuddy.jar.asm.commons.Remapper;
+
+public class ShadingRemapper extends Remapper {
+ public static class Rule {
+ private final String from;
+ private final String to;
+
+ public Rule(String from, String to) {
+ // Strip prefix added to prevent the build-time relocation from changing the names
+ if (from.startsWith("#")) {
+ from = from.substring(1);
+ }
+ if (to.startsWith("#")) {
+ to = to.substring(1);
+ }
+ this.from = from.replace('.', '/');
+ this.to = to.replace('.', '/');
+ }
+ }
+
+ public static Rule rule(final String from, final String to) {
+ return new Rule(from, to);
+ }
+
+ private final TreeMap<String, String> map = new TreeMap<>();
+
+ public ShadingRemapper(final Rule... rules) {
+ for (final Rule rule : rules) {
+ map.put(rule.from, rule.to);
+ }
+ }
+
+ @Override
+ public String map(final String internalName) {
+ final Map.Entry<String, String> e = map.floorEntry(internalName);
+ if (e != null && internalName.startsWith(e.getKey())) {
+ return e.getValue() + internalName.substring(e.getKey().length());
+ }
+ return super.map(internalName);
+ }
+}

MODIFY
agent-tooling/src/main/java/io/opentelemetry/auto/tooling/TracerInstaller.java
agent-tooling/src/main/java/io/opentelemetry/auto/tooling/TracerInstaller.java
package io.opentelemetry.auto.tooling;
+import com.google.common.annotations.VisibleForTesting;
import io.opentelemetry.auto.config.Config;
+import io.opentelemetry.auto.exportersupport.SpanExporterFactory;
import io.opentelemetry.auto.tooling.exporter.ExporterConfigException;
import io.opentelemetry.auto.tooling.exporter.ExporterRegistry;
-import io.opentelemetry.auto.tooling.exporter.SpanExporterFactory;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.trace.export.SimpleSpansProcessor;
+import io.opentelemetry.sdk.trace.export.SpanExporter;
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Iterator;
+import java.util.ServiceLoader;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TracerInstaller {
-
/** Register agent tracer if no agent tracer is already registered. */
public static synchronized void installAgentTracer() {
if (Config.get().isTraceEnabled()) {
// Try to create an exporter
- final String exporter = Config.get().getExporter();
- if (exporter != null) {
+ SpanExporter exporter = null;
+ final String expName = Config.get().getExporter();
+ if (expName != null) {
try {
- final SpanExporterFactory f = ExporterRegistry.getInstance().getFactory(exporter);
- OpenTelemetrySdk.getTracerFactory()
- .addSpanProcessor(SimpleSpansProcessor.newBuilder(f.newExporter()).build());
- log.info("Loaded span exporter: " + exporter);
+ final io.opentelemetry.auto.tooling.exporter.SpanExporterFactory f =
+ ExporterRegistry.getInstance().getFactory(expName);
+ exporter = f.newExporter();
+ log.info("Loaded span exporter: " + expName);
} catch (final ExporterConfigException e) {
log.warn("Error loading exporter. Spans will be dropped", e);
}
+ } else {
+ final String exporterJar = Config.get().getExporterJar();
+ if (exporterJar != null) {
+ exporter = loadFromJar(exporterJar);
+ }
+ }
+ if (exporter != null) {
+ OpenTelemetrySdk.getTracerFactory()
+ .addSpanProcessor(SimpleSpansProcessor.newBuilder(exporter).build());
+ log.info("Installed span exporter: " + exporter.getClass().getCanonicalName());
} else {
log.warn("No exporter is specified. Tracing will run but spans are dropped");
}
} else {
- log.debug("Tracing is disabled.");
+ log.info("Tracing is disabled.");
+ }
+ }
+
+ @VisibleForTesting
+ private static synchronized SpanExporter loadFromJar(final String exporterJar) {
+ final URL url;
+ try {
+ url = new File(exporterJar).toURI().toURL();
+ } catch (final MalformedURLException e) {
+ log.warn("Filename could not be parsed: " + exporterJar + ". Exporter is not installed");
+ return null;
+ }
+
+ final ExporterClassLoader exporterLoader =
+ new ExporterClassLoader(new URL[] {url}, TracerInstaller.class.getClassLoader());
+ final ServiceLoader<SpanExporterFactory> sl =
+ ServiceLoader.load(SpanExporterFactory.class, exporterLoader);
+ final Iterator<SpanExporterFactory> itor = sl.iterator();
+ if (itor.hasNext()) {
+ final SpanExporterFactory f = itor.next();
+ if (itor.hasNext()) {
+ log.warn(
+ "Exporter JAR defines more than one factory. Only the first one found will be used");
+ }
+ return f.fromConfig(new DefaultConfigProvider("exporter"));
}
+ log.warn("No matching providers in jar " + exporterJar);
+ return null;
}
public static void logVersionInfo() {

ADD
agent-tooling/src/test/groovy/io/opentelemetry/auto/tooling/ExporterLoaderTest.groovy
+package io.opentelemetry.auto.tooling
+
+
+import io.opentelemetry.auto.util.test.AgentSpecification
+import io.opentelemetry.sdk.OpenTelemetrySdk
+
+class ExporterLoaderTest extends AgentSpecification {
+ def jarName = "../dummy-exporter/build/libs/dummy-exporter-0.1.0-all.jar"
+ def tracer = OpenTelemetrySdk.getTracerFactory().get("test")
+
+ def "test load exporter"() {
+ when:
+ def exporter = TracerInstaller.loadFromJar(jarName)
+
+ then:
+ exporter.getClass().getName() == "io.opentelemetry.auto.dummyexporter.DummyExporter"
+ }
+}

ADD
dummy-exporter/dummy-exporter.gradle
+plugins {
+ id "com.github.johnrengelman.shadow"
+}
+
+apply from: "${rootDir}/gradle/java.gradle"
+
+dependencies {
+ compileOnly deps.opentelemetrySdk
+ compileOnly deps.opentelemetryApi
+ compile project(":exporter-support")
+}
+
+
+

ADD
dummy-exporter/src/main/java/io/opentelemetry/auto/dummyexporter/DummyExporter.java
+package io.opentelemetry.auto.dummyexporter;
+
+import io.opentelemetry.sdk.trace.SpanData;
+import io.opentelemetry.sdk.trace.export.SpanExporter;
+import io.opentelemetry.trace.AttributeValue;
+import java.util.List;
+import java.util.Map;
+
+public class DummyExporter implements SpanExporter {
+ private final String prefix;
+
+ public DummyExporter(final String prefix) {
+ this.prefix = prefix;
+ }
+
+ @Override
+ public ResultCode export(final List<SpanData> list) {
+ for (final SpanData span : list) {
+ System.out.print(
+ prefix + " " + span.getName() + " " + span.getSpanId().toLowerBase16() + " ");
+ for (final Map.Entry<String, AttributeValue> attr : span.getAttributes().entrySet()) {
+ System.out.print(attr.getKey() + "=");
+ final AttributeValue value = attr.getValue();
+ switch (value.getType()) {
+ case STRING:
+ System.out.print('"' + value.getStringValue() + '"');
+ break;
+ case BOOLEAN:
+ System.out.print(value.getBooleanValue());
+ break;
+ case LONG:
+ System.out.print(value.getLongValue());
+ break;
+ case DOUBLE:
+ System.out.print(value.getDoubleValue());
+ break;
+ }
+ System.out.print(" ");
+ }
+ }
+ System.out.println();
+ return ResultCode.SUCCESS;
+ }
+
+ @Override
+ public void shutdown() {}
+}

ADD
dummy-exporter/src/main/java/io/opentelemetry/auto/dummyexporter/DummySpanExporterFactory.java
+package io.opentelemetry.auto.dummyexporter;
+
+import io.opentelemetry.auto.exportersupport.ConfigProvider;
+import io.opentelemetry.auto.exportersupport.SpanExporterFactory;
+import io.opentelemetry.sdk.trace.export.SpanExporter;
+
+public class DummySpanExporterFactory implements SpanExporterFactory {
+ @Override
+ public SpanExporter fromConfig(final ConfigProvider config) {
+ return new DummyExporter(config.getString("prefix", "no-prefix"));
+ }
+}

ADD
dummy-exporter/src/main/resources/META-INF/services/io.opentelemetry.auto.exportersupport.SpanExporterFactory
+io.opentelemetry.auto.dummyexporter.DummySpanExporterFactory

ADD
exporter-support/exporter-support.gradle
+apply from: "${rootDir}/gradle/java.gradle"
+
+dependencies {
+ compileOnly deps.opentelemetrySdk
+}

ADD
exporter-support/src/main/java/io/opentelemetry/auto/exportersupport/ConfigProvider.java
+package io.opentelemetry.auto.exportersupport;
+
+public interface ConfigProvider {
+ String getString(String key, String defaultValue);
+
+ int getInt(String key, int defaultValue);
+
+ long getLong(String key, long defaultValue);
+
+ boolean getBoolean(String key, boolean defaultValue);
+
+ double getDouble(String key, double defaultValue);
+}

ADD
exporter-support/src/main/java/io/opentelemetry/auto/exportersupport/SpanExporterFactory.java
+package io.opentelemetry.auto.exportersupport;
+
+import io.opentelemetry.sdk.trace.export.SpanExporter;
+
+public interface SpanExporterFactory {
+ SpanExporter fromConfig(ConfigProvider config);
+}

MODIFY
settings.gradle
settings.gradle
@@ -25,6 +25,10 @@ include ':agent-bootstrap'
include ':agent-tooling'
include ':load-generator'
+// exporter support
+include ':exporter-support'
+
+// misc
include ':testing'
include ':utils:test-utils'
include ':utils:thread-utils'
@@ -133,6 +137,9 @@ include ':benchmark-integration'
include ':benchmark-integration:jetty-perftest'
include ':benchmark-integration:play-perftest'
+//Dummy exporter TODO: Move it somewhere better
+include ":dummy-exporter"
+
def setBuildFile(project) {
project.buildFileName = "${project.name}.gradle"
project.children.each {
@@ -145,3 +152,4 @@ setBuildFile(rootProject)
project(':agent-bootstrap').name = 'auto-bootstrap'
project(':agent-tooling').name = 'auto-tooling'
project(':java-agent').name = 'opentelemetry-auto'
+

Third-party exporter loading framework (#159)