Third-party exporter loading framework (#159)

This commit is contained in:
Pontus Rydin 2020-02-13 17:22:22 -05:00 committed by GitHub
parent 75d807add9
commit 8235f7bd04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 338 additions and 9 deletions

View File

@ -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);

View File

@ -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

View File

@ -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?
}
}

View File

@ -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);
}
}
}

View File

@ -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<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);
}
}

View File

@ -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<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() {
VersionLogger.logAllVersions();
log.debug(

View File

@ -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"
}
}

View File

@ -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")
}

View File

@ -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<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() {}
}

View File

@ -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"));
}
}

View File

@ -0,0 +1 @@
io.opentelemetry.auto.dummyexporter.DummySpanExporterFactory

View File

@ -0,0 +1,5 @@
apply from: "${rootDir}/gradle/java.gradle"
dependencies {
compileOnly deps.opentelemetrySdk
}

View File

@ -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);
}

View File

@ -0,0 +1,7 @@
package io.opentelemetry.auto.exportersupport;
import io.opentelemetry.sdk.trace.export.SpanExporter;
public interface SpanExporterFactory {
SpanExporter fromConfig(ConfigProvider config);
}

View File

@ -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'