Skip to content

Commit

Permalink
allow the various globals in the OpenTelemetry instance to be set at …
Browse files Browse the repository at this point in the history
…runtime.
  • Loading branch information
jwatson committed Dec 30, 2019
1 parent 47ed1b1 commit 4d6f4f5
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 61 deletions.
142 changes: 81 additions & 61 deletions api/src/main/java/io/opentelemetry/OpenTelemetry.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,24 @@
import io.opentelemetry.distributedcontext.DistributedContextManager;
import io.opentelemetry.distributedcontext.spi.DistributedContextManagerProvider;
import io.opentelemetry.metrics.DefaultMeterFactory;
import io.opentelemetry.metrics.DefaultMeterFactoryProvider;
import io.opentelemetry.metrics.Meter;
import io.opentelemetry.metrics.MeterFactory;
import io.opentelemetry.metrics.spi.MeterFactoryProvider;
import io.opentelemetry.trace.DefaultTracerFactory;
import io.opentelemetry.trace.DefaultTracerFactoryProvider;
import io.opentelemetry.trace.Tracer;
import io.opentelemetry.trace.TracerFactory;
import io.opentelemetry.trace.spi.TracerFactoryProvider;
import java.util.ServiceLoader;
import javax.annotation.Nullable;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;
import javax.annotation.concurrent.ThreadSafe;

/**
* This class provides a static global accessor for telemetry objects {@link Tracer}, {@link Meter}
* and {@link DistributedContextManager}.
*
* <p>The telemetry objects are lazy-loaded singletons resolved via {@link ServiceLoader} mechanism.
* <p>By default, the telemetry objects are lazy-loaded singletons resolved via the Java SPI {@link
* ServiceLoader} mechanism. This can be circumvented by calling setInstance methods on a individual
* components before any one has attempted to access the default singletons.
*
* @see TracerFactory
* @see MeterFactoryProvider
Expand All @@ -46,22 +46,29 @@
@ThreadSafe
public final class OpenTelemetry {

@Nullable private static volatile OpenTelemetry instance;
private static final Logger logger = Logger.getLogger(OpenTelemetry.class.getName());

private final TracerFactory tracerFactory;
private final MeterFactory meterFactory;
private final DistributedContextManager contextManager;
private static final OpenTelemetry instance = new OpenTelemetry();

private final AtomicReference<TracerFactory> tracerFactory = new AtomicReference<>();
private final AtomicReference<MeterFactory> meterFactory = new AtomicReference<>();
private final AtomicReference<DistributedContextManager> contextManager = new AtomicReference<>();

private OpenTelemetry() {}

/**
* Returns a singleton {@link TracerFactory}.
*
* @return registered TracerFactory of default via {@link DefaultTracerFactory#getInstance()}.
* @return registered TracerFactory or default via {@link DefaultTracerFactory#getInstance()}.
* @throws IllegalStateException if a specified TracerFactory (via system properties) could not be
* found.
* @since 0.1.0
*/
public static TracerFactory getTracerFactory() {
return getInstance().tracerFactory;
if (instance.tracerFactory.get() == null) {
setTracerFactory(SpiOpenTelemetryProvider.makeSpiTracerFactory());
}
return instance.tracerFactory.get();
}

/**
Expand All @@ -73,7 +80,10 @@ public static TracerFactory getTracerFactory() {
* @since 0.1.0
*/
public static MeterFactory getMeterFactory() {
return getInstance().meterFactory;
if (instance.meterFactory.get() == null) {
setMeterFactory(SpiOpenTelemetryProvider.makeSpiMeterFactory());
}
return instance.meterFactory.get();
}

/**
Expand All @@ -86,68 +96,78 @@ public static MeterFactory getMeterFactory() {
* @since 0.1.0
*/
public static DistributedContextManager getDistributedContextManager() {
return getInstance().contextManager;
if (instance.contextManager.get() == null) {
setDistributedContextManager(SpiOpenTelemetryProvider.makeSpiContextManager());
}
return instance.contextManager.get();
}

/** Lazy loads an instance. */
private static OpenTelemetry getInstance() {
if (instance == null) {
synchronized (OpenTelemetry.class) {
if (instance == null) {
instance = new OpenTelemetry();
}
}
/**
* Assigns the global singleton MeterFactory instance. This can be done exactly once; subsequent
* calls will be ignored.
*
* <p>Note, if calls to the static accessor for the MeterFactory are made before this is called,
* the global singleton will be initialized with an SPI-provided implementation.
*
* <p>Therefore, if you wish to use a non-SPI provided instance, you must make sure to call this
* method before any instrumentation will have forced the SPI implementation to be loaded.
*
* @param meterFactory A fully configured and ready to operate MeterFactory implementation.
*/
public static void setMeterFactory(MeterFactory meterFactory) {
if (!instance.meterFactory.compareAndSet(null, meterFactory)) {
logger.warning(
"The global OpenTelemetry MeterFactory instance has already been set. "
+ "Ignoring this assignment");
}
return instance;
}

private OpenTelemetry() {
TracerFactoryProvider tracerFactoryProvider = loadSpi(TracerFactoryProvider.class);
this.tracerFactory =
tracerFactoryProvider != null
? tracerFactoryProvider.create()
: DefaultTracerFactoryProvider.getInstance().create();

MeterFactoryProvider meterFactoryProvider = loadSpi(MeterFactoryProvider.class);
meterFactory =
meterFactoryProvider != null
? meterFactoryProvider.create()
: DefaultMeterFactoryProvider.getInstance().create();
DistributedContextManagerProvider contextManagerProvider =
loadSpi(DistributedContextManagerProvider.class);
contextManager =
contextManagerProvider != null
? contextManagerProvider.create()
: DefaultDistributedContextManager.getInstance();
/**
* Assigns the global singleton TracerFactory instance. This can be done exactly once; subsequent
* calls will be ignored.
*
* <p>Note, if calls to the static accessor for the TracerFactory are made before this is called,
* the global singleton will be initialized with an SPI-provided implementation.
*
* <p>Therefore, if you wish to use a non-SPI provided instance, you must make sure to call this
* method before any instrumentation will have forced the SPI implementation to be loaded.
*
* @param tracerFactory A fully configured and ready to operate {@link TracerFactory}
* implementation.
*/
public static void setTracerFactory(TracerFactory tracerFactory) {
if (!instance.tracerFactory.compareAndSet(null, tracerFactory)) {
logger.warning(
"The global OpenTelemetry TracerFactory instance has already been set. "
+ "Ignoring this assignment");
}
}

/**
* Load provider class via {@link ServiceLoader}. A specific provider class can be requested via
* setting a system property with FQCN.
* Assigns the global singleton DistributedContextManager instance. This can be done exactly once;
* subsequent calls will be ignored.
*
* <p>Note, if calls to the static accessor for the DistributedContextManager are made before this
* is called, the global singleton will be initialized with an SPI-provided implementation.
*
* @param providerClass a provider class
* @param <T> provider type
* @return a provider or null if not found
* @throws IllegalStateException if a specified provider is not found
* <p>Therefore, if you wish to use a non-SPI provided instance, you must make sure to call this
* method before any instrumentation will have forced the SPI implementation to be loaded.
*
* @param contextManager A fully configured and ready to operate DistributedContextManager
* implementation.
*/
@Nullable
private static <T> T loadSpi(Class<T> providerClass) {
String specifiedProvider = System.getProperty(providerClass.getName());
ServiceLoader<T> providers = ServiceLoader.load(providerClass);
for (T provider : providers) {
if (specifiedProvider == null || specifiedProvider.equals(provider.getClass().getName())) {
return provider;
}
}
if (specifiedProvider != null) {
throw new IllegalStateException(
String.format("Service provider %s not found", specifiedProvider));
public static void setDistributedContextManager(DistributedContextManager contextManager) {
if (!instance.contextManager.compareAndSet(null, contextManager)) {
logger.warning(
"The global OpenTelemetry DistributedContextManager instance has already been set. "
+ "Ignoring this assignment");
}
return null;
}

// for testing
// for testing only
static void reset() {
instance = null;
instance.meterFactory.set(null);
instance.tracerFactory.set(null);
instance.contextManager.set(null);
}
}
82 changes: 82 additions & 0 deletions api/src/main/java/io/opentelemetry/SpiOpenTelemetryProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2019, OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.opentelemetry;

import io.opentelemetry.distributedcontext.DefaultDistributedContextManager;
import io.opentelemetry.distributedcontext.DistributedContextManager;
import io.opentelemetry.distributedcontext.spi.DistributedContextManagerProvider;
import io.opentelemetry.metrics.DefaultMeterFactoryProvider;
import io.opentelemetry.metrics.MeterFactory;
import io.opentelemetry.metrics.spi.MeterFactoryProvider;
import io.opentelemetry.trace.DefaultTracerFactoryProvider;
import io.opentelemetry.trace.TracerFactory;
import io.opentelemetry.trace.spi.TracerFactoryProvider;
import java.util.ServiceLoader;
import javax.annotation.Nullable;

/** Creates an OpenTelemetry instance via Java's built-in {@link ServiceLoader} SPI capabilities. */
public class SpiOpenTelemetryProvider {

private SpiOpenTelemetryProvider() {}

/**
* Load provider class via {@link ServiceLoader}. A specific provider class can be requested via
* setting a system property with FQCN.
*
* @param providerClass a provider class
* @param <T> provider type
* @return a provider or null if not found
* @throws IllegalStateException if a specified provider is not found
*/
@Nullable
private static <T> T loadSpi(Class<T> providerClass) {
String specifiedProvider = System.getProperty(providerClass.getName());
ServiceLoader<T> providers = ServiceLoader.load(providerClass);
for (T provider : providers) {
if (specifiedProvider == null || specifiedProvider.equals(provider.getClass().getName())) {
return provider;
}
}
if (specifiedProvider != null) {
throw new IllegalStateException(
String.format("Service provider %s not found", specifiedProvider));
}
return null;
}

static TracerFactory makeSpiTracerFactory() {
TracerFactoryProvider tracerFactoryProvider = loadSpi(TracerFactoryProvider.class);
return tracerFactoryProvider != null
? tracerFactoryProvider.create()
: DefaultTracerFactoryProvider.getInstance().create();
}

static MeterFactory makeSpiMeterFactory() {
MeterFactoryProvider meterFactoryProvider = loadSpi(MeterFactoryProvider.class);
return meterFactoryProvider != null
? meterFactoryProvider.create()
: DefaultMeterFactoryProvider.getInstance().create();
}

static DistributedContextManager makeSpiContextManager() {
DistributedContextManagerProvider contextManagerProvider =
loadSpi(DistributedContextManagerProvider.class);
return contextManagerProvider != null
? contextManagerProvider.create()
: DefaultDistributedContextManager.getInstance();
}
}
32 changes: 32 additions & 0 deletions api/src/test/java/io/opentelemetry/OpenTelemetryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;

import io.opentelemetry.context.Scope;
import io.opentelemetry.context.propagation.BinaryFormat;
Expand All @@ -41,6 +42,7 @@
import io.opentelemetry.metrics.MeterFactory;
import io.opentelemetry.metrics.spi.MeterFactoryProvider;
import io.opentelemetry.trace.DefaultTracer;
import io.opentelemetry.trace.DefaultTracerFactory;
import io.opentelemetry.trace.Span;
import io.opentelemetry.trace.SpanContext;
import io.opentelemetry.trace.Tracer;
Expand Down Expand Up @@ -93,6 +95,30 @@ public void testDefault() {
.isEqualTo(OpenTelemetry.getDistributedContextManager());
}

@Test
public void testSetInstanceBeforeSingletonAccess() {
TracerFactory tracerFactory = mock(TracerFactory.class);
MeterFactory meterFactory = mock(MeterFactory.class);
DistributedContextManager contextManager = mock(DistributedContextManager.class);
OpenTelemetry.setTracerFactory(tracerFactory);
OpenTelemetry.setMeterFactory(meterFactory);
OpenTelemetry.setDistributedContextManager(contextManager);
assertThat(OpenTelemetry.getTracerFactory()).isSameInstanceAs(tracerFactory);
assertThat(OpenTelemetry.getMeterFactory()).isSameInstanceAs(meterFactory);
assertThat(OpenTelemetry.getDistributedContextManager()).isSameInstanceAs(contextManager);
}

@Test
public void testSetInstanceOneTimeOnly() {
TracerFactory tracerFactory = OpenTelemetry.getTracerFactory();
assertThat(tracerFactory).isInstanceOf(DefaultTracerFactory.class);

// this call should be ignored, since we've already accessed the singletons.
OpenTelemetry.setTracerFactory(mock(TracerFactory.class));

assertThat(OpenTelemetry.getTracerFactory()).isSameInstanceAs(tracerFactory);
}

@Test
public void testTracerLoadArbitrary() throws IOException {
File serviceFile =
Expand Down Expand Up @@ -225,6 +251,7 @@ private static File createService(Class<?> service, Class<?>... impls) throws IO
}

public static class SecondTracerFactory extends FirstTracerFactory {

@Override
public Tracer get(String instrumentationName) {
return new SecondTracerFactory();
Expand All @@ -242,6 +269,7 @@ public TracerFactory create() {
}

public static class FirstTracerFactory implements Tracer, TracerFactory, TracerFactoryProvider {

@Override
public Tracer get(String instrumentationName) {
return new FirstTracerFactory();
Expand Down Expand Up @@ -289,6 +317,7 @@ public TracerFactory create() {
}

public static class SecondMeterFactory extends FirstMeterFactory {

@Override
public Meter get(String instrumentationName) {
return new SecondMeterFactory();
Expand All @@ -306,6 +335,7 @@ public MeterFactory create() {
}

public static class FirstMeterFactory implements Meter, MeterFactoryProvider, MeterFactory {

@Override
public MeterFactory create() {
return new FirstMeterFactory();
Expand Down Expand Up @@ -409,6 +439,7 @@ public Meter get(String instrumentationName, String instrumentationVersion) {
}

public static class SecondDistributedContextManager extends FirstDistributedContextManager {

@Override
public DistributedContextManager create() {
return new SecondDistributedContextManager();
Expand All @@ -417,6 +448,7 @@ public DistributedContextManager create() {

public static class FirstDistributedContextManager
implements DistributedContextManager, DistributedContextManagerProvider {

@Override
public DistributedContextManager create() {
return new FirstDistributedContextManager();
Expand Down

0 comments on commit 4d6f4f5

Please sign in to comment.