Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(logging): OpenTelemetry trace/span ID integration for Java logging library #1596

Merged
merged 32 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
da8bd86
Add Otel support
Mar 21, 2024
ab65496
Use overloading setCurrentContext function
Mar 26, 2024
246c1f5
Add logging handler test for traceEnhancer
Apr 25, 2024
107144b
"Add tracehandler test"
Apr 30, 2024
b4d3ee7
Add otel unit tests
Apr 30, 2024
4808c99
fix otel context unit test
Apr 30, 2024
8416b66
Remove comments
Apr 30, 2024
7ca6f19
fix test failures and dependency conflict
Apr 30, 2024
72aa518
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Apr 30, 2024
db7f163
Merge branch 'main' into cindy/span-fix-0320
cindy-peng May 2, 2024
e67c907
Add open-telemetry context dependency
May 2, 2024
42a49e1
Resolve otel context dependency
May 3, 2024
e4c3f06
Make otel-context as test dependency
May 3, 2024
9f09a85
make otel-context as compile dependency
May 3, 2024
cefcd73
Add span context import
May 4, 2024
c600f0d
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] May 4, 2024
1d48166
Add test and compile dependency
May 4, 2024
0e61c7d
Use transitive dependency
May 4, 2024
9cf24ab
Ignore otel context non-test warning
May 4, 2024
e59fd6b
comment otel-context
May 5, 2024
4ff6738
Add otel current context detection
May 5, 2024
d560806
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] May 5, 2024
c8e4eca
Add system tests
May 6, 2024
ae60311
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] May 6, 2024
43d69d0
Remove current priority null check
May 7, 2024
2d8345e
Add context handler unit tests
May 7, 2024
a8bba3c
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] May 7, 2024
e035733
Merge branch 'main' into cindy/span-fix-0320
cindy-peng May 29, 2024
4270847
Add otel bom to pom
Jun 3, 2024
e4b8de1
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Jun 3, 2024
fe58cd8
Remove unused dependency
Jun 3, 2024
ba86d25
Remove comment
Jun 3, 2024
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,20 @@ If you are using Maven without the BOM, add this to your dependencies:
If you are using Gradle 5.x or later, add this to your dependencies:

```Groovy
implementation platform('com.google.cloud:libraries-bom:26.34.0')
implementation platform('com.google.cloud:libraries-bom:26.37.0')

implementation 'com.google.cloud:google-cloud-logging'
```
If you are using Gradle without BOM, add this to your dependencies:

```Groovy
implementation 'com.google.cloud:google-cloud-logging:3.16.1'
implementation 'com.google.cloud:google-cloud-logging:3.17.0'
```

If you are using SBT, add this to your dependencies:

```Scala
libraryDependencies += "com.google.cloud" % "google-cloud-logging" % "3.16.1"
libraryDependencies += "com.google.cloud" % "google-cloud-logging" % "3.17.0"
```
<!-- {x-version-update-end} -->

Expand Down Expand Up @@ -452,7 +452,7 @@ Java is a registered trademark of Oracle and/or its affiliates.
[kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-logging/java11.html
[stability-image]: https://img.shields.io/badge/stability-stable-green
[maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-logging.svg
[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-logging/3.16.1
[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-logging/3.17.0
[authentication]: https://github.com/googleapis/google-cloud-java#authentication
[auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
[predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles
Expand Down
16 changes: 16 additions & 0 deletions google-cloud-logging/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<site.installationModule>google-cloud-logging</site.installationModule>
</properties>
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
Expand Down Expand Up @@ -133,6 +137,18 @@
<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>
<!-- 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,7 @@
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 java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
Expand All @@ -34,22 +35,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
gkevinzheng marked this conversation as resolved.
Show resolved Hide resolved
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 +124,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 +166,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 +189,25 @@ 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() {
if (Span.current().getSpanContext() != null && Span.current().getSpanContext().isValid()) {
setTraceId(Span.current().getSpanContext().getTraceId());
setSpanId(Span.current().getSpanContext().getSpanId());
setTraceSampled(Span.current().getSpanContext().isSampled());
}
return this;
}
Expand All @@ -191,6 +227,7 @@ public Context build() {
}
this.traceId = builder.traceId;
this.spanId = builder.spanId;
this.traceSampled = builder.traceSampled;
}

public HttpRequest getHttpRequest() {
Expand All @@ -205,6 +242,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 +257,7 @@ public String toString() {
.add("request", request)
.add("traceId", traceId)
.add("spanId", spanId)
.add("traceSampled", traceSampled)
.toString();
}

Expand All @@ -230,7 +272,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,
W3_HEADER,
OTEL_EXTRACTED
}

private static final ThreadLocal<Context> contextHolder = initContextHolder();
private static final ThreadLocal<ContextPriority> currentPriority =
new ThreadLocal<ContextPriority>();
cindy-peng marked this conversation as resolved.
Show resolved Hide resolved

/**
* Initializes the context holder to {@link InheritableThreadLocal} if {@link LogManager}
Expand All @@ -44,6 +54,34 @@ public void setCurrentContext(Context context) {
contextHolder.set(context);
}

/**
* 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 ((currentPriority.get() == null || priority.compareTo(currentPriority.get()) >= 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

If currentPriority gets initialized to NO_INPUT, is it possible for it to ever be null?

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 have added priority parameter null check and remove currentPriority null check, now I don't think currentPriority will be null.

&& 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();
}
Expand Down
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);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you set the ContextPriority for the other modes as well? I only see OTEL_EXTRACTED being set in this PR.

Copy link
Contributor Author

@cindy-peng cindy-peng May 7, 2024

Choose a reason for hiding this comment

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

Unlike other logging libraries, java-logging library doesn't call other setCurrentContext() within the library itself. It's usually delegated to customers or integration library. An example is mentioned here (2nd approach to set trace for log entries):

RequestContextFilter in java-logging-servlet-initializer repository will call setCurrentContext () and load tracing context from cloud tracing or W3C headers.

And we do have a plan mentioned here to switch to the new setCurrentContext(Context context, ContextPriority priority) in java-logging-servlet-initializer after the change in this repository is released.

I have also added some unit tests here for setCurrentContext(Context context, ContextPriority priority): https://github.com/googleapis/java-logging/pull/1596/files#diff-cca9976e4f3cf85575f1f366cb06f8047e56f4888f16340facf158dfa7ec6721

}

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
Loading