Third-party exporter loading framework (#159)
This commit is contained in:
parent
75d807add9
commit
8235f7bd04
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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() {}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
io.opentelemetry.auto.dummyexporter.DummySpanExporterFactory
|
|
@ -0,0 +1,5 @@
|
|||
apply from: "${rootDir}/gradle/java.gradle"
|
||||
|
||||
dependencies {
|
||||
compileOnly deps.opentelemetrySdk
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package io.opentelemetry.auto.exportersupport;
|
||||
|
||||
import io.opentelemetry.sdk.trace.export.SpanExporter;
|
||||
|
||||
public interface SpanExporterFactory {
|
||||
SpanExporter fromConfig(ConfigProvider config);
|
||||
}
|
|
@ -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'
|
||||
|
||||
|
|
Loading…
Reference in New Issue