Skip to content

Commit

Permalink
Adds ThreadLocalSpan to show how to deal with callbacks w/o attributes (
Browse files Browse the repository at this point in the history
#455)

Sometimes you have to instrument a library where There's no attribute
namespace shared across request and response. For this scenario, you can
use `ThreadLocalSpan` to temporarily store the span between callbacks.

Here's an example:
```java
class MyFilter extends Filter {
  final ThreadLocalSpan threadLocalSpan;

  public void onStart(Request request) {
    // Assume you have code to start the span and add relevant tags...

    // We now set the span in scope so that any code between here and
    // the end of the request can see it with Tracer.currentSpan()
    threadLocalSpan.set(span);
  }

  public void onFinish(Response response, Attributes attributes) {
    // as long as we are on the same thread, we can read the span started above
    Span span = threadLocalSpan.remove();
    if (span == null) return;

    // Assume you have code to complete the span
  }
}
```
  • Loading branch information
adriancole committed Nov 25, 2017
1 parent a1b6f64 commit 4ccf3ba
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 95 deletions.
31 changes: 29 additions & 2 deletions brave/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,34 @@ class MyFilter extends Filter {
// Assume you have code to complete the span

// We now remove the scope (which implicitly detaches it from the span)
attributes.get(SpanInScope.class).close();
attributes.remove(SpanInScope.class).close();
}
}
```

Sometimes you have to instrument a library where There's no attribute
namespace shared across request and response. For this scenario, you can
use `ThreadLocalSpan` to temporarily store the span between callbacks.

Here's an example:
```java
class MyFilter extends Filter {
final ThreadLocalSpan threadLocalSpan;

public void onStart(Request request) {
// Assume you have code to start the span and add relevant tags...

// We now set the span in scope so that any code between here and
// the end of the request can see it with Tracer.currentSpan()
threadLocalSpan.set(span);
}

public void onFinish(Response response, Attributes attributes) {
// as long as we are on the same thread, we can read the span started above
Span span = threadLocalSpan.remove();
if (span == null) return;

// Assume you have code to complete the span
}
}
```
Expand Down Expand Up @@ -551,7 +578,7 @@ class MyFilter extends Filter {
public void onFinish(Response response, Attributes attributes) {
// We can't rely on Tracer.currentSpan(), but we can rely on explicit
// propagation
Span span = attributes.get(Span.class);
Span span = attributes.remove(Span.class);

// Assume you have code to complete the span
}
Expand Down
122 changes: 122 additions & 0 deletions brave/src/main/java/brave/propagation/ThreadLocalSpan.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package brave.propagation;

import brave.Span;
import brave.Tracer;
import brave.Tracing;
import com.google.auto.value.AutoValue;
import javax.annotation.Nullable;

/**
* This type allows you to place a span in scope in one method and access it in another without
* using an explicit request parameter.
*
* <p>Many libraries expose a callback model as opposed to an interceptor one. When creating new
* instrumentation, you may find places where you need to place a span in scope in one callback
* (like `onStart()`) and end the scope in another callback (like `onFinish()`).
*
* <p>Provided the library guarantees these run on the same thread, you can simply propagate the
* result of {@link Tracer#withSpanInScope(Span)} from the starting callback to the closing one.
* This is typically done with a request-scoped attribute.
*
* Here's an example:
* <pre>{@code
* class MyFilter extends Filter {
* public void onStart(Request request, Attributes attributes) {
* // Assume you have code to start the span and add relevant tags...
*
* // We now set the span in scope so that any code between here and
* // the end of the request can see it with Tracer.currentSpan()
* SpanInScope spanInScope = tracer.withSpanInScope(span);
*
* // We don't want to leak the scope, so we place it somewhere we can
* // lookup later
* attributes.put(SpanInScope.class, spanInScope);
* }
*
* public void onFinish(Response response, Attributes attributes) {
* // as long as we are on the same thread, we can read the span started above
* Span span = tracer.currentSpan();
*
* // Assume you have code to complete the span
*
* // We now remove the scope (which implicitly detaches it from the span)
* attributes.remove(SpanInScope.class).close();
* }
* }
* }</pre>
*
* <p>Sometimes you have to instrument a library where There's no attribute namespace shared across
* request and response. For this scenario, you can use {@link ThreadLocalSpan} to temporarily store
* the span between callbacks.
*
* Here's an example:
* <pre>{@code
* class MyFilter extends Filter {
* final ThreadLocalSpan threadLocalSpan;
*
* public void onStart(Request request) {
* // Allocates a span and places it in scope so that code between here and onFinish can see it
* Span span = threadLocalSpan.next();
* if (span == null || span.isNoop()) return; // skip below logic on noop
*
* // Assume you have code to start the span and add relevant tags...
* }
*
* public void onFinish(Response response, Attributes attributes) {
* // as long as we are on the same thread, we can read the span started above
* Span span = threadLocalSpan.remove();
* if (span == null || span.isNoop()) return; // skip below logic on noop
*
* // Assume you have code to complete the span
* }
* }
* }</pre>
*/
@AutoValue
public abstract class ThreadLocalSpan {
/**
* This uses the {@link Tracing#currentTracer()}, which means calls to {@link #next()} may return
* null. Use this when you have no other means to get a reference to the tracer. For example, JDBC
* connections, as they often initialize prior to the tracing component.
*/
public static final ThreadLocalSpan CURRENT_TRACER = new ThreadLocalSpan() {
@Override Tracer tracer() {
return Tracing.currentTracer();
}
};

public static ThreadLocalSpan create(Tracer tracer) {
return new AutoValue_ThreadLocalSpan(tracer);
}

final ThreadLocal<Tracer.SpanInScope> currentSpanInScope = new ThreadLocal<>();

abstract Tracer tracer();

/**
* Returns the {@link Tracer#nextSpan()} or null if {@link #CURRENT_TRACER} and tracing isn't
* available.
*/
public @Nullable Span next() {
Tracer tracer = tracer();
if (tracer == null) return null;
Span next = tracer.nextSpan();
currentSpanInScope.set(tracer.withSpanInScope(next));
return next;
}

/** Returns the span set in scope via {@link #next()} or null if there was none. */
public @Nullable Span remove() {
Tracer tracer = tracer();
Span span = tracer != null ? tracer.currentSpan() : null;
Tracer.SpanInScope scope = currentSpanInScope.get();
if (scope != null) {
scope.close();
currentSpanInScope.remove();
}
return span;
}

ThreadLocalSpan() {
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package brave.mysql;

import brave.Span;
import brave.Tracer;
import brave.Tracing;
import brave.propagation.ThreadLocalSpan;
import com.mysql.jdbc.Connection;
import com.mysql.jdbc.PreparedStatement;
import com.mysql.jdbc.ResultSetInternalMethods;
Expand All @@ -21,49 +20,38 @@
*/
public class TracingStatementInterceptor implements StatementInterceptorV2 {

/**
* Uses {@link ThreadLocalSpan} as there's no attribute namespace shared between callbacks, but
* all callbacks happen on the same thread.
*
* <p>Uses {@link ThreadLocalSpan#CURRENT_TRACER} and this interceptor initializes before tracing.
*/
@Override
public ResultSetInternalMethods preProcess(String sql, Statement interceptedStatement,
Connection connection) throws SQLException {
Tracer tracer = Tracing.currentTracer();
if (tracer == null) return null;
// Gets the next span (and places it in scope) so code between here and postProcess can read it
Span span = ThreadLocalSpan.CURRENT_TRACER.next();
if (span == null || span.isNoop()) return null;

Span span = tracer.nextSpan();
// regardless of noop or not, set it in scope so that custom contexts can see it (like slf4j)
if (!span.isNoop()) {
// When running a prepared statement, sql will be null and we must fetch the sql from the statement itself
if (interceptedStatement instanceof PreparedStatement) {
sql = ((PreparedStatement) interceptedStatement).getPreparedSql();
}
int spaceIndex = sql.indexOf(' '); // Allow span names of single-word statements like COMMIT
span.kind(Span.Kind.CLIENT).name(spaceIndex == -1 ? sql : sql.substring(0, spaceIndex));
span.tag("sql.query", sql);
parseServerAddress(connection, span);
span.start();
// When running a prepared statement, sql will be null and we must fetch the sql from the statement itself
if (interceptedStatement instanceof PreparedStatement) {
sql = ((PreparedStatement) interceptedStatement).getPreparedSql();
}

currentSpanInScope.set(tracer.withSpanInScope(span));

int spaceIndex = sql.indexOf(' '); // Allow span names of single-word statements like COMMIT
span.kind(Span.Kind.CLIENT).name(spaceIndex == -1 ? sql : sql.substring(0, spaceIndex));
span.tag("sql.query", sql);
parseServerAddress(connection, span);
span.start();
return null;
}

/**
* There's no attribute namespace shared across request and response. Hence, we need to save off
* a reference to the span in scope, so that we can close it in the response.
*/
final ThreadLocal<Tracer.SpanInScope> currentSpanInScope = new ThreadLocal<>();

@Override
public ResultSetInternalMethods postProcess(String sql, Statement interceptedStatement,
ResultSetInternalMethods originalResultSet, Connection connection, int warningCount,
boolean noIndexUsed, boolean noGoodIndexUsed, SQLException statementException)
throws SQLException {
Tracer tracer = Tracing.currentTracer();
if (tracer == null) return null;

Span span = tracer.currentSpan();
if (span == null) return null;
currentSpanInScope.get().close();
currentSpanInScope.remove();
Span span = ThreadLocalSpan.CURRENT_TRACER.remove();
if (span == null || span.isNoop()) return null;

if (statementException != null) {
span.tag("error", Integer.toString(statementException.getErrorCode()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package brave.mysql6;

import brave.Span;
import brave.Tracer;
import brave.Tracing;
import brave.propagation.ThreadLocalSpan;
import com.mysql.cj.api.MysqlConnection;
import com.mysql.cj.api.jdbc.JdbcConnection;
import com.mysql.cj.api.jdbc.Statement;
Expand All @@ -23,47 +22,36 @@
*/
public class TracingStatementInterceptor implements StatementInterceptor {


/**
* Uses {@link ThreadLocalSpan} as there's no attribute namespace shared between callbacks, but
* all callbacks happen on the same thread.
*
* <p>Uses {@link ThreadLocalSpan#CURRENT_TRACER} and this interceptor initializes before tracing.
*/
@Override
public Resultset preProcess(String sql, Statement interceptedStatement) throws SQLException {
Tracer tracer = Tracing.currentTracer();
if (tracer == null) return null;
// Gets the next span (and places it in scope) so code between here and postProcess can read it
Span span = ThreadLocalSpan.CURRENT_TRACER.next();
if (span == null || span.isNoop()) return null;

Span span = tracer.nextSpan();
// regardless of noop or not, set it in scope so that custom contexts can see it (like slf4j)
if (!span.isNoop()) {
// When running a prepared statement, sql will be null and we must fetch the sql from the statement itself
if (interceptedStatement instanceof PreparedStatement) {
sql = ((PreparedStatement) interceptedStatement).getPreparedSql();
}
int spaceIndex = sql.indexOf(' '); // Allow span names of single-word statements like COMMIT
span.kind(Span.Kind.CLIENT).name(spaceIndex == -1 ? sql : sql.substring(0, spaceIndex));
span.tag("sql.query", sql);
parseServerAddress(connection, span);
span.start();
// When running a prepared statement, sql will be null and we must fetch the sql from the statement itself
if (interceptedStatement instanceof PreparedStatement) {
sql = ((PreparedStatement) interceptedStatement).getPreparedSql();
}

currentSpanInScope.set(tracer.withSpanInScope(span));

int spaceIndex = sql.indexOf(' '); // Allow span names of single-word statements like COMMIT
span.kind(Span.Kind.CLIENT).name(spaceIndex == -1 ? sql : sql.substring(0, spaceIndex));
span.tag("sql.query", sql);
parseServerAddress(connection, span);
span.start();
return null;
}

/**
* There's no attribute namespace shared across request and response. Hence, we need to save off
* a reference to the span in scope, so that we can close it in the response.
*/
final ThreadLocal<Tracer.SpanInScope> currentSpanInScope = new ThreadLocal<>();
private MysqlConnection connection;

@Override
public <T extends Resultset> T postProcess(String sql, Statement interceptedStatement, T originalResultSet, int warningCount, boolean noIndexUsed, boolean noGoodIndexUsed, Exception statementException) throws SQLException {
Tracer tracer = Tracing.currentTracer();
if (tracer == null) return null;

Span span = tracer.currentSpan();
if (span == null) return null;
currentSpanInScope.get().close();
currentSpanInScope.remove();
Span span = ThreadLocalSpan.CURRENT_TRACER.remove();
if (span == null || span.isNoop()) return null;

if (statementException != null && statementException instanceof SQLException) {
span.tag("error", Integer.toString(((SQLException)statementException).getErrorCode()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package brave.p6spy;

import brave.Span;
import brave.Tracer;
import brave.Tracing;
import brave.internal.Nullable;
import brave.propagation.ThreadLocalSpan;
import com.p6spy.engine.common.StatementInformation;
import com.p6spy.engine.event.SimpleJdbcEventListener;
import java.net.URI;
Expand All @@ -26,42 +25,30 @@ final class TracingJdbcEventListener extends SimpleJdbcEventListener {
this.includeParameterValues = includeParameterValues;
}

/**
* Uses {@link ThreadLocalSpan} as there's no attribute namespace shared between callbacks, but
* all callbacks happen on the same thread.
*
* <p>Uses {@link ThreadLocalSpan#CURRENT_TRACER} and this interceptor initializes before tracing.
*/
@Override public void onBeforeAnyExecute(StatementInformation info) {
Tracer tracer = Tracing.currentTracer();
if (tracer == null) return;
String sql = includeParameterValues ? info.getSqlWithValues() : info.getSql();
// don't start a span unless there is SQL as we cannot choose a relevant name without it
if (sql == null || sql.isEmpty()) return;

Span span = tracer.nextSpan();
// regardless of noop or not, set it in scope so that custom contexts can see it (like slf4j)
if (!span.isNoop()) {
span.kind(Span.Kind.CLIENT).name(sql.substring(0, sql.indexOf(' ')));
span.tag("sql.query", sql);
parseServerAddress(info.getConnectionInformation().getConnection(), span);
span.start();
}
// Gets the next span (and places it in scope) so code between here and postProcess can read it
Span span = ThreadLocalSpan.CURRENT_TRACER.next();
if (span == null || span.isNoop()) return;

currentSpanInScope.set(tracer.withSpanInScope(span));
span.kind(Span.Kind.CLIENT).name(sql.substring(0, sql.indexOf(' ')));
span.tag("sql.query", sql);
parseServerAddress(info.getConnectionInformation().getConnection(), span);
span.start();
}

/**
* There's no attribute namespace shared across request and response. Hence, we need to save off
* a reference to the span in scope, so that we can close it in the response.
*/
final ThreadLocal<Tracer.SpanInScope> currentSpanInScope = new ThreadLocal<>();

@Override public void onAfterAnyExecute(StatementInformation info, long elapsed, SQLException e) {
Tracer tracer = Tracing.currentTracer();
if (tracer == null) return;

Span span = tracer.currentSpan();
if (span == null) return;
Tracer.SpanInScope scope = currentSpanInScope.get();
if (scope != null) {
scope.close();
currentSpanInScope.remove();
}
Span span = ThreadLocalSpan.CURRENT_TRACER.remove();
if (span == null || span.isNoop()) return;

if (e != null) {
span.tag("error", Integer.toString(e.getErrorCode()));
Expand Down

0 comments on commit 4ccf3ba

Please sign in to comment.