Skip to content

Commit

Permalink
Adds support for "current span" formerly supported with thread binders
Browse files Browse the repository at this point in the history
This implements a "current span" concept which represents the in-flight
operation. `Tracer.currentSpan()` can be used to add custom tags to a
span and `Tracer.nextSpan()` can be used to create a child of whatever
is in-flight.

The backend of this is `CurrentTraceContext` which can be extended to
support SLF4J or Finagle synchronization. It also exposes concurrent
wrappers for types like `ExecutorService`.

Particularly, this includes the ability to get the "current span" and assign is to a scope. In most cases the implementation will be backed by thread locals, whether that's directly (as brave's former thread state was) or indirectly via a GRPC or Finagle Context.

The (power) user apis are located in the Tracer api:
* `Span currentSpan` - retrieves any span in scope or null
* `Span nextSpan` - creates a new child or trace if there's no current span
* `SpanInScope withSpanInScope(span)` - makes a span current and available to calls within the scope

The SPI for this is called `CurrentTraceContext`, and it can be implemented to support MDC integration or alternate span storage
* `TraceContext get()` - returns the current trace context or null
*  `Scope newScope(context)` - creates a scope where calls to get will return the input

By default, `CurrentTraceContext` is implemented by a thread local, though there are tests to show it can be implemented by Finagle (or gRPC or otherwise).
  • Loading branch information
Adrian Cole committed Apr 2, 2017
1 parent 6b55f2d commit 91f75bd
Show file tree
Hide file tree
Showing 20 changed files with 1,108 additions and 52 deletions.
2 changes: 1 addition & 1 deletion brave-benchmarks/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
<dependency>
<groupId>com.twitter</groupId>
<artifactId>finagle-core_2.12</artifactId>
<version>6.41.0</version>
<version>${finagle.version}</version>
</dependency>
</dependencies>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,11 @@ Client configureClient(JerseyClientTraceFilter filter) {

@Override protected void get(Client client, String pathIncludingQuery)
throws IOException {
client.resource(server.url("/foo").uri()).get(String.class);
client.resource(server.url(pathIncludingQuery).uri()).get(String.class);
}

@Override protected void getAsync(Client client, String pathIncludingQuery) throws Exception {
client.asyncResource(server.url("/foo").uri()).get(String.class);
}


@Override
@Test(expected = AssertionError.class) // query params are not logged in jersey
public void httpUrlTagIncludesQueryParams() throws Exception {
super.httpUrlTagIncludesQueryParams();
client.asyncResource(server.url(pathIncludingQuery).uri()).get(String.class);
}

@Override
Expand Down
129 changes: 129 additions & 0 deletions brave/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,121 @@ span = contextOrFlags.context() != null
: tracer.newTrace(contextOrFlags.samplingFlags());
```

## Current Span

Brave supports a "current span" concept which represents the in-flight
operation. `Tracer.currentSpan()` can be used to add custom tags to a
span and `Tracer.nextSpan()` can be used to create a child of whatever
is in-flight.

### Setting a span in scope via custom executors

Many frameworks allow you to specify an executor which is used for user
callbacks. The type `CurrentTraceContext` implements all functionality
needed to support the current span. It also exposes utilities which you
can use to decorate executors.

```java
CurrentTraceContext currentTraceContext = new CurrentTraceContext.Default();
tracer = Tracer.newBuilder()
.currentTraceContext(currentTraceContext)
...
.build();

Client c = Client.create();
c.setExecutorService(currentTraceContext.executorService(realExecutorService));
```

### Setting a span in scope manually

When writing new instrumentation, it is important to place a span you
created in scope as the current span. Not only does this allow users to
access it with `Tracer.currentSpan()`, but it also allows customizations
like SLF4J MDC to see the current trace IDs.

`Tracer.withSpanInScope(Span)` facilitates this and is most conveniently
employed via the try-with-resources idiom. Whenever external code might
be invoked (such as proceeding an interceptor or otherwise), place the
span in scope like this.

```java
try (SpanInScope ws = tracer.withSpanInScope(span)) {
return inboundRequest.invoke();
} finally { // note the scope is independent of the span
span.finish();
}
```

### Working with callbacks

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()`).

Provided the library guarantees these run on the same thread, you can
simply propagate the result of `Tracer.withSpanInScope(Span)` from the
starting callback to the closing one. This is typically done with a
request-scoped attribute.

Here's an example:
```java
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.get(SpanInScope.class).close();
}
}
```

### Working with callbacks that occur on different threads

The examples above work because the callbacks happen on the same thread.
You should not set a span in scope if you cannot close that scope on the
same thread. This may be the case in some asynchronous libraries. Often,
you will need to propagate the span directly in a custom attribute. This
will allow you to trace the RPC, even if this approach doesn't facilitate
use of `Tracer.currentSpan()` from external code.

Here's an example of explicit propagation:
```java
class MyFilter extends Filter {
public void onStart(Request request, Attributes attributes) {
// Assume you have code to start the span and add relevant tags...

// We can't open a scope as onFinish happens on another thread.
// Instead, we propagate the span manually so at least basic tracing
// will work.
attributes.put(Span.class, span);
}

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);

// Assume you have code to complete the span
}
}
```

## Performance
Brave has been built with performance in mind. Using the core Span api,
you can record spans in sub-microseconds. When a span is sampled, there's
Expand Down Expand Up @@ -360,6 +475,20 @@ direct integration with carrier types (such as http request) vs routing
through an intermediate (such as a map). Brave also considers propagation
a separate api from the tracer.


### CurrentTraceContext Api
The first design of `CurrentTraceContext` was borrowed from `ContextUtils`
in Google's [instrumentation-java](https://github.com/google/instrumentation-java) project.
This was possible because of high collaboration between the projects and
an almost identical goal. Slight departures including naming (which prefers
Guice's naming conventions), and that the object scoped is a TraceContext
vs a Span.

Propagating a trace context instead of a span is a right fit for several reasons:
* `TraceContext` can be created without a reference to a tracer
* Common tasks like making child spans and staining log context only need the context
* Brave's recorder is keyed on context, so there's no feature loss in this choice

### Public namespace
Brave 4's pubic namespace is more defensive that the past, using a package
accessor design from [OkHttp](https://github.com/square/okhttp).
6 changes: 6 additions & 0 deletions brave/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,11 @@
<version>${grpc.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.twitter</groupId>
<artifactId>finagle-core_2.12</artifactId>
<version>${finagle.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
18 changes: 5 additions & 13 deletions brave/src/main/java/brave/NoopSpan.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
package brave;

import brave.propagation.TraceContext;
import com.google.auto.value.AutoValue;
import zipkin.Endpoint;

final class NoopSpan extends Span {
final TraceContext context;
@AutoValue
abstract class NoopSpan extends Span {

NoopSpan(TraceContext context) {
this.context = context;
static NoopSpan create(TraceContext context) {
return new AutoValue_NoopSpan(context);
}

@Override public boolean isNoop() {
return true;
}

@Override public TraceContext context() {
return context;
}

@Override public Span start() {
return this;
}
Expand Down Expand Up @@ -58,9 +55,4 @@ final class NoopSpan extends Span {

@Override public void flush() {
}

@Override
public String toString() {
return "NoopSpan(" + context + ")";
}
}
44 changes: 20 additions & 24 deletions brave/src/main/java/brave/RealSpan.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,81 +2,77 @@

import brave.internal.recorder.Recorder;
import brave.propagation.TraceContext;
import com.google.auto.value.AutoValue;
import zipkin.Endpoint;

/** This wraps the public api and guards access to a mutable span. */
final class RealSpan extends Span {
@AutoValue
abstract class RealSpan extends Span {

final TraceContext context;
final Clock clock;
final Recorder recorder;
abstract Clock clock();

RealSpan(TraceContext context, Clock clock, Recorder recorder) {
this.context = context;
this.clock = clock;
this.recorder = recorder;
abstract Recorder recorder();

static RealSpan create(TraceContext context, Clock clock, Recorder recorder) {
return new AutoValue_RealSpan(context, clock, recorder);
}

@Override public boolean isNoop() {
return false;
}

@Override public TraceContext context() {
return context;
}

@Override public Span start() {
return start(clock.currentTimeMicroseconds());
return start(clock().currentTimeMicroseconds());
}

@Override public Span start(long timestamp) {
recorder.start(context, timestamp);
recorder().start(context(), timestamp);
return this;
}

@Override public Span name(String name) {
recorder.name(context, name);
recorder().name(context(), name);
return this;
}

@Override public Span kind(Kind kind) {
recorder.kind(context, kind);
recorder().kind(context(), kind);
return this;
}

@Override public Span annotate(String value) {
return annotate(clock.currentTimeMicroseconds(), value);
return annotate(clock().currentTimeMicroseconds(), value);
}

@Override public Span annotate(long timestamp, String value) {
recorder.annotate(context, timestamp, value);
recorder().annotate(context(), timestamp, value);
return this;
}

@Override public Span tag(String key, String value) {
recorder.tag(context, key, value);
recorder().tag(context(), key, value);
return this;
}

@Override public Span remoteEndpoint(Endpoint remoteEndpoint) {
recorder.remoteEndpoint(context, remoteEndpoint);
recorder().remoteEndpoint(context(), remoteEndpoint);
return this;
}

@Override public void finish() {
finish(clock.currentTimeMicroseconds());
finish(clock().currentTimeMicroseconds());
}

@Override public void finish(long timestamp) {
recorder.finish(context, timestamp);
recorder().finish(context(), timestamp);
}

@Override public void flush() {
recorder.flush(context);
recorder().flush(context());
}

@Override
public String toString() {
return "RealSpan(" + context + ")";
return "RealSpan(" + context() + ")";
}
}
Loading

0 comments on commit 91f75bd

Please sign in to comment.