Skip to content

Commit

Permalink
Opentracing ScopeManager implementation based on our context (#110)
Browse files Browse the repository at this point in the history
Implementation of ScopeManager based on our own AbstractThreadLocalContext class with the following advantages over the 'standard' opentracing-util ThreadLocalScopeManager:

1. Close is explicitly idempotent; closing more than once has no additional side-effects
(even when finishOnClose is set to true).
2. 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.
3. Support for {@link nl.talsmasoftware.context.observer.ContextObserver}.
See opentracing/opentracing-java#334 why this is an advantage.
  • Loading branch information
sjoerdtalsma authored Mar 13, 2019
1 parent f4177c6 commit e481471
Show file tree
Hide file tree
Showing 4 changed files with 421 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>
* 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.
*
* <p>
* 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:
* <ol>
* <li>Close is explicitly idempotent; closing more than once has no additional side-effects
* (even when finishOnClose is set to {@code true}).</li>
* <li>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.
* <li>Support for {@link nl.talsmasoftware.context.observer.ContextObserver}.
* See https://github.com/opentracing/opentracing-java/issues/334 explicitly wanting this.
* </ol>
*
* <p>
* 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.
*
* <p>
* The <em>active span</em> 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<Span> {
/**
* 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<Span> 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<Span> getActiveContext() {
return ThreadLocalSpanContext.current();
}

/**
* @return String representation for this context manager.
*/
public String toString() {
return getClass().getSimpleName();
}

private static final class ThreadLocalSpanContext extends AbstractThreadLocalContext<Span> 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();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Span> {
static final List<Event> observed = synchronizedList(new ArrayList<Event>());

@Override
public Class<? extends ContextManager<Span>> 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<Event> {
Thread inThread;
Event.Type type;
Matcher<MockSpan> spanMatcher;

private EventMatcher(Event.Type type, Matcher<MockSpan> spanMatcher) {
this.type = type;
this.spanMatcher = spanMatcher;
}

static EventMatcher activated(Matcher<MockSpan> span) {
return new EventMatcher(Event.Type.ACTIVATE, span);
}

static EventMatcher deactivated(Matcher<MockSpan> 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);
}
}
}
Loading

0 comments on commit e481471

Please sign in to comment.