Skip to content

Commit

Permalink
Extended tracer (#6017)
Browse files Browse the repository at this point in the history
Co-authored-by: jack-berg <34418638+jack-berg@users.noreply.github.com>
Co-authored-by: Jack Berg <jberg@newrelic.com>
  • Loading branch information
3 people authored Dec 7, 2023
1 parent c8e1e36 commit 2b33f9f
Show file tree
Hide file tree
Showing 9 changed files with 734 additions and 109 deletions.
167 changes: 167 additions & 0 deletions extensions/incubator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# ExtendedTracer

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:

<!-- markdownlint-disable -->
```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();
}
```
<!-- markdownlint-enable -->

After:

```java
import io.opentelemetry.extension.incubator.trace.ExtendedTracer;

ExtendedTracer extendedTracer = ExtendedTracer.create(tracer);
String transactionId = extendedTracer.spanBuilder("reset_checkout").startAndCall(() -> resetCheckout(cartId));
```

If you want to set attributes on the span, you can use the `startAndCall` method on the span builder:

```java
import io.opentelemetry.extension.incubator.trace.ExtendedTracer;

ExtendedTracer extendedTracer = ExtendedTracer.create(tracer);
String transactionId = extendedTracer.spanBuilder("reset_checkout")
.setAttribute("foo", "bar")
.startAndCall(() -> resetCheckout(cartId));
```

Note:

- Use `startAndRun` instead of `startAndCall` if the function returns `void` (both on the tracer and span builder).
- 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
```

<!-- markdownlint-disable -->
```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");

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) {
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);
String transactionId;
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();
}
}
```
<!-- markdownlint-enable -->

After:

```java
import io.opentelemetry.extension.incubator.propagation.ExtendedContextPropagators;

Map<String, String> propagationHeaders =
ExtendedContextPropagators.getTextMapPropagationContext(openTelemetry.getPropagators());
// add propagationHeaders to request headers and call checkout service
```

```java
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.extension.incubator.trace.ExtendedTracer;

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

ExtendedTracer extendedTracer = ExtendedTracer.create(tracer);
String transactionId = extendedTracer.spanBuilder("checkout_cart")
.setSpanKind(SpanKind.SERVER)
.setParentFrom(openTelemetry.getPropagators(), requestHeaders)
.startAndCall(() -> processCheckout(cartId));
```

## Exception handling

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

When an exception is encountered during an `ExtendedTracer` call, the span is marked as error and
the exception is recorded.

If you want to customize this behaviour, e.g. to only record the exception, because you are
able to recover from the error, you can call the overloaded method of `startAndCall` or
`startAndRun` that takes an exception handler:

```java
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.extension.incubator.trace.ExtendedTracer;

ExtendedTracer extendedTracer = ExtendedTracer.create(tracer);
String transactionId = extendedTracer.spanBuilder("checkout_cart")
.startAndCall(() -> processCheckout(cartId), Span::recordException);
```
1 change: 1 addition & 0 deletions extensions/incubator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ dependencies {

annotationProcessor("com.google.auto.value:auto-value")

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

package io.opentelemetry.extension.incubator.propagation;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;

class CaseInsensitiveMap extends HashMap<String, String> {

private static final long serialVersionUID = -4202518750189126871L;

CaseInsensitiveMap(Map<String, String> carrier) {
super(carrier);
}

@Override
public String put(String key, String value) {
return super.put(key.toLowerCase(Locale.ROOT), value);
}

@Override
@Nullable
public String get(Object key) {
return super.get(((String) key).toLowerCase(Locale.ROOT));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.extension.incubator.propagation;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.context.propagation.TextMapGetter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
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 ExtendedContextPropagators {

private ExtendedContextPropagators() {}

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) {
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 a message.
*
* @param propagators provide the propagators from {@link OpenTelemetry#getPropagators()}
*/
public static Map<String, String> getTextMapPropagationContext(ContextPropagators propagators) {
Map<String, String> carrier = new HashMap<>();
propagators
.getTextMapPropagator()
.inject(
Context.current(),
carrier,
(map, key, value) -> {
if (map != null) {
map.put(key, value);
}
});

return Collections.unmodifiableMap(carrier);
}

/**
* Extract the context from a string map, which you get from HTTP headers of the metadata of a
* message you're processing.
*
* @param carrier the string map
* @param propagators provide the propagators from {@link OpenTelemetry#getPropagators()}
*/
public static Context extractTextMapPropagationContext(
Map<String, String> carrier, ContextPropagators propagators) {
Context current = Context.current();
if (carrier == null) {
return current;
}
CaseInsensitiveMap caseInsensitiveMap = new CaseInsensitiveMap(carrier);
return propagators.getTextMapPropagator().extract(current, caseInsensitiveMap, TEXT_MAP_GETTER);
}
}
Loading

0 comments on commit 2b33f9f

Please sign in to comment.