Skip to content

Commit

Permalink
TracerConverter (Wrapper) support (#8)
Browse files Browse the repository at this point in the history
* Check license header as part of build.

* Initial rough version of Wrapper support (without @priority order).

* Isolate Resolver concept from Wrapper changes.

* Add a short notice about tracer wrappers.

* Move warning (about wrapping) from TracerResolver into Wrappers class.

* Add missing license header.

* Add braces around if.

* Rename TracerWrapperProvider to TracerConverter.

* Outline potential 'wrapper' use of the tracer converter

* Update JavaDoc example when converter can recieve a null tracer.

* Don't call converter if resolved Tracer is null.

* Just break out of the loop (thanks @objectiser)

* Document priority of TracerResolver.

* Document priority concept in javadoc of TracerConverter as well.

* Don't begin loop in the first place if resolved == null.

* Fix illegal JavaDoc.

* Add unit-tests for TracerConverter.

Including priority-order, returning nulls and throwing exceptions.
  • Loading branch information
sjoerdtalsma authored May 1, 2017
1 parent 01c4718 commit 8eb083e
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 9 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,22 @@ to find declared `TracerResolver` implementations to resolve a Tracer.
If no `Tracer` is resolved by any `TracerResolver`, a [ServiceLoader lookup][serviceloader] for a declared
`Tracer` class is used as _fallback_ resolver.

## Tracer converters

Each resolved tracer is passed to _all_ `TracerConverter` instances that were found.

A tracer converter can be useful for _automatically wrapping_ the resolved `Tracer`:
```java
public final class FooWrapperConverter implements TracerConverter {
public Tracer convert(Tracer existingTracer) {
return new FooTracerWrapper(existingTracer);
}
}
```

## Priority

If multiple `TracerResolver` or `Tracer` implementations are found,
If multiple `TracerResolver`, `TracerConverter` or `Tracer` implementations are found,
they are checked for presence of the [`@Priority`][priority] annotation
on their class or superclasses.
The priority is applied as follows:
Expand All @@ -28,6 +41,7 @@ The priority is applied as follows:

The order of objects with equal (implicit) priority is undefined.


[ci-img]: https://img.shields.io/travis/opentracing-contrib/java-tracerresolver/master.svg
[ci]: https://travis-ci.org/opentracing-contrib/java-tracerresolver
[maven-img]: https://img.shields.io/maven-central/v/io.opentracing.contrib/opentracing-tracerresolver.svg
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2017 The OpenTracing 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.opentracing.contrib.tracerresolver;

import io.opentracing.Tracer;

/**
* Function converting an existing {@link Tracer}.
* <p>
* This can be useful for <em>wrapping</em> tracers:
* <pre><code>
* public final class FooWrapperConverter implements TracerConverter {
* public Tracer convert(Tracer existingTracer) {
* return new FooTracerWrapper(existingTracer);
* }
* }
* </code></pre>
* <p>
* If there are multiple {@linkplain TracerConverter} implementations resolved,
* they will be applied in the order of their {@literal @}Priority annotation:
* <ol>
* <li>First, non-negative priority is applied in natural order (e.g. {@code 0}, {@code 1}, {@code 2}, ...).</li>
* <li>Next, objects without <code>{@literal @}Priority</code> annotation are applied
* by assigning a <em>default priority</em> of {@link Integer#MAX_VALUE}.</li>
* <li>Finally, negative priority is applied in reverse-natural order (e.g. {@code -1}, {@code -2}, {@code -3}, ...).</li>
* </ol>
* <p>
* The order of objects with equal (implicit) priority is undefined.
*
* @author Sjoerd Talsma
*/
public interface TracerConverter {

/**
* Function that converts a {@link Tracer}.
* <p>
* It may either manipulate the tracer or return an entirely new {@linkplain Tracer} instance.
*
* @param existingTracer The existing tracer to be converted.
* @return The converted tracer.
*/
Tracer convert(Tracer existingTracer);

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@
* <p>
* If no {@link TracerResolver} implementations are found, the {@link #resolveTracer()} method will fallback to
* {@link ServiceLoader} lookup of the {@link Tracer} service itself.
* <p>
* Available {@link TracerConverter} implementations are applied to the resolved {@link Tracer} instance.
*
* @author Sjoerd Talsma
*/
public abstract class TracerResolver {
private static final Logger LOGGER = Logger.getLogger(TracerResolver.class.getName());
private static final ServiceLoader<TracerResolver> RESOLVERS = ServiceLoader.load(TracerResolver.class);
private static final ServiceLoader<TracerConverter> CONVERTERS = ServiceLoader.load(TracerConverter.class);
private static final ServiceLoader<Tracer> FALLBACK = ServiceLoader.load(Tracer.class);

/**
Expand All @@ -54,19 +57,18 @@ public abstract class TracerResolver {
public static Tracer resolveTracer() {
for (TracerResolver resolver : prioritize(RESOLVERS)) {
try {
Tracer tracer = resolver.resolve();
Tracer tracer = convert(resolver.resolve());
if (tracer != null) {
LOGGER.log(Level.FINER, "Resolved tracer: {0}.", tracer);
return tracer;
return logResolved(tracer);
}
} catch (RuntimeException rte) {
LOGGER.log(Level.WARNING, "Error resolving tracer using " + resolver + ": " + rte.getMessage(), rte);
}
}
for (Tracer tracer : prioritize(FALLBACK)) {
tracer = convert(tracer);
if (tracer != null) {
LOGGER.log(Level.FINER, "Resolved tracer: {0}.", tracer);
return tracer;
return logResolved(tracer);
}
}
LOGGER.log(Level.FINEST, "No tracer was resolved.");
Expand All @@ -78,8 +80,30 @@ public static Tracer resolveTracer() {
*/
public static void reload() {
RESOLVERS.reload();
CONVERTERS.reload();
FALLBACK.reload();
LOGGER.log(Level.FINER, "Resolvers were reloaded.");
LOGGER.log(Level.FINER, "Tracer resolvers were reloaded.");
}

private static Tracer convert(Tracer resolved) {
if (resolved != null) {
for (TracerConverter converter : prioritize(CONVERTERS)) {
try {
Tracer converted = converter.convert(resolved);
LOGGER.log(Level.FINEST, "Converted {0} using {1}: {2}.", new Object[]{resolved, converter, converted});
resolved = converted;
} catch (RuntimeException rte) {
LOGGER.log(Level.WARNING, "Error converting " + resolved + " with " + converter + ": " + rte.getMessage(), rte);
}
if (resolved == null) break;
}
}
return resolved;
}

private static Tracer logResolved(Tracer resolvedTracer) {
LOGGER.log(Level.FINER, "Resolved tracer: {0}.", resolvedTracer);
return resolvedTracer;
}

}
39 changes: 39 additions & 0 deletions src/test/java/io/opentracing/contrib/tracerresolver/Mocks.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
import io.opentracing.Tracer;
import io.opentracing.mock.MockTracer;

import javax.annotation.Priority;
import java.util.ArrayList;
import java.util.List;

public final class Mocks {
static final List<Class<?>> calledConverterTypes = new ArrayList<Class<?>>();

public static class FallbackTracer extends MockTracer {
}
Expand All @@ -40,4 +45,38 @@ protected Tracer resolve() {
}
}

@Priority(1)
public static class HighPriorityThrowingResolver extends TracerResolver {
@Override
protected Tracer resolve() {
throw new IllegalStateException("Can't resolve tracer (missing configuration?)");
}
}

public static class IdentityConverter implements TracerConverter {
@Override
public Tracer convert(Tracer existingTracer) {
calledConverterTypes.add(getClass());
return existingTracer;
}
}

@Priority(5)
public static class Prio5_ThrowingConverter implements TracerConverter {
@Override
public Tracer convert(Tracer existingTracer) {
calledConverterTypes.add(getClass());
throw new UnsupportedOperationException("Conversion of " + existingTracer + " not supported.");
}
}

@Priority(10)
public static class Prio10_ConvertToNull implements TracerConverter {
@Override
public Tracer convert(Tracer existingTracer) {
calledConverterTypes.add(getClass());
return null;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2017 The OpenTracing 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.opentracing.contrib.tracerresolver;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.File;
import java.io.IOException;

import static io.opentracing.contrib.tracerresolver.TracerResolverTest.writeServiceFile;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

public class TracerConverterTest {
private static final File SERVICES_DIR = new File("target/test-classes/META-INF/services/");

@After
public void cleanServiceFiles() throws IOException {
new File(SERVICES_DIR, TracerResolver.class.getName()).delete();
new File(SERVICES_DIR, TracerConverter.class.getName()).delete();
}

@Before
@After
public void resetTracerResolver() {
Mocks.calledConverterTypes.clear();
TracerResolver.reload();
}

@Test
public void testConverterNotCalledWhenNothingRegistered() throws IOException {
writeServiceFile(TracerConverter.class, Mocks.IdentityConverter.class);
assertThat("No tracer should be resolved", TracerResolver.resolveTracer(), is(nullValue()));
assertThat("No converter should be called", Mocks.calledConverterTypes, hasSize(0));
}

@Test
public void testTrivialConverterExample() throws IOException {
writeServiceFile(TracerResolver.class, Mocks.MockTracerResolver.class);
writeServiceFile(TracerConverter.class, Mocks.IdentityConverter.class);

assertThat("Tracer must be resolved", TracerResolver.resolveTracer(), instanceOf(Mocks.ResolvedTracer.class));
assertThat("Converter must be called", Mocks.calledConverterTypes, contains((Class) Mocks.IdentityConverter.class));
}

@Test
public void testConverterNotCalledWhenProviderReturnsNull() throws IOException {
writeServiceFile(TracerResolver.class, Mocks.NullTracerResolver.class);
writeServiceFile(TracerConverter.class, Mocks.IdentityConverter.class);

assertThat("Resolved tracer", TracerResolver.resolveTracer(), is(nullValue()));
assertThat("No converter should be called", Mocks.calledConverterTypes, hasSize(0));
}

@Test
public void testConverterThrowingException() throws IOException {
writeServiceFile(TracerResolver.class, Mocks.MockTracerResolver.class);
// although identity converter is listed first, the @Priority(5) should let throwing converter be called first!
writeServiceFile(TracerConverter.class, Mocks.IdentityConverter.class, Mocks.Prio5_ThrowingConverter.class);

assertThat("Resolved tracer", TracerResolver.resolveTracer(), instanceOf(Mocks.ResolvedTracer.class));
assertThat("Continue after exception", Mocks.calledConverterTypes, hasSize(2));
assertThat("Predictable order", Mocks.calledConverterTypes,
contains((Class) Mocks.Prio5_ThrowingConverter.class, Mocks.IdentityConverter.class));
}

@Test
public void testconvertToNull() throws IOException {
writeServiceFile(TracerResolver.class, Mocks.MockTracerResolver.class);
// although identity converter is listed first, the @Priority(10) should let convertToNull be called first!
writeServiceFile(TracerConverter.class, Mocks.IdentityConverter.class, Mocks.Prio10_ConvertToNull.class);

assertThat("Tracer after conversion(s)", TracerResolver.resolveTracer(), is(nullValue()));
assertThat("Second converter shouldn't be called with <null>", Mocks.calledConverterTypes, hasSize(1));
assertThat("Prioritized converter", Mocks.calledConverterTypes, contains((Class) Mocks.Prio10_ConvertToNull.class));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,21 @@ public void testResolvingNullWithoutFallback() throws IOException {
assertThat(TracerResolver.resolveTracer(), is(nullValue()));
}

private static <SVC> void writeServiceFile(Class<SVC> service, Class<? extends SVC> implementation) throws IOException {
@Test
public void testSkipResolverThrowingException() throws IOException {
writeServiceFile(TracerResolver.class, Mocks.HighPriorityThrowingResolver.class, Mocks.MockTracerResolver.class);
assertThat(TracerResolver.resolveTracer(), is(instanceOf(Mocks.ResolvedTracer.class)));
}

static <SVC> void writeServiceFile(Class<SVC> service, Class<?>... implementations) throws IOException {
SERVICES_DIR.mkdirs();
File serviceFile = new File(SERVICES_DIR, service.getName());
if (serviceFile.isFile()) serviceFile.delete();
PrintWriter writer = new PrintWriter(new FileWriter(serviceFile));
try {
writer.println(implementation.getName());
for (Class<?> implementation : implementations) {
writer.println(implementation.getName());
}
} finally {
writer.close();
}
Expand Down

0 comments on commit 8eb083e

Please sign in to comment.