From 8235f7bd0455400fe461acaf50906f6b0bd85c33 Mon Sep 17 00:00:00 2001 From: Pontus Rydin Date: Thu, 13 Feb 2020 17:22:22 -0500 Subject: [PATCH] Third-party exporter loading framework (#159) --- .../io/opentelemetry/auto/config/Config.java | 4 ++ agent-tooling/agent-tooling.gradle | 4 ++ .../auto/tooling/DefaultConfigProvider.java | 53 ++++++++++++++++ .../auto/tooling/ExporterClassLoader.java | 54 ++++++++++++++++ .../auto/tooling/ShadingRemapper.java | 45 ++++++++++++++ .../auto/tooling/TracerInstaller.java | 62 ++++++++++++++++--- .../auto/tooling/ExporterLoaderTest.groovy | 18 ++++++ dummy-exporter/dummy-exporter.gradle | 14 +++++ .../auto/dummyexporter/DummyExporter.java | 47 ++++++++++++++ .../DummySpanExporterFactory.java | 12 ++++ ...y.auto.exportersupport.SpanExporterFactory | 1 + exporter-support/exporter-support.gradle | 5 ++ .../auto/exportersupport/ConfigProvider.java | 13 ++++ .../exportersupport/SpanExporterFactory.java | 7 +++ settings.gradle | 8 +++ 15 files changed, 338 insertions(+), 9 deletions(-) create mode 100644 agent-tooling/src/main/java/io/opentelemetry/auto/tooling/DefaultConfigProvider.java create mode 100644 agent-tooling/src/main/java/io/opentelemetry/auto/tooling/ExporterClassLoader.java create mode 100644 agent-tooling/src/main/java/io/opentelemetry/auto/tooling/ShadingRemapper.java create mode 100644 agent-tooling/src/test/groovy/io/opentelemetry/auto/tooling/ExporterLoaderTest.groovy create mode 100644 dummy-exporter/dummy-exporter.gradle create mode 100644 dummy-exporter/src/main/java/io/opentelemetry/auto/dummyexporter/DummyExporter.java create mode 100644 dummy-exporter/src/main/java/io/opentelemetry/auto/dummyexporter/DummySpanExporterFactory.java create mode 100644 dummy-exporter/src/main/resources/META-INF/services/io.opentelemetry.auto.exportersupport.SpanExporterFactory create mode 100644 exporter-support/exporter-support.gradle create mode 100644 exporter-support/src/main/java/io/opentelemetry/auto/exportersupport/ConfigProvider.java create mode 100644 exporter-support/src/main/java/io/opentelemetry/auto/exportersupport/SpanExporterFactory.java diff --git a/agent-bootstrap/src/main/java/io/opentelemetry/auto/config/Config.java b/agent-bootstrap/src/main/java/io/opentelemetry/auto/config/Config.java index 884a609e5d..d30ea1fb3c 100644 --- a/agent-bootstrap/src/main/java/io/opentelemetry/auto/config/Config.java +++ b/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); diff --git a/agent-tooling/agent-tooling.gradle b/agent-tooling/agent-tooling.gradle index e64cd027f6..4c626ea3b4 100644 --- a/agent-tooling/agent-tooling.gradle +++ b/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 diff --git a/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/DefaultConfigProvider.java b/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/DefaultConfigProvider.java new file mode 100644 index 0000000000..cd55764c74 --- /dev/null +++ b/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/DefaultConfigProvider.java @@ -0,0 +1,53 @@ +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? + } +} diff --git a/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/ExporterClassLoader.java b/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/ExporterClassLoader.java new file mode 100644 index 0000000000..aaff5406ee --- /dev/null +++ b/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/ExporterClassLoader.java @@ -0,0 +1,54 @@ +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); + } + } +} diff --git a/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/ShadingRemapper.java b/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/ShadingRemapper.java new file mode 100644 index 0000000000..95b9282b6c --- /dev/null +++ b/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/ShadingRemapper.java @@ -0,0 +1,45 @@ +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 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 e = map.floorEntry(internalName); + if (e != null && internalName.startsWith(e.getKey())) { + return e.getValue() + internalName.substring(e.getKey().length()); + } + return super.map(internalName); + } +} diff --git a/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/TracerInstaller.java b/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/TracerInstaller.java index c22b4e530c..ecb673ce89 100644 --- a/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/TracerInstaller.java +++ b/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/TracerInstaller.java @@ -1,39 +1,83 @@ 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 sl = + ServiceLoader.load(SpanExporterFactory.class, exporterLoader); + final Iterator 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() { VersionLogger.logAllVersions(); log.debug( diff --git a/agent-tooling/src/test/groovy/io/opentelemetry/auto/tooling/ExporterLoaderTest.groovy b/agent-tooling/src/test/groovy/io/opentelemetry/auto/tooling/ExporterLoaderTest.groovy new file mode 100644 index 0000000000..6b77c32a56 --- /dev/null +++ b/agent-tooling/src/test/groovy/io/opentelemetry/auto/tooling/ExporterLoaderTest.groovy @@ -0,0 +1,18 @@ +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" + } +} diff --git a/dummy-exporter/dummy-exporter.gradle b/dummy-exporter/dummy-exporter.gradle new file mode 100644 index 0000000000..fc023b8c1f --- /dev/null +++ b/dummy-exporter/dummy-exporter.gradle @@ -0,0 +1,14 @@ +plugins { + id "com.github.johnrengelman.shadow" +} + +apply from: "${rootDir}/gradle/java.gradle" + +dependencies { + compileOnly deps.opentelemetrySdk + compileOnly deps.opentelemetryApi + compile project(":exporter-support") +} + + + diff --git a/dummy-exporter/src/main/java/io/opentelemetry/auto/dummyexporter/DummyExporter.java b/dummy-exporter/src/main/java/io/opentelemetry/auto/dummyexporter/DummyExporter.java new file mode 100644 index 0000000000..621fa71a66 --- /dev/null +++ b/dummy-exporter/src/main/java/io/opentelemetry/auto/dummyexporter/DummyExporter.java @@ -0,0 +1,47 @@ +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 list) { + for (final SpanData span : list) { + System.out.print( + prefix + " " + span.getName() + " " + span.getSpanId().toLowerBase16() + " "); + for (final Map.Entry 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() {} +} diff --git a/dummy-exporter/src/main/java/io/opentelemetry/auto/dummyexporter/DummySpanExporterFactory.java b/dummy-exporter/src/main/java/io/opentelemetry/auto/dummyexporter/DummySpanExporterFactory.java new file mode 100644 index 0000000000..9e36c4200e --- /dev/null +++ b/dummy-exporter/src/main/java/io/opentelemetry/auto/dummyexporter/DummySpanExporterFactory.java @@ -0,0 +1,12 @@ +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")); + } +} diff --git a/dummy-exporter/src/main/resources/META-INF/services/io.opentelemetry.auto.exportersupport.SpanExporterFactory b/dummy-exporter/src/main/resources/META-INF/services/io.opentelemetry.auto.exportersupport.SpanExporterFactory new file mode 100644 index 0000000000..6bdf2a296d --- /dev/null +++ b/dummy-exporter/src/main/resources/META-INF/services/io.opentelemetry.auto.exportersupport.SpanExporterFactory @@ -0,0 +1 @@ +io.opentelemetry.auto.dummyexporter.DummySpanExporterFactory diff --git a/exporter-support/exporter-support.gradle b/exporter-support/exporter-support.gradle new file mode 100644 index 0000000000..d5ecfabb2f --- /dev/null +++ b/exporter-support/exporter-support.gradle @@ -0,0 +1,5 @@ +apply from: "${rootDir}/gradle/java.gradle" + +dependencies { + compileOnly deps.opentelemetrySdk +} diff --git a/exporter-support/src/main/java/io/opentelemetry/auto/exportersupport/ConfigProvider.java b/exporter-support/src/main/java/io/opentelemetry/auto/exportersupport/ConfigProvider.java new file mode 100644 index 0000000000..8c3aeac563 --- /dev/null +++ b/exporter-support/src/main/java/io/opentelemetry/auto/exportersupport/ConfigProvider.java @@ -0,0 +1,13 @@ +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); +} diff --git a/exporter-support/src/main/java/io/opentelemetry/auto/exportersupport/SpanExporterFactory.java b/exporter-support/src/main/java/io/opentelemetry/auto/exportersupport/SpanExporterFactory.java new file mode 100644 index 0000000000..dbe40f81cc --- /dev/null +++ b/exporter-support/src/main/java/io/opentelemetry/auto/exportersupport/SpanExporterFactory.java @@ -0,0 +1,7 @@ +package io.opentelemetry.auto.exportersupport; + +import io.opentelemetry.sdk.trace.export.SpanExporter; + +public interface SpanExporterFactory { + SpanExporter fromConfig(ConfigProvider config); +} diff --git a/settings.gradle b/settings.gradle index 1347efc32f..8b20222fea 100644 --- a/settings.gradle +++ b/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' +