Skip to content

Commit 67db829

Browse files
cindy-pengcindy-penggcf-owl-bot[bot]
authored
feat(logging): OpenTelemetry trace/span ID integration for Java logging 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>
1 parent 5c7eb1a commit 67db829

File tree

12 files changed

+871
-34
lines changed

12 files changed

+871
-34
lines changed

google-cloud-logging/pom.xml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@
1717
<site.installationModule>google-cloud-logging</site.installationModule>
1818
</properties>
1919
<dependencies>
20+
<dependency>
21+
<groupId>io.opentelemetry</groupId>
22+
<artifactId>opentelemetry-api</artifactId>
23+
</dependency>
24+
<dependency>
25+
<groupId>io.opentelemetry</groupId>
26+
<artifactId>opentelemetry-context</artifactId>
27+
</dependency>
2028
<dependency>
2129
<groupId>com.google.guava</groupId>
2230
<artifactId>guava</artifactId>
@@ -133,6 +141,23 @@
133141
<artifactId>grpc-google-cloud-logging-v2</artifactId>
134142
<scope>test</scope>
135143
</dependency>
144+
<!-- OpenTelemetry -->
145+
<dependency>
146+
<groupId>io.opentelemetry</groupId>
147+
<artifactId>opentelemetry-sdk</artifactId>
148+
<scope>test</scope>
149+
</dependency>
150+
<dependency>
151+
<groupId>io.opentelemetry</groupId>
152+
<artifactId>opentelemetry-sdk-testing</artifactId>
153+
<scope>test</scope>
154+
</dependency>
155+
<dependency>
156+
<groupId>io.opentelemetry</groupId>
157+
<artifactId>opentelemetry-sdk-trace</artifactId>
158+
<scope>test</scope>
159+
</dependency>
160+
<!-- END OpenTelemetry -->
136161
<!-- Need testing utility classes for generated gRPC clients tests -->
137162
<dependency>
138163
<groupId>com.google.api</groupId>

google-cloud-logging/src/main/java/com/google/cloud/logging/Context.java

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import com.google.common.base.Splitter;
2323
import com.google.common.collect.Iterables;
2424
import com.google.errorprone.annotations.CanIgnoreReturnValue;
25+
import io.opentelemetry.api.trace.Span;
26+
import io.opentelemetry.api.trace.SpanContext;
2527
import java.util.List;
2628
import java.util.Objects;
2729
import java.util.regex.Matcher;
@@ -34,22 +36,27 @@ public class Context {
3436
private static final Pattern W3C_TRACE_CONTEXT_FORMAT =
3537
Pattern.compile(
3638
"^00-(?!00000000000000000000000000000000)[0-9a-f]{32}-(?!0000000000000000)[0-9a-f]{16}-[0-9a-f]{2}$");
39+
// Trace sampled flag for bit masking
40+
// see https://www.w3.org/TR/trace-context/#trace-flags for details
41+
private static final byte FLAG_SAMPLED = 1; // 00000001
3742
private final HttpRequest request;
3843
private final String traceId;
3944
private final String spanId;
40-
45+
private final boolean traceSampled;
4146
/** A builder for {@see Context} objects. */
4247
public static final class Builder {
4348
private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
4449
private String traceId;
4550
private String spanId;
51+
private boolean traceSampled;
4652

4753
Builder() {}
4854

4955
Builder(Context context) {
5056
this.requestBuilder = context.request.toBuilder();
5157
this.traceId = context.traceId;
5258
this.spanId = context.spanId;
59+
this.traceSampled = context.traceSampled;
5360
}
5461

5562
/** Sets the HTTP request. */
@@ -118,17 +125,28 @@ public Builder setSpanId(String spanId) {
118125
return this;
119126
}
120127

128+
/** Sets the boolean as trace sampled flag. */
129+
@CanIgnoreReturnValue
130+
public Builder setTraceSampled(boolean traceSampled) {
131+
this.traceSampled = traceSampled;
132+
return this;
133+
}
134+
121135
/**
122-
* Sets the trace id and span id values by parsing the string which represents xCloud Trace
123-
* Context. The Cloud Trace Context is passed as {@code x-cloud-trace-context} header (can be in
124-
* Pascal case format). The string format is <code>TRACE_ID/SPAN_ID;o=TRACE_TRUE</code>.
136+
* Sets the trace id, span id and trace sampled flag values by parsing the string which
137+
* represents xCloud Trace Context. The Cloud Trace Context is passed as {@code
138+
* x-cloud-trace-context} header (can be in Pascal case format). The string format is <code>
139+
* TRACE_ID/SPAN_ID;o=TRACE_TRUE</code>.
125140
*
126141
* @see <a href="https://cloud.google.com/trace/docs/setup#force-trace">Cloud Trace header
127142
* format.</a>
128143
*/
129144
@CanIgnoreReturnValue
130145
public Builder loadCloudTraceContext(String cloudTrace) {
131146
if (cloudTrace != null) {
147+
if (cloudTrace.indexOf("o=") >= 0) {
148+
setTraceSampled(Iterables.get(Splitter.on("o=").split(cloudTrace), 1).equals("1"));
149+
}
132150
cloudTrace = Iterables.get(Splitter.on(';').split(cloudTrace), 0);
133151
int split = cloudTrace.indexOf('/');
134152
if (split >= 0) {
@@ -149,10 +167,11 @@ public Builder loadCloudTraceContext(String cloudTrace) {
149167
}
150168

151169
/**
152-
* Sets the trace id and span id values by parsing the string which represents the standard W3C
153-
* trace context propagation header. The context propagation header is passed as {@code
154-
* traceparent} header. The method currently supports ONLY version {@code "00"}. The string
155-
* format is <code>00-TRACE_ID-SPAN_ID-FLAGS</code>. field of the {@code version-format} value.
170+
* Sets the trace id, span id and trace sampled flag values by parsing the string which
171+
* represents the standard W3C trace context propagation header. The context propagation header
172+
* is passed as {@code traceparent} header. The method currently supports ONLY version {@code
173+
* "00"}. The string format is <code>00-TRACE_ID-SPAN_ID-FLAGS</code>. field of the {@code
174+
* version-format} value.
156175
*
157176
* @see <a href=
158177
* "https://www.w3.org/TR/trace-context/#traceparent-header-field-values">traceparent header
@@ -171,7 +190,27 @@ public Builder loadW3CTraceParentContext(String traceParent) {
171190
List<String> fields = Splitter.on('-').splitToList(traceParent);
172191
setTraceId(fields.get(1));
173192
setSpanId(fields.get(2));
174-
// fields[3] contains flag(s)
193+
boolean sampled = (Integer.parseInt(fields.get(3), 16) & FLAG_SAMPLED) == FLAG_SAMPLED;
194+
setTraceSampled(sampled);
195+
}
196+
return this;
197+
}
198+
199+
/**
200+
* Sets the trace id, span id and trace sampled flag values by parsing detected OpenTelemetry
201+
* span context.
202+
*
203+
* @see <a href="https://opentelemetry.io/docs/specs/otel/trace/api/#spancontext">OpenTelemetry
204+
* SpanContext.</a>
205+
*/
206+
@CanIgnoreReturnValue
207+
public Builder loadOpenTelemetryContext() {
208+
io.opentelemetry.context.Context currentContext = io.opentelemetry.context.Context.current();
209+
SpanContext spanContext = Span.fromContext(currentContext).getSpanContext();
210+
if (spanContext != null && spanContext.isValid()) {
211+
setTraceId(spanContext.getTraceId());
212+
setSpanId(spanContext.getSpanId());
213+
setTraceSampled(spanContext.isSampled());
175214
}
176215
return this;
177216
}
@@ -191,6 +230,7 @@ public Context build() {
191230
}
192231
this.traceId = builder.traceId;
193232
this.spanId = builder.spanId;
233+
this.traceSampled = builder.traceSampled;
194234
}
195235

196236
public HttpRequest getHttpRequest() {
@@ -205,6 +245,10 @@ public String getSpanId() {
205245
return this.spanId;
206246
}
207247

248+
public boolean getTraceSampled() {
249+
return this.traceSampled;
250+
}
251+
208252
@Override
209253
public int hashCode() {
210254
return Objects.hash(request, traceId, spanId);
@@ -216,6 +260,7 @@ public String toString() {
216260
.add("request", request)
217261
.add("traceId", traceId)
218262
.add("spanId", spanId)
263+
.add("traceSampled", traceSampled)
219264
.toString();
220265
}
221266

@@ -230,7 +275,8 @@ public boolean equals(Object obj) {
230275
Context other = (Context) obj;
231276
return Objects.equals(request, other.request)
232277
&& Objects.equals(traceId, other.traceId)
233-
&& Objects.equals(spanId, other.spanId);
278+
&& Objects.equals(spanId, other.spanId)
279+
&& Objects.equals(traceSampled, other.traceSampled);
234280
}
235281

236282
/** Returns a builder for this object. */

google-cloud-logging/src/main/java/com/google/cloud/logging/ContextHandler.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,17 @@
1818

1919
/** Class provides a per-thread storage of the {@see Context} instances. */
2020
public class ContextHandler {
21+
22+
public enum ContextPriority {
23+
NO_INPUT,
24+
XCLOUD_HEADER,
25+
W3C_HEADER,
26+
OTEL_EXTRACTED
27+
}
28+
2129
private static final ThreadLocal<Context> contextHolder = initContextHolder();
30+
private static final ThreadLocal<ContextPriority> currentPriority =
31+
ThreadLocal.withInitial(() -> ContextPriority.NO_INPUT);
2232

2333
/**
2434
* Initializes the context holder to {@link InheritableThreadLocal} if {@link LogManager}
@@ -41,10 +51,45 @@ public Context getCurrentContext() {
4151
}
4252

4353
public void setCurrentContext(Context context) {
44-
contextHolder.set(context);
54+
setCurrentContext(context, ContextPriority.NO_INPUT);
55+
}
56+
57+
public ContextPriority getCurrentContextPriority() {
58+
return currentPriority.get();
59+
}
60+
61+
/**
62+
* Sets the context based on the priority. Overrides traceId, spanId and TraceSampled if the
63+
* passed priority is higher. HttpRequest values will be retrieved and combined from existing
64+
* context if HttpRequest in the new context is empty .
65+
*/
66+
public void setCurrentContext(Context context, ContextPriority priority) {
67+
if (priority != null && priority.compareTo(currentPriority.get()) >= 0 && context != null) {
68+
Context.Builder combinedContextBuilder =
69+
Context.newBuilder()
70+
.setTraceId(context.getTraceId())
71+
.setSpanId(context.getSpanId())
72+
.setTraceSampled(context.getTraceSampled());
73+
Context currentContext = getCurrentContext();
74+
75+
if (context.getHttpRequest() != null) {
76+
combinedContextBuilder.setRequest(context.getHttpRequest());
77+
}
78+
// Combines HttpRequest from the existing context if HttpRequest in new context is empty.
79+
else if (currentContext != null && currentContext.getHttpRequest() != null) {
80+
combinedContextBuilder.setRequest(currentContext.getHttpRequest());
81+
}
82+
83+
contextHolder.set(combinedContextBuilder.build());
84+
currentPriority.set(priority);
85+
}
4586
}
4687

4788
public void removeCurrentContext() {
4889
contextHolder.remove();
4990
}
91+
92+
public void removeCurrentContextPriority() {
93+
currentPriority.remove();
94+
}
5095
}

google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ public enum LogTarget {
171171

172172
private final WriteOption[] defaultWriteOptions;
173173

174-
/** Creates an handler that publishes messages to Cloud Logging. */
174+
/** Creates a handler that publishes messages to Cloud Logging. */
175175
public LoggingHandler() {
176176
this(null, null, null);
177177
}

google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingImpl.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import com.google.cloud.MonitoredResourceDescriptor;
4242
import com.google.cloud.PageImpl;
4343
import com.google.cloud.Tuple;
44+
import com.google.cloud.logging.ContextHandler.ContextPriority;
4445
import com.google.cloud.logging.spi.v2.LoggingRpc;
4546
import com.google.common.annotations.VisibleForTesting;
4647
import com.google.common.base.Ascii;
@@ -89,6 +90,7 @@
8990
import com.google.logging.v2.WriteLogEntriesResponse;
9091
import com.google.protobuf.Empty;
9192
import com.google.protobuf.util.Durations;
93+
import io.opentelemetry.api.trace.Span;
9294
import java.text.ParseException;
9395
import java.util.ArrayList;
9496
import java.util.List;
@@ -822,7 +824,7 @@ public Iterable<LogEntry> populateMetadata(
822824
customResource == null
823825
? MonitoredResourceUtil.getResource(getOptions().getProjectId(), null)
824826
: customResource;
825-
final Context context = new ContextHandler().getCurrentContext();
827+
826828
final ArrayList<LogEntry> populatedLogEntries = Lists.newArrayList();
827829

828830
// populate empty metadata fields of log entries before calling write API
@@ -834,13 +836,23 @@ public Iterable<LogEntry> populateMetadata(
834836
if (resourceMetadata != null && entry.getResource() == null) {
835837
entityBuilder.setResource(resourceMetadata);
836838
}
839+
840+
ContextHandler contextHandler = new ContextHandler();
841+
// Populate trace/span ID from OpenTelemetry span context to logging context.
842+
if (Span.current().getSpanContext().isValid()) {
843+
Context.Builder contextBuilder = Context.newBuilder().loadOpenTelemetryContext();
844+
contextHandler.setCurrentContext(contextBuilder.build(), ContextPriority.OTEL_EXTRACTED);
845+
}
846+
847+
Context context = contextHandler.getCurrentContext();
837848
if (context != null && entry.getHttpRequest() == null) {
838849
entityBuilder.setHttpRequest(context.getHttpRequest());
839850
}
840851
if (context != null && Strings.isNullOrEmpty(entry.getTrace())) {
841852
MonitoredResource resource =
842853
entry.getResource() != null ? entry.getResource() : resourceMetadata;
843854
entityBuilder.setTrace(getFormattedTrace(context.getTraceId(), resource));
855+
entityBuilder.setTraceSampled(context.getTraceSampled());
844856
}
845857
if (context != null && Strings.isNullOrEmpty(entry.getSpanId())) {
846858
entityBuilder.setSpanId(context.getSpanId());

google-cloud-logging/src/main/java/com/google/cloud/logging/TraceLoggingEnhancer.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public TraceLoggingEnhancer() {}
2424
public TraceLoggingEnhancer(String prefix) {}
2525

2626
private static final ThreadLocal<String> traceId = new ThreadLocal<>();
27+
private static final ThreadLocal<String> spanId = new ThreadLocal<>();
28+
private static final ThreadLocal<Boolean> traceSampled = new ThreadLocal<Boolean>();
2729

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

43+
/**
44+
* Set the Span ID associated with any logging done by the current thread.
45+
*
46+
* @param id The spanID
47+
*/
48+
public static void setCurrentSpanId(String id) {
49+
if (id == null) {
50+
spanId.remove();
51+
} else {
52+
spanId.set(id);
53+
}
54+
}
55+
56+
/**
57+
* Set the trace sampled flag associated with any logging done by the current thread.
58+
*
59+
* @param isTraceSampled The traceSampled flag
60+
*/
61+
public static void setCurrentTraceSampled(Boolean isTraceSampled) {
62+
if (isTraceSampled == null) {
63+
traceSampled.remove();
64+
} else {
65+
traceSampled.set(isTraceSampled);
66+
}
67+
}
68+
4169
/**
4270
* Get the Trace ID associated with any logging done by the current thread.
4371
*
44-
* @return id The traceID
72+
* @return id The trace ID
4573
*/
4674
public static String getCurrentTraceId() {
4775
return traceId.get();
4876
}
4977

78+
/**
79+
* Get the Span ID associated with any logging done by the current thread.
80+
*
81+
* @return id The span ID
82+
*/
83+
public static String getCurrentSpanId() {
84+
return spanId.get();
85+
}
86+
87+
/**
88+
* Get the trace sampled flag associated with any logging done by the current thread.
89+
*
90+
* @return traceSampled The traceSampled flag
91+
*/
92+
public static Boolean getCurrentTraceSampled() {
93+
return traceSampled.get();
94+
}
95+
5096
@Override
5197
public void enhanceLogEntry(LogEntry.Builder builder) {
5298
String traceId = getCurrentTraceId();
5399
if (traceId != null) {
54100
builder.setTrace(traceId);
55101
}
102+
String spanId = getCurrentSpanId();
103+
if (spanId != null) {
104+
builder.setSpanId(spanId);
105+
}
106+
Boolean isTraceSampled = getCurrentTraceSampled();
107+
if (isTraceSampled != null) {
108+
builder.setTraceSampled(isTraceSampled);
109+
}
56110
}
57111
}

0 commit comments

Comments
 (0)