Skip to content

Commit

Permalink
feat(logging): OpenTelemetry trace/span ID integration for Java loggi…
Browse files Browse the repository at this point in the history
…ng library (#1596)

* Add Otel support

* Use overloading setCurrentContext function

* Add logging handler test for traceEnhancer

* "Add tracehandler test"

* Add otel unit tests

* fix otel context unit test

* Remove comments

* fix test failures and dependency conflict

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* Add open-telemetry context dependency

* Resolve otel context dependency

* Make otel-context as test dependency

* make otel-context as compile dependency

* Add span context import

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* Add test and compile dependency

* Use transitive dependency

* Ignore otel context non-test warning

* comment otel-context

* Add otel current context detection

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* Add system tests

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* Remove current priority null check

* Add context handler unit tests

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* Add otel bom to pom

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* Remove unused dependency

* Remove comment

---------

Co-authored-by: cindy-peng <[email protected]>
Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed Jun 4, 2024
1 parent 5c7eb1a commit 67db829
Show file tree
Hide file tree
Showing 12 changed files with 871 additions and 34 deletions.
25 changes: 25 additions & 0 deletions google-cloud-logging/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 17,14 @@
<site.installationModule>google-cloud-logging</site.installationModule>
</properties>
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-context</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
Expand Down Expand Up @@ -133,6 141,23 @@
<artifactId>grpc-google-cloud-logging-v2</artifactId>
<scope>test</scope>
</dependency>
<!-- OpenTelemetry -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-testing</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-trace</artifactId>
<scope>test</scope>
</dependency>
<!-- END OpenTelemetry -->
<!-- Need testing utility classes for generated gRPC clients tests -->
<dependency>
<groupId>com.google.api</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 22,8 @@
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanContext;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
Expand All @@ -34,22 36,27 @@ public class Context {
private static final Pattern W3C_TRACE_CONTEXT_FORMAT =
Pattern.compile(
"^00-(?!00000000000000000000000000000000)[0-9a-f]{32}-(?!0000000000000000)[0-9a-f]{16}-[0-9a-f]{2}$");
// Trace sampled flag for bit masking
// see https://www.w3.org/TR/trace-context/#trace-flags for details
private static final byte FLAG_SAMPLED = 1; // 00000001
private final HttpRequest request;
private final String traceId;
private final String spanId;

private final boolean traceSampled;
/** A builder for {@see Context} objects. */
public static final class Builder {
private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
private String traceId;
private String spanId;
private boolean traceSampled;

Builder() {}

Builder(Context context) {
this.requestBuilder = context.request.toBuilder();
this.traceId = context.traceId;
this.spanId = context.spanId;
this.traceSampled = context.traceSampled;
}

/** Sets the HTTP request. */
Expand Down Expand Up @@ -118,17 125,28 @@ public Builder setSpanId(String spanId) {
return this;
}

/** Sets the boolean as trace sampled flag. */
@CanIgnoreReturnValue
public Builder setTraceSampled(boolean traceSampled) {
this.traceSampled = traceSampled;
return this;
}

/**
* Sets the trace id and span id values by parsing the string which represents xCloud Trace
* Context. The Cloud Trace Context is passed as {@code x-cloud-trace-context} header (can be in
* Pascal case format). The string format is <code>TRACE_ID/SPAN_ID;o=TRACE_TRUE</code>.
* Sets the trace id, span id and trace sampled flag values by parsing the string which
* represents xCloud Trace Context. The Cloud Trace Context is passed as {@code
* x-cloud-trace-context} header (can be in Pascal case format). The string format is <code>
* TRACE_ID/SPAN_ID;o=TRACE_TRUE</code>.
*
* @see <a href="https://cloud.google.com/trace/docs/setup#force-trace">Cloud Trace header
* format.</a>
*/
@CanIgnoreReturnValue
public Builder loadCloudTraceContext(String cloudTrace) {
if (cloudTrace != null) {
if (cloudTrace.indexOf("o=") >= 0) {
setTraceSampled(Iterables.get(Splitter.on("o=").split(cloudTrace), 1).equals("1"));
}
cloudTrace = Iterables.get(Splitter.on(';').split(cloudTrace), 0);
int split = cloudTrace.indexOf('/');
if (split >= 0) {
Expand All @@ -149,10 167,11 @@ public Builder loadCloudTraceContext(String cloudTrace) {
}

/**
* Sets the trace id and span id values by parsing the string which represents the standard W3C
* trace context propagation header. The context propagation header is passed as {@code
* traceparent} header. The method currently supports ONLY version {@code "00"}. The string
* format is <code>00-TRACE_ID-SPAN_ID-FLAGS</code>. field of the {@code version-format} value.
* Sets the trace id, span id and trace sampled flag values by parsing the string which
* represents the standard W3C trace context propagation header. The context propagation header
* is passed as {@code traceparent} header. The method currently supports ONLY version {@code
* "00"}. The string format is <code>00-TRACE_ID-SPAN_ID-FLAGS</code>. field of the {@code
* version-format} value.
*
* @see <a href=
* "https://www.w3.org/TR/trace-context/#traceparent-header-field-values">traceparent header
Expand All @@ -171,7 190,27 @@ public Builder loadW3CTraceParentContext(String traceParent) {
List<String> fields = Splitter.on('-').splitToList(traceParent);
setTraceId(fields.get(1));
setSpanId(fields.get(2));
// fields[3] contains flag(s)
boolean sampled = (Integer.parseInt(fields.get(3), 16) & FLAG_SAMPLED) == FLAG_SAMPLED;
setTraceSampled(sampled);
}
return this;
}

/**
* Sets the trace id, span id and trace sampled flag values by parsing detected OpenTelemetry
* span context.
*
* @see <a href="https://opentelemetry.io/docs/specs/otel/trace/api/#spancontext">OpenTelemetry
* SpanContext.</a>
*/
@CanIgnoreReturnValue
public Builder loadOpenTelemetryContext() {
io.opentelemetry.context.Context currentContext = io.opentelemetry.context.Context.current();
SpanContext spanContext = Span.fromContext(currentContext).getSpanContext();
if (spanContext != null && spanContext.isValid()) {
setTraceId(spanContext.getTraceId());
setSpanId(spanContext.getSpanId());
setTraceSampled(spanContext.isSampled());
}
return this;
}
Expand All @@ -191,6 230,7 @@ public Context build() {
}
this.traceId = builder.traceId;
this.spanId = builder.spanId;
this.traceSampled = builder.traceSampled;
}

public HttpRequest getHttpRequest() {
Expand All @@ -205,6 245,10 @@ public String getSpanId() {
return this.spanId;
}

public boolean getTraceSampled() {
return this.traceSampled;
}

@Override
public int hashCode() {
return Objects.hash(request, traceId, spanId);
Expand All @@ -216,6 260,7 @@ public String toString() {
.add("request", request)
.add("traceId", traceId)
.add("spanId", spanId)
.add("traceSampled", traceSampled)
.toString();
}

Expand All @@ -230,7 275,8 @@ public boolean equals(Object obj) {
Context other = (Context) obj;
return Objects.equals(request, other.request)
&& Objects.equals(traceId, other.traceId)
&& Objects.equals(spanId, other.spanId);
&& Objects.equals(spanId, other.spanId)
&& Objects.equals(traceSampled, other.traceSampled);
}

/** Returns a builder for this object. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 18,17 @@

/** Class provides a per-thread storage of the {@see Context} instances. */
public class ContextHandler {

public enum ContextPriority {
NO_INPUT,
XCLOUD_HEADER,
W3C_HEADER,
OTEL_EXTRACTED
}

private static final ThreadLocal<Context> contextHolder = initContextHolder();
private static final ThreadLocal<ContextPriority> currentPriority =
ThreadLocal.withInitial(() -> ContextPriority.NO_INPUT);

/**
* Initializes the context holder to {@link InheritableThreadLocal} if {@link LogManager}
Expand All @@ -41,10 51,45 @@ public Context getCurrentContext() {
}

public void setCurrentContext(Context context) {
contextHolder.set(context);
setCurrentContext(context, ContextPriority.NO_INPUT);
}

public ContextPriority getCurrentContextPriority() {
return currentPriority.get();
}

/**
* Sets the context based on the priority. Overrides traceId, spanId and TraceSampled if the
* passed priority is higher. HttpRequest values will be retrieved and combined from existing
* context if HttpRequest in the new context is empty .
*/
public void setCurrentContext(Context context, ContextPriority priority) {
if (priority != null && priority.compareTo(currentPriority.get()) >= 0 && context != null) {
Context.Builder combinedContextBuilder =
Context.newBuilder()
.setTraceId(context.getTraceId())
.setSpanId(context.getSpanId())
.setTraceSampled(context.getTraceSampled());
Context currentContext = getCurrentContext();

if (context.getHttpRequest() != null) {
combinedContextBuilder.setRequest(context.getHttpRequest());
}
// Combines HttpRequest from the existing context if HttpRequest in new context is empty.
else if (currentContext != null && currentContext.getHttpRequest() != null) {
combinedContextBuilder.setRequest(currentContext.getHttpRequest());
}

contextHolder.set(combinedContextBuilder.build());
currentPriority.set(priority);
}
}

public void removeCurrentContext() {
contextHolder.remove();
}

public void removeCurrentContextPriority() {
currentPriority.remove();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 171,7 @@ public enum LogTarget {

private final WriteOption[] defaultWriteOptions;

/** Creates an handler that publishes messages to Cloud Logging. */
/** Creates a handler that publishes messages to Cloud Logging. */
public LoggingHandler() {
this(null, null, null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 41,7 @@
import com.google.cloud.MonitoredResourceDescriptor;
import com.google.cloud.PageImpl;
import com.google.cloud.Tuple;
import com.google.cloud.logging.ContextHandler.ContextPriority;
import com.google.cloud.logging.spi.v2.LoggingRpc;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
Expand Down Expand Up @@ -89,6 90,7 @@
import com.google.logging.v2.WriteLogEntriesResponse;
import com.google.protobuf.Empty;
import com.google.protobuf.util.Durations;
import io.opentelemetry.api.trace.Span;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -822,7 824,7 @@ public Iterable<LogEntry> populateMetadata(
customResource == null
? MonitoredResourceUtil.getResource(getOptions().getProjectId(), null)
: customResource;
final Context context = new ContextHandler().getCurrentContext();

final ArrayList<LogEntry> populatedLogEntries = Lists.newArrayList();

// populate empty metadata fields of log entries before calling write API
Expand All @@ -834,13 836,23 @@ public Iterable<LogEntry> populateMetadata(
if (resourceMetadata != null && entry.getResource() == null) {
entityBuilder.setResource(resourceMetadata);
}

ContextHandler contextHandler = new ContextHandler();
// Populate trace/span ID from OpenTelemetry span context to logging context.
if (Span.current().getSpanContext().isValid()) {
Context.Builder contextBuilder = Context.newBuilder().loadOpenTelemetryContext();
contextHandler.setCurrentContext(contextBuilder.build(), ContextPriority.OTEL_EXTRACTED);
}

Context context = contextHandler.getCurrentContext();
if (context != null && entry.getHttpRequest() == null) {
entityBuilder.setHttpRequest(context.getHttpRequest());
}
if (context != null && Strings.isNullOrEmpty(entry.getTrace())) {
MonitoredResource resource =
entry.getResource() != null ? entry.getResource() : resourceMetadata;
entityBuilder.setTrace(getFormattedTrace(context.getTraceId(), resource));
entityBuilder.setTraceSampled(context.getTraceSampled());
}
if (context != null && Strings.isNullOrEmpty(entry.getSpanId())) {
entityBuilder.setSpanId(context.getSpanId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 24,8 @@ public TraceLoggingEnhancer() {}
public TraceLoggingEnhancer(String prefix) {}

private static final ThreadLocal<String> traceId = new ThreadLocal<>();
private static final ThreadLocal<String> spanId = new ThreadLocal<>();
private static final ThreadLocal<Boolean> traceSampled = new ThreadLocal<Boolean>();

/**
* Set the Trace ID associated with any logging done by the current thread.
Expand All @@ -38,20 40,72 @@ public static void setCurrentTraceId(String id) {
}
}

/**
* Set the Span ID associated with any logging done by the current thread.
*
* @param id The spanID
*/
public static void setCurrentSpanId(String id) {
if (id == null) {
spanId.remove();
} else {
spanId.set(id);
}
}

/**
* Set the trace sampled flag associated with any logging done by the current thread.
*
* @param isTraceSampled The traceSampled flag
*/
public static void setCurrentTraceSampled(Boolean isTraceSampled) {
if (isTraceSampled == null) {
traceSampled.remove();
} else {
traceSampled.set(isTraceSampled);
}
}

/**
* Get the Trace ID associated with any logging done by the current thread.
*
* @return id The traceID
* @return id The trace ID
*/
public static String getCurrentTraceId() {
return traceId.get();
}

/**
* Get the Span ID associated with any logging done by the current thread.
*
* @return id The span ID
*/
public static String getCurrentSpanId() {
return spanId.get();
}

/**
* Get the trace sampled flag associated with any logging done by the current thread.
*
* @return traceSampled The traceSampled flag
*/
public static Boolean getCurrentTraceSampled() {
return traceSampled.get();
}

@Override
public void enhanceLogEntry(LogEntry.Builder builder) {
String traceId = getCurrentTraceId();
if (traceId != null) {
builder.setTrace(traceId);
}
String spanId = getCurrentSpanId();
if (spanId != null) {
builder.setSpanId(spanId);
}
Boolean isTraceSampled = getCurrentTraceSampled();
if (isTraceSampled != null) {
builder.setTraceSampled(isTraceSampled);
}
}
}
Loading

0 comments on commit 67db829

Please sign in to comment.