Skip to content
This repository has been archived by the owner on May 23, 2023. It is now read-only.

Introduce ActiveSpan, ActiveSpan.Continuation, and ActiveSpanSource #115

Merged
merged 48 commits into from
May 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
000a8c3
Introduce ActiveSpanHolder ASH.Continuation
bhs Mar 28, 2017
6e24189
Improve README and adjust to improve fluency
bhs Mar 28, 2017
5c6b992
Factor out Handle and move to pure interfaces
bhs Apr 1, 2017
cc71c24
Separate ActiveSpan and ActiveSpanSource
bhs Apr 4, 2017
2058413
Demonstrate the ActiveSpanHolder interfaces
bhs Mar 28, 2017
d533226
Adjust to rebase
bhs Mar 28, 2017
6b10c9b
Actually write to MDC
bhs Mar 28, 2017
f63d20b
Adjust to core changes
bhs Apr 1, 2017
e88b0a9
Update to follow suit with core changes
bhs Apr 4, 2017
26b23f3
Separate ActiveSpan and ActiveSpanSource
bhs Apr 4, 2017
e48d475
Improve README and fix small issues
bhs Apr 5, 2017
79880a4
Deal with rebase fallout
bhs Apr 7, 2017
e3ee448
ActiveSpanSource -> ActiveSpanProvider
bhs Apr 7, 2017
56d01f9
Un-rename
bhs Apr 7, 2017
54e6166
Make Tracer inherit from ActiveSpanSource
bhs Apr 7, 2017
24178b9
Deprecate start() in favor of startManual()
bhs Apr 7, 2017
281e5dc
Clean up comments
bhs Apr 7, 2017
4707b23
Truth in advertising
bhs Apr 7, 2017
5b6f17f
Update GlobalTracer
bhs Apr 8, 2017
619fcd9
Add ThreadLocalActiveSpan* tests
bhs Apr 8, 2017
b26fe7c
Update license headers
bhs Apr 8, 2017
d87c1c7
Make javadoc work and fix some documentation bugs
bhs Apr 10, 2017
b1468f7
Perform a `%s/defer/capture/g`
bhs Apr 10, 2017
4087c3e
Fix IntelliJ mangling
bhs Apr 10, 2017
b39fe6d
Update README
bhs Apr 14, 2017
28223dd
s/adopt/makeActive/g
bhs Apr 14, 2017
a8e1a1d
Fix asRoot naming
bhs Apr 14, 2017
9799fd4
Remove MDC demo (preparing for release)
bhs Apr 15, 2017
9dd7be6
Fix some minor rebase issues
bhs Apr 16, 2017
49a3548
Respond to PR comments
bhs Apr 16, 2017
1c250e9
Respond to PR comments
bhs Apr 17, 2017
8776bff
Respond to PR comments
bhs Apr 18, 2017
cd16c62
Respond to PR comments
bhs Apr 20, 2017
07ad79a
Fix NoopSource's SpanContext
bhs Apr 20, 2017
089c904
Review responses
bhs Apr 21, 2017
574a7d2
Fix typo
bhs Apr 24, 2017
c28c44c
Add licenses to files the maven plugin missed
bhs Apr 24, 2017
4718dfb
Respond to PR comments
bhs Apr 25, 2017
bc351dc
Minimize public surface of NoopActiveSpanSource
bhs Apr 28, 2017
6539f4a
Add BaseSpan to clean up ActiveSpan/Span mechanics
bhs Apr 29, 2017
36bdb45
Move ThreadLocalActiveSpan to .util
bhs Apr 29, 2017
4d03cc5
Add license headers
bhs Apr 29, 2017
b5ddc97
Fix tests and a comment
bhs Apr 30, 2017
bf4606c
Clean up more Span/BaseSpan code
bhs Apr 30, 2017
ae36949
Make BaseSpan sub-interface chaining work
bhs May 1, 2017
20a3c67
Self-review with a fine-toothed comb
bhs May 8, 2017
9856c90
Do a full clarification pass on the README
bhs May 9, 2017
f1e5aa7
Respond to PR comments
bhs May 9, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .settings.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--

Copyright 2016 The OpenTracing Authors
Copyright 2016-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
Expand Down
303 changes: 303 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,309 @@ to facilitate unit-testing of OpenTracing Java instrumentation.

Packages are deployed to Maven Central under the `io.opentracing` group.

## Usage
[![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] [![Released Version][maven-img]][maven]

# OpenTracing API for Java

This library is a Java platform API for OpenTracing.

## Required Reading

In order to understand the Java platform API, one must first be familiar with
the [OpenTracing project](http://opentracing.io) and
[terminology](http://opentracing.io/documentation/pages/spec.html) more specifically.

## Status

This project has a working design of interfaces for the OpenTracing API. There
is a [MockTracer](https://github.com/opentracing/opentracing-java/tree/master/opentracing-mock)
to facilitate unit-testing of OpenTracing Java instrumentation.

Packages are deployed to Maven Central under the `io.opentracing` group.

## Usage

### Initialization

Initialization is OpenTracing-implementation-specific. Generally speaking, the pattern is to initialize a `Tracer` once for the entire process and to use that `Tracer` for the remainder of the process lifetime. The [GlobalTracer](https://github.com/opentracing/opentracing-java/blob/master/opentracing-util/src/main/java/io/opentracing/util/GlobalTracer.java) provides a helper for singleton access to the `Tracer`.

### `ActiveSpan`s, `Continuation`s, and within-process propagation

For any thread, at most one `Span` may be "active". Of course there may be many other `Spans` involved with the thread which are (a) started, (b) not finished, and yet (c) not "active": perhaps they are waiting for I/O, blocked on a child Span, or otherwise off of the critical path.

It's inconvenient to pass an active `Span` from function to function manually, so OpenTracing requires that every `Tracer` implement an `ActiveSpanSource` interface that grants access to an `ActiveSpan`. Any `ActiveSpan` may be transferred to another callback or thread via `ActiveSpan#defer()` and `Continuation#activate()`; more on this below.

#### Accessing the `ActiveSpan`

Access to the active span is straightforward:

```
io.opentracing.Tracer tracer = ...;
...
ActiveSpan span = tracer.activeSpan();
if (span != null) {
span.log("...");
}
```

### Starting a new Span

The common case starts an `ActiveSpan` that's automatically registered for intra-process propagation via `ActiveSpanSource`. The best practice is to use a try-with-resources pattern which handles Exceptions and early returns:

```
io.opentracing.Tracer tracer = ...;
...
try (ActiveSpan activeSpan = tracer.buildSpan("someWork").startActive()) {
// Do things.
//
// If we create async work, `activeSpan.capture()` allows us to pass the `ActiveSpan` along as well.
}
```

The above is semantically equivalent to the more explicit try-finally version:

```
io.opentracing.Tracer tracer = ...;
...
ActiveSpan activeSpan = tracer.buildSpan("someWork").startActive();
try {
// Do things.
} finally {
activeSpan.deactivate();
}
```

To manually step around the `ActiveSpanSource` registration, use `startManual()`, like this:

```
io.opentracing.Tracer tracer = ...;
...
Span span = tracer.buildSpan("someWork").startManual();
try {
// (do things / record data to `span`)
} finally {
span.finish();
}
```

**If there is an `ActiveSpan`, it will act as the parent to any newly started `Span`** unless the programmer invokes `ignoreActiveSpan()` at `buildSpan()` time, like so:

```
io.opentracing.Tracer tracer = ...;
...
ActiveSpan span = tracer.buildSpan("someWork").ignoreActiveSpan().startActive();
```

### Deferring asynchronous work

Consider the case where a `Span`'s lifetime logically starts in one thread and ends in another. For instance, the intra-Span timing breakdown might look like this:

```
[ ServiceHandlerSpan ]
|·FunctionA·|·····waiting on an RPC······|·FunctionB·|

------------------------------------------------> time
```

The `"ServiceHandlerSpan"` is _active_ when it's running FunctionA and FunctionB, and inactive while it's waiting on an RPC (presumably modelled as its own Span, though that's not the concern here).

**The `ActiveSpanSource` makes it easy to `capture()` the Span and execution context in `FunctionA` and re-activate it in `FunctionB`.** Note that every `Tracer` must also implement `ActiveSpanSource`. These are the steps:

1. Start the `ActiveSpan` via `Tracer.startActive()` rather than via `Tracer.startManual()`; or, if the `Span` was already started manually via `startManual()`, call `ActiveSpanSource#makeActive(span)`. Either route will yield an `ActiveSpan` instance that encapsulates the `Span`.
2. In the method/function that *allocates* the closure/`Runnable`/`Future`/etc, call `ActiveSpan#capture()` to obtain an `ActiveSpan.Continuation`
3. In the closure/`Runnable`/`Future`/etc itself, invoke `ActiveSpan.Continuation#activate` to re-activate the `ActiveSpan`, then `deactivate()` it when the Span is no longer active (or use try-with-resources for less typing).

For example:

```
io.opentracing.Tracer tracer = ...;
...
// STEP 1 ABOVE: start the ActiveSpan
try (ActiveSpan serviceSpan = tracer.buildSpan("ServiceHandlerSpan").startActive()) {
...

// STEP 2 ABOVE: capture the ActiveSpan
final ActiveSpan.Continuation cont = serviceSpan.capture();
doAsyncWork(new Runnable() {
@Override
public void run() {

// STEP 3 ABOVE: use the Continuation to reactivate the Span in the callback.
try (ActiveSpan activeSpan = cont.activate()) {
...
}
}
});
}
```

In practice, all of this is most fluently accomplished through the use of an OpenTracing-aware `ExecutorService` and/or `Runnable`/`Callable` adapter; they can factor most of the typing.

#### Automatic `finish()`ing via `ActiveSpan` reference counts

When an `ActiveSpan` is created (either via `Tracer.SpanBuilder#startActive` or `ActiveSpanSource#makeActive(Span)`), the reference count associated with the `ActiveSpan` is `1`.

- When an `ActiveSpan.Continuation` is created via `ActiveSpan#capture`, the reference count **increments**
- When an `ActiveSpan.Continuation` is `ActiveSpan.Continuation#activate()`d and thus transformed back into an `ActiveSpan`, the reference count **is unchanged**
- When an `ActiveSpan` is `ActiveSpan#deactivate()`d, the reference count **decrements**

When the reference count decrements to zero, **the `Span`'s `finish()` method is invoked automatically.**

When used as designed, the programmer lets `ActiveSpan` and `ActiveSpan.Continuation` finish the `Span` as soon as the last active or deferred `ActiveSpan` is deactivated.

# Development

This is a maven project, and provides a wrapper, `./mvnw` to pin a consistent
version. For example, `./mvnw clean install`.

This wrapper was generated by `mvn -N io.takari:maven:wrapper -Dmaven=3.3.9`

## Building

Execute `./mvnw clean install` to build, run tests, and create jars.

## Contributing

See [Contributing](CONTRIBUTING.md) for matters such as license headers.


[ci-img]: https://travis-ci.org/opentracing/opentracing-java.svg?branch=master
[ci]: https://travis-ci.org/opentracing/opentracing-java
[cov-img]: https://coveralls.io/repos/github/opentracing/opentracing-java/badge.svg?branch=master
[cov]: https://coveralls.io/github/opentracing/opentracing-java?branch=master
[maven-img]: https://img.shields.io/maven-central/v/io.opentracing/opentracing-api.svg?maxAge=2592000
[maven]: http://search.maven.org/#search%7Cga%7C1%7Copentracing-api


### Initialization

Initialization is OpenTracing-implementation-specific. Generally speaking, the pattern is to initialize a `Tracer` once for the entire process and to use that `Tracer` for the remainder of the process lifetime. The [GlobalTracer](https://github.com/opentracing/opentracing-java/blob/master/opentracing-util/src/main/java/io/opentracing/util/GlobalTracer.java) provides a helper for singleton access to the `Tracer`.

### `ActiveSpan`s, `Continuation`s, and within-process propagation

For any thread, at most one `Span` may be "active". Of course there may be many other `Spans` involved with the thread which are (a) started, (b) not finished, and yet (c) not "active": perhaps they are waiting for I/O, blocked on a child Span, or otherwise off of the critical path.

It's inconvenient to pass an active `Span` from function to function manually, so OpenTracing requires that every `Tracer` implement an `ActiveSpanSource` interface that grants access to an `ActiveSpan`. Any `ActiveSpan` may be transferred to another callback or thread via `ActiveSpan#defer()` and `Continuation#activate()`; more on this below.

#### Accessing the `ActiveSpan`

Access to the active span is straightforward:

```
io.opentracing.Tracer tracer = ...;
...
ActiveSpan span = tracer.activeSpan();
if (span != null) {
span.log("...");
}
```

### Starting a new Span

The common case starts an `ActiveSpan` that's automatically registered for intra-process propagation via `ActiveSpanSource`. The best practice is to use a try-with-resources pattern which handles Exceptions and early returns:

```
io.opentracing.Tracer tracer = ...;
...
try (ActiveSpan activeSpan = tracer.buildSpan("someWork").startActive()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be good to give the full API example first, i.e.

ActiveSpan activeSpan = tracer.buildSpan("someWork").startActive()
try {
} finally {
  activeSpan.deactivate();
}

and then the same with using try-with-resource, as a shorthand. The try-finally example better explains the API imo, without "magic".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did something...

// Do things.
//
// If we create async work, `activeSpan.capture()` allows us to pass the `ActiveSpan` along as well.
}
```

The above is semantically equivalent to the more explicit try-finally version:

```
io.opentracing.Tracer tracer = ...;
...
ActiveSpan activeSpan = tracer.buildSpan("someWork").startActive();
try {
// Do things.
} finally {
activeSpan.deactivate();
}
```

To manually step around the `ActiveSpanSource` registration, use `startManual()`, like this:

```
io.opentracing.Tracer tracer = ...;
...
Span span = tracer.buildSpan("someWork").startManual();
try {
// (do things / record data to `span`)
} finally {
span.finish();
}
```

**If there is an `ActiveSpan`, it will act as the parent to any newly started `Span`** unless the programmer invokes `ignoreActiveSpan()` at `buildSpan()` time, like so:

```
io.opentracing.Tracer tracer = ...;
...
ActiveSpan span = tracer.buildSpan("someWork").ignoreActiveSpan().startActive();
```

### Deferring asynchronous work

Consider the case where a `Span`'s lifetime logically starts in one thread and ends in another. For instance, the Span's own internal timing breakdown might look like this:

```
[ ServiceHandlerSpan ]
|·FunctionA·|·····waiting on an RPC······|·FunctionB·|

---------------------------------------------------------> time
```

The `"ServiceHandlerSpan"` is _active_ while it's running FunctionA and FunctionB, and inactive while it's waiting on an RPC (presumably modelled as its own Span, though that's not the concern here).

**The `ActiveSpanSource` API makes it easy to `capture()` the Span and execution context in `FunctionA` and re-activate it in `FunctionB`.** Note that every `Tracer` implements `ActiveSpanSource`. These are the steps:

1. Start an `ActiveSpan` via `Tracer.startActive()` (or, if the `Span` was already started manually via `startManual()`, call `ActiveSpanSource#makeActive(span)`)
2. In the method that *allocates* the closure/`Runnable`/`Future`/etc, call `ActiveSpan#capture()` to obtain an `ActiveSpan.Continuation`
3. In the closure/`Runnable`/`Future`/etc itself, invoke `ActiveSpan.Continuation#activate` to re-activate the `ActiveSpan`, then `deactivate()` it when the Span is no longer active (or use try-with-resources for less typing).

For example:

```
io.opentracing.Tracer tracer = ...;
...
// STEP 1 ABOVE: start the ActiveSpan
try (ActiveSpan serviceSpan = tracer.buildSpan("ServiceHandlerSpan").startActive()) {
...

// STEP 2 ABOVE: capture the ActiveSpan
final ActiveSpan.Continuation cont = serviceSpan.capture();
doAsyncWork(new Runnable() {
@Override
public void run() {

// STEP 3 ABOVE: use the Continuation to reactivate the Span in the callback.
try (ActiveSpan activeSpan = cont.activate()) {
...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case where this Runnable exists solely/primarily to make a remote call to some other service it seems inconvenient to have to tracer.inject(cont.activate().context(), ...). I'm tempted to ask if a Continuation can be "serializable" for inter-process calls, at which point Continuation becomes the vehicle for context propagation intra- and inter-process?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting idea.

Typically the RPC instrumentation creates a child span just before making the RPC call, and in the same thread, even if the response is handled in the other thread. So the span and its context is usually available, without needing to use the Continuation to inject(...). And in case of synchronous calls forcing the creation of Continuation just to inject(...) is unnecessary overhead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it's an interesting idea... that said, I really like that the inject machinery only depends upon immutable SpanContexts... in various edge cases that's important.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a stupid question because I don't know much about Java. But in this example, if I have access to the outer cont variable in the Runnable (it's not passed as a parameter), why couldn't I just access the serviceSpan directly instead and write something like this - completely without the need of active spans, continuations etc.:

someMethod() {
    final Span serviceSpan = tracer.buildSpan("ServiceHandlerSpan").start();
    
    serviceSpan.setTag("key1", "value");

    doAsyncWork(new Runnable() {
        @Override
        public void run() {
            serviceSpan.setTag("key3", "value");
            serviceSpan.finish();
        }
    }

    serviceSpan.setTag("key2", "value");
}

Would this not be possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cwe1ss there are two problems. The larger one is that the Tracer.activeSpan() wouldn't be set up properly whene someMethod finishes (i.e., it wouldn't revert to whatever was active prior to the start() call). The smaller one is that – when forking and joining work – it is not always easy to know which callback finishes last, and the refcounting can help with that. But I consider the former issue to be way more important (and honestly the latter comes with a bunch of new failure modes, so it's not all gravy).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yes - I didn't think about the first issue. thx for the explanation! I wonder if Java will get async/await as well one day. It makes it sooo much easier!

}
}
});
}
```

In practice, all of this is most fluently accomplished through the use of an OpenTracing-aware `ExecutorService` and/or `Runnable`/`Callable` adapter; they factor out most of the typing.

#### Automatic `finish()`ing via `ActiveSpan` reference counts
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy with ThreadLocalActiveSpan being moved to opentracing-util, but this section still suggests that automatic finishing via ref counting will happen all the time. IMHO this section should be explicit on whether all tracers must implement ref counting (via ThreadLocalActiveSpan or any other means) or if it is a optional behavior that the tracer implementations can decide to support or not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the behavior of active span is different between implementations, then frameworks wouldn't know what the right way is to instrument their code. So the spec must be unambiguous that de-activating the last instance of the active span always finishes the span. Is there another way to achieve that without ref counting?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whether there's an AtomicInteger is an implementation detail, but I don't see how this spec could be implemented without reference counting of some sort.


When an `ActiveSpan` is created (either via `Tracer.SpanBuilder#startActive` or `ActiveSpanSource#makeActive(Span)`), the reference count associated with the `ActiveSpan` is `1`.

- When an `ActiveSpan.Continuation` is created via `ActiveSpan#capture`, the reference count **increments**
- When an `ActiveSpan.Continuation` is `ActiveSpan.Continuation#activate()`d and thus transformed back into an `ActiveSpan`, the reference count **is unchanged**
- When an `ActiveSpan` is `ActiveSpan#deactivate()`d, the reference count **decrements**

When the reference count decrements to zero, **the `Span`'s `finish()` method is invoked automatically.**

When used as designed, the programmer lets `ActiveSpan` and `ActiveSpan.Continuation` finish the `Span` as soon as the last active or deferred `ActiveSpan` is deactivated.

# Development

This is a maven project, and provides a wrapper, `./mvnw` to pin a consistent
Expand Down
Loading