
com.lightstep.opentelemetry.exporter.LightstepSpanExporter Maven / Gradle / Ivy
Show all versions of lightstep-opentelemetry-auto-exporter Show documentation
package com.lightstep.opentelemetry.exporter;
import com.google.common.annotations.VisibleForTesting;
import com.lightstep.tracer.grpc.Auth;
import com.lightstep.tracer.grpc.KeyValue;
import com.lightstep.tracer.grpc.ReportRequest;
import com.lightstep.tracer.grpc.ReportResponse;
import com.lightstep.tracer.grpc.Reporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.trace.TracerSdkProvider;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import shaded.shaded.okhttp3.Dns;
import shaded.shaded.okhttp3.MediaType;
import shaded.shaded.okhttp3.OkHttpClient;
import shaded.shaded.okhttp3.Request;
import shaded.shaded.okhttp3.RequestBody;
import shaded.shaded.okhttp3.Response;
import shaded.shaded.okhttp3.ResponseBody;
/**
* Exports spans to Lightstep via OkHttp, using Lightstep's protobuf model.
*/
@ThreadSafe
public class LightstepSpanExporter implements SpanExporter {
private static final Logger logger = Logger.getLogger(LightstepSpanExporter.class.getName());
static final String MEDIA_TYPE_STRING = "application/octet-stream";
static final String LIGHTSTEP_ACCESS_TOKEN = "Lightstep-Access-Token";
static final String PATH = "/api/v2/reports";
@Nullable
private static final MediaType MEDIA_TYPE = MediaType.parse(MEDIA_TYPE_STRING);
private static final String GUID_KEY = "lightstep.guid";
// TODO: shouldn't be replaced by lightstep.service_name?
static final String COMPONENT_NAME_KEY = "lightstep.component_name";
static final String SERVICE_VERSION_KEY = "service.version";
static final String LIGHTSTEP_HOSTNAME_KEY = "lightstep.hostname";
static final String LIGHTSTEP_TRACER_PLATFORM_KEY = "lightstep.tracer_platform";
static final String LIGHTSTEP_TRACER_PLATFORM_VERSION_KEY =
"lightstep.tracer_platform_version";
/**
* Thread-specific random number generators. Each is seeded with the thread ID, so the sequence of
* pseudo-random numbers are unique between threads.
*
* See http://stackoverflow.com/questions/2546078/java-random-long-number-in-0-x-n-range
*/
private static final ThreadLocal random =
new ThreadLocal() {
@Override
protected Random initialValue() {
// It'd be nice to get the process ID into the mix, but there's no clear
// cross-platform, Java 6-compatible way to determine that
return new Random(
System.currentTimeMillis()
* (System.nanoTime() % 1000000)
* Thread.currentThread().getId()
* (long) (1024 * Math.random()));
}
};
private final URL collectorUrl;
private final OkHttpClient client;
private final Auth.Builder auth;
private final String serviceName;
private final String serviceVersion;
private final List lsSpanAttributes;
private final Reporter reporter;
/**
* Creates a new Lightstep OkHttp Span Reporter.
*
* @param collectorUrl collector url.
* @param deadlineMillis The maximum amount of time the tracer should wait for a response from the
* collector when sending a report.
* @param accessToken Your specific token for Lightstep access.
* @param okHttpDns DNS service used to lookup IP addresses for hostnames
* @param serviceName The service name attribute. If not set, will default to the Java runtime
* command.
*/
private LightstepSpanExporter(
URL collectorUrl,
long deadlineMillis,
String accessToken,
OkHttpDns okHttpDns,
String serviceName,
String serviceVersion) {
this.collectorUrl = collectorUrl;
this.serviceName = serviceName;
this.serviceVersion = serviceVersion;
OkHttpClient.Builder builder =
new OkHttpClient.Builder().connectTimeout(deadlineMillis, TimeUnit.MILLISECONDS);
if (okHttpDns != null) {
builder.dns(new CustomDns(okHttpDns));
}
this.client = builder.build();
this.auth = Auth.newBuilder().setAccessToken(accessToken);
this.lsSpanAttributes = new ArrayList<>();
this.lsSpanAttributes.add(KeyValue.newBuilder().setKey(LIGHTSTEP_HOSTNAME_KEY)
.setStringValue(LightstepConfig.LOCAL_HOSTNAME).build());
if (serviceVersion != null && !serviceVersion.isEmpty()) {
this.lsSpanAttributes.add(
KeyValue.newBuilder().setKey(SERVICE_VERSION_KEY).setStringValue(serviceVersion).build());
}
// should be last step in constructor
this.reporter = createReporter();
}
private Reporter createReporter() {
// Note that ThreadLocalRandom is a singleton, thread safe Random Generator
final long guid = random.get().nextLong();
final Reporter.Builder reporterBuilder = Reporter.newBuilder()
.setReporterId(guid)
.addTags(
KeyValue.newBuilder()
.setStringValue(serviceName)
.setKey(COMPONENT_NAME_KEY)
.build())
.addTags(KeyValue.newBuilder().setKey(GUID_KEY).setIntValue(guid).build())
.addTags(
KeyValue.newBuilder()
.setKey(LIGHTSTEP_HOSTNAME_KEY)
.setStringValue(LightstepConfig.LOCAL_HOSTNAME)
.build())
.addTags(
KeyValue.newBuilder()
.setKey(LIGHTSTEP_TRACER_PLATFORM_KEY)
.setStringValue("jre")
.build())
.addTags(
KeyValue.newBuilder()
.setKey(LIGHTSTEP_TRACER_PLATFORM_VERSION_KEY)
.setStringValue(System.getProperty("java.version"))
.build());
if (serviceVersion != null && !serviceVersion.isEmpty()) {
reporterBuilder.addTags(
KeyValue.newBuilder()
.setStringValue(serviceVersion)
.setKey(SERVICE_VERSION_KEY)
.build());
}
return reporterBuilder.build();
}
/**
* Creates a new builder instance.
*
* @return a new instance builder for this exporter
*/
public static Builder newBuilder() {
return new Builder();
}
/**
* Submits all the given spans in a single batch to the Lightstep collector.
*
* @param spans the list of sampled Spans to be exported.
* @return the result of the operation
*/
@Override
public ResultCode export(Collection spans) {
ReportRequest request =
ReportRequest.newBuilder()
.setAuth(auth)
.setReporter(reporter)
.addAllSpans(Adapter.toLightstepSpans(spans, lsSpanAttributes))
.build();
try (Response response = client.newCall(toRequest(request)).execute()) {
if (!response.isSuccessful()) {
logger.log(Level.WARNING, "Failed to post spans to collector. " + response.toString());
return ResultCode.FAILURE;
}
final ResponseBody body = response.body();
if (body == null) {
logger.log(Level.WARNING, "Response body is null");
return ResultCode.FAILURE;
}
final ReportResponse reportResponse = ReportResponse.parseFrom(body.byteStream());
if (!reportResponse.getErrorsList().isEmpty()) {
List errs = reportResponse.getErrorsList();
for (String err : errs) {
logger.log(Level.WARNING, "Collector response contained error: " + err);
}
return ResultCode.FAILURE;
}
return ResultCode.SUCCESS;
} catch (Throwable e) {
logger.log(Level.WARNING, "Failed to post spans", e);
return ResultCode.FAILURE;
}
}
private Request toRequest(ReportRequest request) {
return new Request.Builder()
.url(this.collectorUrl)
.post(RequestBody.create(request.toByteArray(), MEDIA_TYPE))
.addHeader(LIGHTSTEP_ACCESS_TOKEN, request.getAuth().getAccessToken())
.build();
}
/**
* Initiates an orderly shutdown in which preexisting calls continue but new calls are immediately
* cancelled.
*/
@Override
public void shutdown() {
client.dispatcher().executorService().shutdown();
}
/**
* The Lightstep exporter does not batch spans, so this method will immediately return with
* success.
*
* @return always Success
*/
@Override
public ResultCode flush() {
return ResultCode.SUCCESS;
}
public interface OkHttpDns {
List lookup(String hostname);
}
static class CustomDns implements Dns {
final OkHttpDns dns;
public CustomDns(OkHttpDns dns) {
this.dns = dns;
}
@Override
public List lookup(String hostname) {
return dns.lookup(hostname);
}
}
/**
* Builder utility for this exporter.
*/
public static class Builder {
private int collectorPort = -1;
private String collectorProtocol = LightstepConfig.PROTOCOL_HTTPS;
private String collectorHost = LightstepConfig.DEFAULT_HOST;
private long deadlineMillis = LightstepConfig.DEFAULT_DEADLINE_MILLIS;
private String accessToken = "";
private OkHttpDns okHttpDns;
private String serviceName;
private String serviceVersion;
/**
* Creates builder from configuration file
*
* @param configFile path to configuration file
* @return this builder's instance
*/
public static Builder fromConfigFile(final String configFile) {
final Properties properties = LightstepConfig.loadConfig(configFile);
final Builder builder = new Builder();
builder.collectorProtocol = properties.getProperty(
LightstepConfig.COLLECTOR_PROTOCOL_PROPERTY_KEY, LightstepConfig.PROTOCOL_HTTPS);
builder.collectorHost = properties
.getProperty(LightstepConfig.COLLECTOR_HOST_PROPERTY_KEY,
LightstepConfig.DEFAULT_HOST);
builder.collectorPort = Integer
.parseInt(properties.getProperty(LightstepConfig.COLLECTOR_PORT_PROPERTY_KEY,
String.valueOf(LightstepConfig.DEFAULT_SECURE_PORT)));
builder.accessToken = properties
.getProperty(LightstepConfig.ACCESS_TOKEN_PROPERTY_KEY, "");
builder.deadlineMillis = Long.parseLong(properties
.getProperty(LightstepConfig.DEADLINE_MILLIS_PROPERTY_KEY,
String.valueOf(LightstepConfig.DEFAULT_DEADLINE_MILLIS)));
builder.serviceName = properties
.getProperty(LightstepConfig.SERVICE_NAME_PROPERTY_KEY,
LightstepConfig.defaultServiceName());
builder.serviceVersion = properties
.getProperty(LightstepConfig.SERVICE_VERSION_PROPERTY_KEY);
return builder;
}
/**
* Creates builder from system properties and environmental variables.
*
* @return this builder's instance
*/
public static Builder fromEnv() {
final Builder builder = new Builder();
builder.collectorProtocol = getProperty(LightstepConfig.COLLECTOR_PROTOCOL,
LightstepConfig.PROTOCOL_HTTPS);
builder.collectorPort = Integer
.parseInt(getProperty(LightstepConfig.COLLECTOR_PORT,
String.valueOf(LightstepConfig.DEFAULT_SECURE_PORT)));
builder.collectorHost = getProperty(LightstepConfig.COLLECTOR_HOST,
LightstepConfig.DEFAULT_HOST);
builder.deadlineMillis = Long.parseLong(
getProperty(LightstepConfig.DEADLINE_MILLIS,
String.valueOf(LightstepConfig.DEFAULT_DEADLINE_MILLIS)));
builder.accessToken = getProperty(LightstepConfig.ACCESS_TOKEN, "");
builder.serviceName = getProperty(LightstepConfig.SERVICE_NAME,
LightstepConfig.defaultServiceName());
builder.serviceVersion = getProperty(LightstepConfig.SERVICE_VERSION, null);
// Deprecated LightstepConfig.COMPONENT_NAME should be removed in next releases
builder.serviceName = getProperty(LightstepConfig.COMPONENT_NAME, builder.serviceName);
return builder;
}
/**
* Sets the host to which the tracer will send data. If not set, will default to the primary
* Lightstep collector address.
*
* @param collectorHost The hostname for the Lightstep collector.
* @return this builder's instance
* @throws IllegalArgumentException If the collectorHost argument is invalid.
*/
public Builder setCollectorHost(String collectorHost) {
if (collectorHost == null || "".equals(collectorHost.trim())) {
throw new IllegalArgumentException("Invalid collector host: " + collectorHost);
}
this.collectorHost = collectorHost;
return this;
}
/**
* Sets the port to which the tracer will send data. If not set, will default to {@code
* DEFAULT_SECURE_PORT} when the protocol is https and {@code DEFAULT_PLAINTEXT_PORT} when the
* protocol is http.
*
* @param collectorPort The port for the Lightstep collector.
* @return this builder's instance
* @throws IllegalArgumentException If the collectorPort is invalid.
*/
public Builder setCollectorPort(int collectorPort) {
if (collectorPort <= 0) {
throw new IllegalArgumentException("Invalid collector port: " + collectorPort);
}
this.collectorPort = collectorPort;
return this;
}
/**
* Sets the protocol which will be used when sending data to the tracer.
*
* @param protocol Either 'http' or 'https'
* @return this builder's instance
* @throws IllegalArgumentException If the protocol argument is invalid.
*/
public Builder setCollectorProtocol(String protocol) {
if (!LightstepConfig.PROTOCOL_HTTPS.equals(protocol) && !LightstepConfig.PROTOCOL_HTTP
.equals(protocol)) {
throw new IllegalArgumentException("Invalid protocol for collector: " + protocol);
}
this.collectorProtocol = protocol;
return this;
}
/**
* Overrides the default deadlineMillis with the provided value.
*
* @param deadlineMillis The maximum amount of time the tracer should wait for a response from
* the collector when sending a report.
* @return this builder's instance
*/
public Builder setDeadlineMillis(long deadlineMillis) {
this.deadlineMillis = deadlineMillis;
return this;
}
/**
* Sets the token for Lightstep access
*
* @param accessToken Your specific token for Lightstep access.
* @return this builder's instance
*/
public Builder setAccessToken(String accessToken) {
this.accessToken = accessToken;
return this;
}
/**
* Sets the DNS service used to lookup IP addresses for hostnames when using the OkHttp
* transport. If not set, the default DNS service used by OkHttp will be used.
*
* @param okHttpDns the Dns service object.
* @return this builder's instance
*/
public Builder setOkHttpDns(OkHttpDns okHttpDns) {
if (okHttpDns == null) {
throw new IllegalArgumentException("dns cannot be null");
} else {
this.okHttpDns = okHttpDns;
return this;
}
}
/**
* Sets the service name attribute. If not set, will default to the Java runtime command.
*
* Deprecated, use {@link #setServiceName(String)} instead.
*
* @param name The name of the service being traced.
* @return this builder's instance
*/
@Deprecated
public Builder setComponentName(String name) {
this.serviceName = name;
return this;
}
/**
* Sets the service name attribute. If not set, will default to the Java runtime command.
*
* @param name The name of the service being traced.
* @return this builder's instance
*/
public Builder setServiceName(String name) {
this.serviceName = name;
return this;
}
/**
* Sets the service version attribute. Optional value.
*
* @param version The version of the service being traced.
* @return this builder's instance
*/
public Builder setServiceVersion(String version) {
this.serviceVersion = version;
return this;
}
/**
* If not set, provides a default value for the service name.
*/
private void setDefaultServiceName() {
if (serviceName == null) {
setServiceName(LightstepConfig.defaultServiceName());
}
}
private void defaultDeadlineMillis() {
if (deadlineMillis < 0) {
deadlineMillis = LightstepConfig.DEFAULT_DEADLINE_MILLIS;
}
}
private int getPort() {
if (collectorPort > 0) {
return collectorPort;
} else if (collectorProtocol.equals(LightstepConfig.PROTOCOL_HTTPS)) {
return LightstepConfig.DEFAULT_SECURE_PORT;
} else {
return LightstepConfig.DEFAULT_PLAINTEXT_PORT;
}
}
@VisibleForTesting
URL getCollectorUrl() throws MalformedURLException {
int port = getPort();
return new URL(collectorProtocol, collectorHost, port, PATH);
}
/**
* Constructs a new instance of the exporter based on the builder's values.
*
* @return a new exporter's instance
* @throws MalformedURLException if an unknown protocol or the port is a negative number
*/
public LightstepSpanExporter build() throws MalformedURLException {
defaultDeadlineMillis();
setDefaultServiceName();
return new LightstepSpanExporter(
getCollectorUrl(), deadlineMillis, accessToken, okHttpDns, serviceName, serviceVersion);
}
/**
* Installs exporter into tracer SDK provider with batching span processor.
*
* @param tracerSdkProvider tracer SDK provider
*/
public void install(TracerSdkProvider tracerSdkProvider) throws MalformedURLException {
BatchSpanProcessor spansProcessor = BatchSpanProcessor.newBuilder(this.build()).build();
tracerSdkProvider.addSpanProcessor(spansProcessor);
}
/**
* Installs exporter into tracer SDK default provider with batching span processor.
*/
public void install() throws MalformedURLException {
BatchSpanProcessor spansProcessor = BatchSpanProcessor.newBuilder(this.build()).build();
OpenTelemetrySdk.getTracerProvider().addSpanProcessor(spansProcessor);
}
private static String getProperty(String name, String defaultValue) {
String val = System.getProperty(name, System.getenv(name));
if (val == null || val.isEmpty()) {
return defaultValue;
}
return val;
}
@VisibleForTesting
long getDeadlineMillis() {
return deadlineMillis;
}
@VisibleForTesting
String getAccessToken() {
return accessToken;
}
@VisibleForTesting
String getServiceName() {
return serviceName;
}
@VisibleForTesting
String getServiceVersion() {
return serviceVersion;
}
}
@VisibleForTesting
URL getCollectorUrl() {
return collectorUrl;
}
@VisibleForTesting
String getServiceName() {
return serviceName;
}
@VisibleForTesting
String getServiceVersion() {
return serviceVersion;
}
@VisibleForTesting
Auth.Builder getAuth() {
return auth;
}
@VisibleForTesting
List getLsSpanAttributes() {
return lsSpanAttributes;
}
@VisibleForTesting
Reporter getReporter() {
return reporter;
}
@VisibleForTesting
OkHttpClient getClient() {
return client;
}
}