Skip to content

Commit

Permalink
add extended tracer module (#759)
Browse files Browse the repository at this point in the history
Co-authored-by: Trask Stalnaker <[email protected]>
  • Loading branch information
zeitlinger and trask authored Nov 8, 2023
1 parent 1b7d0c6 commit 2983ea7
Show file tree
Hide file tree
Showing 9 changed files with 827 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/scripts/draft-change-log-entries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ component_names["maven-extension/"]="Maven extension"
component_names["micrometer-meter-provider/"]="Micrometer MeterProvider"
component_names["noop-api/"]="No-op API"
component_names["processors/"]="Telemetry processors"
component_names["extended-tracer/"]="Extended Tracer"
component_names["prometheus-client-bridge/"]="Prometheus client bridge"
component_names["runtime-attach/"]="Runtime attach"
component_names["resource-providers/"]="Resource providers"
Expand Down
168 changes: 168 additions & 0 deletions extended-tracer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# extended-tracer

Utility methods to make it easier to use the OpenTelemetry tracer.

## Usage Examples

Here are some examples how the utility methods can help reduce boilerplate code.

### Tracing a function

Before:

```java
Span span = tracer.spanBuilder("reset_checkout").startSpan();
String transactionId;
try (Scope scope = span.makeCurrent()) {
transactionId = resetCheckout(cartId);
} catch (Throwable e) {
span.setStatus(StatusCode.ERROR);
span.recordException(e);
throw e; // or throw new RuntimeException(e) - depending on your error handling strategy
} finally {
span.end();
}
```

After:

```java
Tracing tracing = new Tracing(openTelemetry, "service");
String transactionId = tracing.call("reset_checkout", () -> resetCheckout(cartId));
```

Note:

- Use `run` instead of `call` if the function returns `void`.
- Exceptions are re-thrown without modification - see [Exception handling](#exception-handling)
for more details.

### Trace context propagation

Before:

```java
Map<String, String> propagationHeaders = new HashMap<>();
openTelemetry
.getPropagators()
.getTextMapPropagator()
.inject(
Context.current(),
propagationHeaders,
(map, key, value) -> {
if (map != null) {
map.put(key, value);
}
});

// add propagationHeaders to request headers and call checkout service
```

```java
// in checkout service: get request headers into a Map<String, String> requestHeaders
Map<String, String> requestHeaders = new HashMap<>();
String cartId = "cartId";

SpanBuilder spanBuilder = tracer.spanBuilder("checkout_cart");
String transactionId;

TextMapGetter<Map<String, String>> TEXT_MAP_GETTER =
new TextMapGetter<Map<String, String>>() {
@Override
public Set<String> keys(Map<String, String> carrier) {
return carrier.keySet();
}

@Override
@Nullable
public String get(@Nullable Map<String, String> carrier, String key) {
//noinspection ConstantConditions
return carrier == null ? null : carrier.get(key);
}
};

Map<String, String> normalizedTransport =
requestHeaders.entrySet().stream()
.collect(
Collectors.toMap(
entry -> entry.getKey().toLowerCase(Locale.ROOT), Map.Entry::getValue));
Context newContext = openTelemetry
.getPropagators()
.getTextMapPropagator()
.extract(Context.current(), normalizedTransport, TEXT_MAP_GETTER);
try (Scope ignore = newContext.makeCurrent()) {
Span span = spanBuilder.setSpanKind(SERVER).startSpan();
try (Scope scope = span.makeCurrent()) {
transactionId = processCheckout(cartId);
} catch (Throwable e) {
span.setStatus(StatusCode.ERROR);
span.recordException(e);
throw e; // or throw new RuntimeException(e) - depending on your error handling strategy
} finally {
span.end();
}
}
```

After:

```java
Tracing tracing = new Tracing(openTelemetry, "service");
Map<String, String> propagationHeaders = tracing.getTextMapPropagationContext();
// add propagationHeaders to request headers and call checkout service
```

```java
// in checkout service: get request headers into a Map<String, String> requestHeaders
Map<String, String> requestHeaders = new HashMap<>();
String cartId = "cartId";

Tracing tracing = new Tracing(openTelemetry, "service");
String transactionId = tracing.traceServerSpan(requestHeaders,
tracer.spanBuilder("checkout_cart"), () -> processCheckout(cartId));
```

Note:

- You can use `traceConsumerSpan` if you want to trace a consumer (e.g. from a message queue)
instead of a server.
- Exceptions are re-thrown without modification - see [Exception handling](#exception-handling)
for more details.

### Setting baggage entries

Before:

```java
BaggageBuilder builder = Baggage.current().toBuilder();
builder.put("key", "value");
Context context = builder.build().storeInContext(Context.current());
try (Scope ignore = context.makeCurrent()) {
String value = Baggage.current().getEntryValue("key");
}
```

After:

```java
Tracing tracing = new Tracing(openTelemetry, "service");
String value = Tracing.callWithBaggage(
Collections.singletonMap("key", "value"),
() -> Baggage.current().getEntryValue("key"))
```

## Exception handling

`Tracing` re-throws exceptions without modification. This means you can catch exceptions around
`Tracing` calls and handle them as you would without `Tracing`.

Note that the `Tracing` methods do not declare any checked exceptions
(the idea is taken from [@SneakyThrows](https://projectlombok.org/features/SneakyThrows)).
Declaring a checked exception would force callers to handle it, which would create a lot of
boilerplate code. Instead, `Tracing` re-throws checked exceptions as unchecked exceptions.

## Component owners

- [Gregor Zeitlinger](https://github.com/zeitlinger), Grafana Labs

Learn more about component owners in [component_owners.yml](../.github/component_owners.yml).
12 changes: 12 additions & 0 deletions extended-tracer/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugins {
id("otel.java-conventions")
}

description = "Extended Tracer"
otelJava.moduleName.set("io.opentelemetry.contrib.extended-tracer")

dependencies {
api("io.opentelemetry:opentelemetry-api")
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
testImplementation("io.opentelemetry.semconv:opentelemetry-semconv")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.tracer;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;

/**
* Utility class to simplify context propagation.
*
* <p>The <a
* href="https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/extended-tracer/README.md">README</a>
* explains the use cases in more detail.
*/
public final class Propagation {

private Propagation() {}

private static final TextMapGetter<Map<String, String>> TEXT_MAP_GETTER =
new TextMapGetter<Map<String, String>>() {
@Override
public Set<String> keys(Map<String, String> carrier) {
return carrier.keySet();
}

@Override
@Nullable
public String get(@Nullable Map<String, String> carrier, String key) {
//noinspection ConstantConditions
return carrier == null ? null : carrier.get(key);
}
};

/**
* Injects the current context into a string map, which can then be added to HTTP headers or the
* metadata of an event.
*/
public static Map<String, String> getTextMapPropagationContext(OpenTelemetry openTelemetry) {
Map<String, String> carrier = new HashMap<>();
//noinspection ConstantConditions
openTelemetry
.getPropagators()
.getTextMapPropagator()
.inject(
Context.current(),
carrier,
(map, key, value) -> {
if (map != null) {
map.put(key, value);
}
});

return carrier;
}

/**
* Extract the context from a string map, which you get from HTTP headers of the metadata of an
* event you're processing.
*
* @param carrier the string map
*/
static Context extractTextMapPropagationContext(
OpenTelemetry openTelemetry, Map<String, String> carrier) {
Context current = Context.current();
//noinspection ConstantConditions
if (carrier == null) {
return current;
}
// HTTP headers are case-insensitive. As we're using Map, which is case-sensitive, we need to
// normalize all the keys
Map<String, String> normalizedCarrier =
carrier.entrySet().stream()
.collect(
Collectors.toMap(
entry -> entry.getKey().toLowerCase(Locale.ROOT), Map.Entry::getValue));
return openTelemetry
.getPropagators()
.getTextMapPropagator()
.extract(current, normalizedCarrier, TEXT_MAP_GETTER);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.tracer;

/**
* An interface for creating a lambda that is wrapped in a span, returns a value, and that may
* throw, similar to <a
* href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/support/TransactionCallback.html">TransactionCallback</a>.
*
* @param <E> Thrown exception type.
*/
@FunctionalInterface
public interface SpanCallback<T, E extends Throwable> {
T doInSpan() throws E;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.tracer;

/**
* An interface for creating a lambda that is wrapped in a span and that may throw, similar to <a
* href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/support/TransactionCallback.html">TransactionCallback</a>.
*
* @param <E> Thrown exception type.
*/
@FunctionalInterface
public interface SpanRunnable<E extends Throwable> {
void doInSpan() throws E;
}
Loading

0 comments on commit 2983ea7

Please sign in to comment.