diff --git a/opentracing-span-propagation/src/main/java/nl/talsmasoftware/context/opentracing/ContextScopeManager.java b/opentracing-span-propagation/src/main/java/nl/talsmasoftware/context/opentracing/ContextScopeManager.java
new file mode 100644
index 00000000..3ab63e27
--- /dev/null
+++ b/opentracing-span-propagation/src/main/java/nl/talsmasoftware/context/opentracing/ContextScopeManager.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2016-2019 Talsma ICT
+ *
+ * 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 nl.talsmasoftware.context.opentracing;
+
+import io.opentracing.Scope;
+import io.opentracing.ScopeManager;
+import io.opentracing.Span;
+import nl.talsmasoftware.context.Context;
+import nl.talsmasoftware.context.ContextManager;
+import nl.talsmasoftware.context.threadlocal.AbstractThreadLocalContext;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Our own implementation of the opentracing {@linkplain ScopeManager}.
+ *
+ *
+ * Manages opentracing {@linkplain Scope} and allows it to be nested within another active scope,
+ * taking care to restore the previous value when closing an active scope.
+ *
+ *
+ * This manager is based on our {@linkplain nl.talsmasoftware.context.threadlocal.AbstractThreadLocalContext}
+ * implementation. Compared to the 'standard' {@linkplain io.opentracing.util.ThreadLocalScopeManager}
+ * this implementation has the following advantages:
+ *
+ * - Close is explicitly idempotent; closing more than once has no additional side-effects
+ * (even when finishOnClose is set to {@code true}).
+ * - More predictable behaviour for out-of-order closing of scopes.
+ * Although this is explicitly unsupported by the opentracing specification,
+ * we think having consistent and predictable behaviour is an advantage.
+ *
- Support for {@link nl.talsmasoftware.context.observer.ContextObserver}.
+ * See https://github.com/opentracing/opentracing-java/issues/334 explicitly wanting this.
+ *
+ *
+ *
+ * Please note that this scope manager is not somehow automatically enabled.
+ * You will have to provide an instance to your tracer of choice when initializing it.
+ *
+ *
+ * The active span that is automatically propagated when using this
+ * {@code opentracing-span-propagation} library in combination with
+ * the context aware support classes is from the registered ScopeManager
+ * from the {@linkplain io.opentracing.util.GlobalTracer}.
+ *
+ * @since 1.0.6
+ */
+public class ContextScopeManager implements ScopeManager, ContextManager {
+ /**
+ * Makes the given span the new active span.
+ *
+ * @param span The span to become the active span.
+ * @param finishSpanOnClose Whether the span should automatically finish when closing the resulting scope.
+ * @return The new active scope (must be closed from the same thread).
+ */
+ @Override
+ public Scope activate(Span span, boolean finishSpanOnClose) {
+ return new ThreadLocalSpanContext(getClass(), span, finishSpanOnClose);
+ }
+
+ /**
+ * The currently active {@link Scope} containing the active span {@link Scope#span()}.
+ *
+ * @return the active scope, or {@code null} if none could be found.
+ */
+ @Override
+ public Scope active() {
+ return ThreadLocalSpanContext.current();
+ }
+
+ /**
+ * Initializes a new context for the given {@linkplain Span}.
+ *
+ * @param value The span to activate.
+ * @return The new active 'Scope'.
+ * @see #activate(Span, boolean)
+ */
+ @Override
+ public Context initializeNewContext(Span value) {
+ return new ThreadLocalSpanContext(getClass(), value, false);
+ }
+
+ /**
+ * @return The active span context (this is identical to the active scope).
+ * @see #active()
+ */
+ @Override
+ public Context getActiveContext() {
+ return ThreadLocalSpanContext.current();
+ }
+
+ /**
+ * @return String representation for this context manager.
+ */
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+
+ private static final class ThreadLocalSpanContext extends AbstractThreadLocalContext implements Scope {
+ private final AtomicBoolean finishOnClose;
+
+ private ThreadLocalSpanContext(Class extends ContextManager super Span>> contextManagerType, Span newValue, boolean finishOnClose) {
+ super(contextManagerType, newValue);
+ this.finishOnClose = new AtomicBoolean(finishOnClose);
+ }
+
+ private static ThreadLocalSpanContext current() {
+ return current(ThreadLocalSpanContext.class);
+ }
+
+ @Override
+ public Span span() {
+ return value;
+ }
+
+ @Override
+ public void close() {
+ super.close();
+ if (finishOnClose.compareAndSet(true, false) && value != null) {
+ value.finish();
+ }
+ }
+ }
+}
diff --git a/opentracing-span-propagation/src/test/java/nl/talsmasoftware/context/opentracing/ContextScopeManagerObserver.java b/opentracing-span-propagation/src/test/java/nl/talsmasoftware/context/opentracing/ContextScopeManagerObserver.java
new file mode 100644
index 00000000..aed7daba
--- /dev/null
+++ b/opentracing-span-propagation/src/test/java/nl/talsmasoftware/context/opentracing/ContextScopeManagerObserver.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2016-2019 Talsma ICT
+ *
+ * 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 nl.talsmasoftware.context.opentracing;
+
+import io.opentracing.Span;
+import io.opentracing.mock.MockSpan;
+import nl.talsmasoftware.context.ContextManager;
+import nl.talsmasoftware.context.observer.ContextObserver;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.util.Collections.synchronizedList;
+
+public class ContextScopeManagerObserver implements ContextObserver {
+ static final List observed = synchronizedList(new ArrayList());
+
+ @Override
+ public Class extends ContextManager> getObservedContextManager() {
+ return ContextScopeManager.class;
+ }
+
+ @Override
+ public void onActivate(Span activatedContextValue, Span previousContextValue) {
+ observed.add(new Event(Event.Type.ACTIVATE, activatedContextValue));
+ }
+
+ @Override
+ public void onDeactivate(Span deactivatedContextValue, Span restoredContextValue) {
+ observed.add(new Event(Event.Type.DEACTIVATE, deactivatedContextValue));
+ }
+
+ static class Event {
+ enum Type {ACTIVATE, DEACTIVATE}
+
+ final Thread thread;
+ final Type type;
+ final Span value;
+
+ Event(Type type, Span value) {
+ this.thread = Thread.currentThread();
+ this.type = type;
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return "Event{" + type + ", thread=" + thread.getName() + ", span=" + value + '}';
+ }
+ }
+
+ static class EventMatcher extends BaseMatcher {
+ Thread inThread;
+ Event.Type type;
+ Matcher spanMatcher;
+
+ private EventMatcher(Event.Type type, Matcher spanMatcher) {
+ this.type = type;
+ this.spanMatcher = spanMatcher;
+ }
+
+ static EventMatcher activated(Matcher span) {
+ return new EventMatcher(Event.Type.ACTIVATE, span);
+ }
+
+ static EventMatcher deactivated(Matcher span) {
+ return new EventMatcher(Event.Type.DEACTIVATE, span);
+ }
+
+ EventMatcher inThread(Thread thread) {
+ EventMatcher copy = new EventMatcher(type, spanMatcher);
+ copy.inThread = thread;
+ return copy;
+ }
+
+ @Override
+ public boolean matches(Object actual) {
+ if (!(actual instanceof Event)) return actual == null;
+ Event actualEv = (Event) actual;
+ return (inThread == null || inThread.equals(actualEv.thread))
+ && type.equals(actualEv.type)
+ && spanMatcher.matches(actualEv.value);
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("Event ");
+ if (inThread != null) description.appendText("in thread ").appendText(inThread.getName());
+ description.appendValue(type).appendText(" ");
+ spanMatcher.describeTo(description);
+ }
+ }
+}
diff --git a/opentracing-span-propagation/src/test/java/nl/talsmasoftware/context/opentracing/ContextScopeManagerTest.java b/opentracing-span-propagation/src/test/java/nl/talsmasoftware/context/opentracing/ContextScopeManagerTest.java
new file mode 100644
index 00000000..14706d3d
--- /dev/null
+++ b/opentracing-span-propagation/src/test/java/nl/talsmasoftware/context/opentracing/ContextScopeManagerTest.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2016-2019 Talsma ICT
+ *
+ * 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 nl.talsmasoftware.context.opentracing;
+
+import io.opentracing.Scope;
+import io.opentracing.Span;
+import io.opentracing.mock.MockTracer;
+import io.opentracing.util.GlobalTracer;
+import io.opentracing.util.GlobalTracerTestUtil;
+import nl.talsmasoftware.context.Context;
+import nl.talsmasoftware.context.ContextManagers;
+import nl.talsmasoftware.context.executors.ContextAwareExecutorService;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static nl.talsmasoftware.context.opentracing.ContextScopeManagerObserver.EventMatcher.activated;
+import static nl.talsmasoftware.context.opentracing.ContextScopeManagerObserver.EventMatcher.deactivated;
+import static nl.talsmasoftware.context.opentracing.MockSpanMatcher.withOperationName;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasToString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.startsWith;
+
+public class ContextScopeManagerTest {
+ MockTracer mockTracer;
+ ContextScopeManager scopeManager;
+ ExecutorService threadpool;
+
+ @Before
+ public void registerMockGlobalTracer() {
+ GlobalTracerTestUtil.resetGlobalTracer();
+ assertThat("Pre-existing GlobalTracer", GlobalTracer.isRegistered(), is(false));
+ scopeManager = new ContextScopeManager();
+ GlobalTracer.register(mockTracer = new MockTracer(scopeManager));
+ threadpool = new ContextAwareExecutorService(Executors.newCachedThreadPool());
+ }
+
+ @After
+ public void cleanup() {
+ threadpool.shutdown();
+ ContextManagers.clearActiveContexts();
+ GlobalTracerTestUtil.resetGlobalTracer();
+ ContextScopeManagerObserver.observed.clear();
+ }
+
+ @Test
+ public void testObservedSpans() {
+ assertThat(ContextScopeManagerObserver.observed, is(empty()));
+ Scope parent = GlobalTracer.get().buildSpan("parent").startActive(true);
+ Scope inner = GlobalTracer.get().buildSpan("inner").startActive(true);
+ inner.close();
+ parent.close();
+
+ assertThat(ContextScopeManagerObserver.observed, contains(
+ activated(withOperationName("parent")),
+ activated(withOperationName("inner")),
+ deactivated(withOperationName("inner")),
+ deactivated(withOperationName("parent"))
+ ));
+ }
+
+ @Test
+ public void testConcurrency() throws InterruptedException {
+ List threads = new ArrayList();
+ final Scope parent = GlobalTracer.get().buildSpan("parent").startActive(true);
+ final CountDownLatch latch1 = new CountDownLatch(10), latch2 = new CountDownLatch(10);
+ for (int i = 0; i < 10; i++) {
+ final int nr = i;
+ threads.add(new Thread() {
+ @Override
+ public void run() {
+ Scope inner = GlobalTracer.get().buildSpan("inner" + nr).asChildOf(parent.span()).startActive(true);
+ waitFor(latch1);
+ inner.close();
+ waitFor(latch2);
+ parent.close();
+ }
+ });
+ }
+ assertThat(ContextScopeManagerObserver.observed, contains(
+ activated(withOperationName("parent"))
+ ));
+
+ assertThat(GlobalTracer.get().activeSpan(), equalTo(parent.span()));
+ for (Thread t : threads) t.start();
+ for (Thread t : threads) t.join();
+ assertThat(GlobalTracer.get().activeSpan(), is(nullValue()));
+ assertThat(ContextScopeManagerObserver.observed, contains(
+ activated(withOperationName("parent")),
+ activated(withOperationName(startsWith("inner"))),
+ activated(withOperationName(startsWith("inner"))),
+ activated(withOperationName(startsWith("inner"))),
+ activated(withOperationName(startsWith("inner"))),
+ activated(withOperationName(startsWith("inner"))),
+ activated(withOperationName(startsWith("inner"))),
+ activated(withOperationName(startsWith("inner"))),
+ activated(withOperationName(startsWith("inner"))),
+ activated(withOperationName(startsWith("inner"))),
+ activated(withOperationName(startsWith("inner"))),
+ deactivated(withOperationName(startsWith("inner"))),
+ deactivated(withOperationName(startsWith("inner"))),
+ deactivated(withOperationName(startsWith("inner"))),
+ deactivated(withOperationName(startsWith("inner"))),
+ deactivated(withOperationName(startsWith("inner"))),
+ deactivated(withOperationName(startsWith("inner"))),
+ deactivated(withOperationName(startsWith("inner"))),
+ deactivated(withOperationName(startsWith("inner"))),
+ deactivated(withOperationName(startsWith("inner"))),
+ deactivated(withOperationName(startsWith("inner"))),
+ deactivated(withOperationName("parent"))
+ ));
+ }
+
+ @Test
+ public void testInitializeNewContext() {
+ Span span = GlobalTracer.get().buildSpan("span").start();
+ Context context = scopeManager.initializeNewContext(span);
+ assertThat(scopeManager.getActiveContext().getValue(), is(span));
+ assertThat(scopeManager.active().span(), is(span));
+ assertThat(GlobalTracer.get().activeSpan(), is(span));
+ context.close();
+ span.finish();
+ }
+
+ @Test
+ public void testSimpleToStringWhenLogged() {
+ assertThat(scopeManager, hasToString(scopeManager.getClass().getSimpleName()));
+ }
+
+ @Test
+ public void testPredictableOutOfOrderClosing() {
+ Scope first = GlobalTracer.get().buildSpan("first").startActive(true);
+ Scope second = GlobalTracer.get().buildSpan("second").startActive(true);
+ first.close();
+ assertThat(GlobalTracer.get().activeSpan(), is(second.span()));
+ second.close();
+ assertThat(GlobalTracer.get().activeSpan(), is(nullValue())); // first was already closed
+ }
+
+ private static void waitFor(CountDownLatch latch) {
+ try {
+ latch.countDown();
+ latch.await(5, TimeUnit.SECONDS);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ Assert.fail("Interrupted during test..");
+ }
+ }
+}
diff --git a/opentracing-span-propagation/src/test/resources/META-INF/services/nl.talsmasoftware.context.observer.ContextObserver b/opentracing-span-propagation/src/test/resources/META-INF/services/nl.talsmasoftware.context.observer.ContextObserver
new file mode 100644
index 00000000..bf52192a
--- /dev/null
+++ b/opentracing-span-propagation/src/test/resources/META-INF/services/nl.talsmasoftware.context.observer.ContextObserver
@@ -0,0 +1 @@
+nl.talsmasoftware.context.opentracing.ContextScopeManagerObserver