From 67db829621fd1c4a876d158fe1afb4927821fa54 Mon Sep 17 00:00:00 2001 From: Cindy Peng <148148319+cindy-peng@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:59:57 +0800 Subject: [PATCH] feat(logging): OpenTelemetry trace/span ID integration for Java logging library (#1596) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: Owl Bot --- google-cloud-logging/pom.xml | 25 ++ .../com/google/cloud/logging/Context.java | 66 ++++- .../google/cloud/logging/ContextHandler.java | 47 ++- .../google/cloud/logging/LoggingHandler.java | 2 +- .../com/google/cloud/logging/LoggingImpl.java | 14 +- .../cloud/logging/TraceLoggingEnhancer.java | 56 +++- .../logging/AutoPopulateMetadataTests.java | 27 +- .../cloud/logging/ContextHandlerTest.java | 280 ++++++++++++++++++ .../com/google/cloud/logging/ContextTest.java | 73 ++++- .../cloud/logging/LoggingHandlerTest.java | 4 + .../cloud/logging/it/ITTracingLogsTest.java | 244 +++++++++++++++ pom.xml | 67 +++++ 12 files changed, 871 insertions(+), 34 deletions(-) create mode 100644 google-cloud-logging/src/test/java/com/google/cloud/logging/ContextHandlerTest.java create mode 100644 google-cloud-logging/src/test/java/com/google/cloud/logging/it/ITTracingLogsTest.java diff --git a/google-cloud-logging/pom.xml b/google-cloud-logging/pom.xml index 2f5ba7c9e..38a84b92c 100644 --- a/google-cloud-logging/pom.xml +++ b/google-cloud-logging/pom.xml @@ -17,6 +17,14 @@ google-cloud-logging + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-context + com.google.guava guava @@ -133,6 +141,23 @@ grpc-google-cloud-logging-v2 test + + + io.opentelemetry + opentelemetry-sdk + test + + + io.opentelemetry + opentelemetry-sdk-testing + test + + + io.opentelemetry + opentelemetry-sdk-trace + test + + com.google.api diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/Context.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/Context.java index 109edfafc..3466ecd2c 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/Context.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/Context.java @@ -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; @@ -34,15 +36,19 @@ 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() {} @@ -50,6 +56,7 @@ public static final class Builder { this.requestBuilder = context.request.toBuilder(); this.traceId = context.traceId; this.spanId = context.spanId; + this.traceSampled = context.traceSampled; } /** Sets the HTTP request. */ @@ -118,10 +125,18 @@ 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 TRACE_ID/SPAN_ID;o=TRACE_TRUE. + * 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 + * TRACE_ID/SPAN_ID;o=TRACE_TRUE. * * @see Cloud Trace header * format. @@ -129,6 +144,9 @@ public Builder setSpanId(String spanId) { @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) { @@ -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 00-TRACE_ID-SPAN_ID-FLAGS. 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 00-TRACE_ID-SPAN_ID-FLAGS. field of the {@code + * version-format} value. * * @see traceparent header @@ -171,7 +190,27 @@ public Builder loadW3CTraceParentContext(String traceParent) { List 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 OpenTelemetry + * SpanContext. + */ + @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; } @@ -191,6 +230,7 @@ public Context build() { } this.traceId = builder.traceId; this.spanId = builder.spanId; + this.traceSampled = builder.traceSampled; } public HttpRequest getHttpRequest() { @@ -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); @@ -216,6 +260,7 @@ public String toString() { .add("request", request) .add("traceId", traceId) .add("spanId", spanId) + .add("traceSampled", traceSampled) .toString(); } @@ -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. */ diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/ContextHandler.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/ContextHandler.java index 8af084f27..54b7b1854 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/ContextHandler.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/ContextHandler.java @@ -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 contextHolder = initContextHolder(); + private static final ThreadLocal currentPriority = + ThreadLocal.withInitial(() -> ContextPriority.NO_INPUT); /** * Initializes the context holder to {@link InheritableThreadLocal} if {@link LogManager} @@ -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(); + } } diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingHandler.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingHandler.java index 06108a303..d1e56762a 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingHandler.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingHandler.java @@ -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); } diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingImpl.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingImpl.java index d1e3b0ae9..20bf4b507 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingImpl.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingImpl.java @@ -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; @@ -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; @@ -822,7 +824,7 @@ public Iterable populateMetadata( customResource == null ? MonitoredResourceUtil.getResource(getOptions().getProjectId(), null) : customResource; - final Context context = new ContextHandler().getCurrentContext(); + final ArrayList populatedLogEntries = Lists.newArrayList(); // populate empty metadata fields of log entries before calling write API @@ -834,6 +836,15 @@ public Iterable 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()); } @@ -841,6 +852,7 @@ public Iterable populateMetadata( 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()); diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/TraceLoggingEnhancer.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/TraceLoggingEnhancer.java index 8b7b4aea7..834e3b735 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/TraceLoggingEnhancer.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/TraceLoggingEnhancer.java @@ -24,6 +24,8 @@ public TraceLoggingEnhancer() {} public TraceLoggingEnhancer(String prefix) {} private static final ThreadLocal traceId = new ThreadLocal<>(); + private static final ThreadLocal spanId = new ThreadLocal<>(); + private static final ThreadLocal traceSampled = new ThreadLocal(); /** * Set the Trace ID associated with any logging done by the current thread. @@ -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); + } } } diff --git a/google-cloud-logging/src/test/java/com/google/cloud/logging/AutoPopulateMetadataTests.java b/google-cloud-logging/src/test/java/com/google/cloud/logging/AutoPopulateMetadataTests.java index f415f8c4c..7972ca7bf 100644 --- a/google-cloud-logging/src/test/java/com/google/cloud/logging/AutoPopulateMetadataTests.java +++ b/google-cloud-logging/src/test/java/com/google/cloud/logging/AutoPopulateMetadataTests.java @@ -22,8 +22,7 @@ import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.newCapture; import static org.easymock.EasyMock.replay; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.*; import com.google.api.core.ApiFutures; import com.google.cloud.MonitoredResource; @@ -74,6 +73,7 @@ public class AutoPopulateMetadataTests { private static final String FORMATTED_TRACE_ID = String.format(LoggingImpl.RESOURCE_NAME_FORMAT, RESOURCE_PROJECT_ID, TRACE_ID); private static final String SPAN_ID = "1"; + private static final boolean TRACE_SAMPLED = true; private LoggingRpcFactory mockedRpcFactory; private LoggingRpc mockedRpc; @@ -111,15 +111,21 @@ public void teardown() { new ContextHandler().removeCurrentContext(); } - private void mockCurrentContext(HttpRequest request, String traceId, String spanId) { + private void mockCurrentContext( + HttpRequest request, String traceId, String spanId, boolean traceSampled) { Context mockedContext = - Context.newBuilder().setRequest(request).setTraceId(traceId).setSpanId(spanId).build(); + Context.newBuilder() + .setRequest(request) + .setTraceId(traceId) + .setSpanId(spanId) + .setTraceSampled(traceSampled) + .build(); new ContextHandler().setCurrentContext(mockedContext); } @Test public void testAutoPopulationEnabledInLoggingOptions() { - mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID); + mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID, TRACE_SAMPLED); logging.write(ImmutableList.of(SIMPLE_LOG_ENTRY)); @@ -127,6 +133,7 @@ public void testAutoPopulationEnabledInLoggingOptions() { assertEquals(HTTP_REQUEST, actual.getHttpRequest()); assertEquals(FORMATTED_TRACE_ID, actual.getTrace()); assertEquals(SPAN_ID, actual.getSpanId()); + assertEquals(TRACE_SAMPLED, actual.getTraceSampled()); assertEquals(RESOURCE, actual.getResource()); } @@ -136,7 +143,7 @@ public void testAutoPopulationEnabledInWriteOptionsAndDisabledInLoggingOptions() LoggingOptions options = logging.getOptions().toBuilder().setAutoPopulateMetadata(false).build(); logging = options.getService(); - mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID); + mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID, TRACE_SAMPLED); logging.write(ImmutableList.of(SIMPLE_LOG_ENTRY), WriteOption.autoPopulateMetadata(true)); @@ -144,12 +151,13 @@ public void testAutoPopulationEnabledInWriteOptionsAndDisabledInLoggingOptions() assertEquals(HTTP_REQUEST, actual.getHttpRequest()); assertEquals(FORMATTED_TRACE_ID, actual.getTrace()); assertEquals(SPAN_ID, actual.getSpanId()); + assertEquals(TRACE_SAMPLED, actual.getTraceSampled()); assertEquals(RESOURCE, actual.getResource()); } @Test public void testAutoPopulationDisabledInWriteOptions() { - mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID); + mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID, TRACE_SAMPLED); logging.write(ImmutableList.of(SIMPLE_LOG_ENTRY), WriteOption.autoPopulateMetadata(false)); @@ -157,6 +165,7 @@ public void testAutoPopulationDisabledInWriteOptions() { assertNull(actual.getHttpRequest()); assertNull(actual.getTrace()); assertNull(actual.getSpanId()); + assertFalse(actual.getTraceSampled()); assertNull(actual.getResource()); } @@ -174,7 +183,7 @@ public void testSourceLocationPopulation() { @Test public void testNotFormattedTraceId() { - mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID); + mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID, TRACE_SAMPLED); final MonitoredResource expectedResource = MonitoredResource.newBuilder("custom").build(); @@ -186,7 +195,7 @@ public void testNotFormattedTraceId() { @Test public void testMonitoredResourcePopulationInWriteOptions() { - mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID); + mockCurrentContext(HTTP_REQUEST, TRACE_ID, SPAN_ID, TRACE_SAMPLED); final MonitoredResource expectedResource = MonitoredResource.newBuilder("custom").build(); diff --git a/google-cloud-logging/src/test/java/com/google/cloud/logging/ContextHandlerTest.java b/google-cloud-logging/src/test/java/com/google/cloud/logging/ContextHandlerTest.java new file mode 100644 index 000000000..a47ef9a2b --- /dev/null +++ b/google-cloud-logging/src/test/java/com/google/cloud/logging/ContextHandlerTest.java @@ -0,0 +1,280 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.logging; + +import static org.junit.Assert.*; + +import com.google.cloud.logging.ContextHandler.ContextPriority; +import com.google.cloud.logging.HttpRequest.RequestMethod; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ContextHandlerTest { + private static final HttpRequest OLD_HTTP_REQUEST = + HttpRequest.newBuilder() + .setRequestMethod(RequestMethod.POST) + .setRequestUrl("https://old.com") + .setUserAgent("Test User Agent") + .build(); + private static final HttpRequest HTTP_REQUEST = + HttpRequest.newBuilder() + .setRequestMethod(RequestMethod.GET) + .setRequestUrl("https://example.com") + .setUserAgent("Test User Agent") + .build(); + private static final String OLD_TRACE_ID = "10100101010101010101010101010101"; + private static final String OLD_SPAN_ID = "0"; + private static final boolean OLD_TRACE_SAMPLED = false; + private static final String TRACE_ID = "01010101010101010101010101010101"; + private static final String SPAN_ID = "1"; + private static final boolean TRACE_SAMPLED = true; + + @After + public void teardown() { + new ContextHandler().removeCurrentContext(); + new ContextHandler().removeCurrentContextPriority(); + } + + @Test + public void testDefaultSetContext() { + Context newContext = + Context.newBuilder() + .setRequest(HTTP_REQUEST) + .setTraceId(TRACE_ID) + .setSpanId(SPAN_ID) + .setTraceSampled(TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(newContext); + Context currentContext = new ContextHandler().getCurrentContext(); + assertEquals(HTTP_REQUEST, currentContext.getHttpRequest()); + assertEquals(TRACE_ID, currentContext.getTraceId()); + assertEquals(SPAN_ID, currentContext.getSpanId()); + assertEquals(TRACE_SAMPLED, currentContext.getTraceSampled()); + assertEquals(ContextPriority.NO_INPUT, new ContextHandler().getCurrentContextPriority()); + } + + @Test + public void testSetContextWithPriorityFromNoInput() { + Context newContext = + Context.newBuilder() + .setRequest(HTTP_REQUEST) + .setTraceId(TRACE_ID) + .setSpanId(SPAN_ID) + .setTraceSampled(TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(newContext, ContextPriority.NO_INPUT); + Context currentContext = new ContextHandler().getCurrentContext(); + assertEquals(ContextPriority.NO_INPUT, new ContextHandler().getCurrentContextPriority()); + assertEquals(HTTP_REQUEST, currentContext.getHttpRequest()); + assertEquals(TRACE_ID, currentContext.getTraceId()); + assertEquals(SPAN_ID, currentContext.getSpanId()); + assertEquals(TRACE_SAMPLED, currentContext.getTraceSampled()); + } + + @Test + public void testSetContextWithPriorityFromW3CHeader() { + Context newContext = + Context.newBuilder() + .setRequest(HTTP_REQUEST) + .setTraceId(TRACE_ID) + .setSpanId(SPAN_ID) + .setTraceSampled(TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(newContext, ContextPriority.W3C_HEADER); + Context currentContext = new ContextHandler().getCurrentContext(); + assertEquals(ContextPriority.W3C_HEADER, new ContextHandler().getCurrentContextPriority()); + assertEquals(HTTP_REQUEST, currentContext.getHttpRequest()); + assertEquals(TRACE_ID, currentContext.getTraceId()); + assertEquals(SPAN_ID, currentContext.getSpanId()); + assertEquals(TRACE_SAMPLED, currentContext.getTraceSampled()); + } + + @Test + public void testSetContextFromXCloudHeader() { + Context newContext = + Context.newBuilder() + .setRequest(HTTP_REQUEST) + .setTraceId(TRACE_ID) + .setSpanId(SPAN_ID) + .setTraceSampled(TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(newContext, ContextPriority.XCLOUD_HEADER); + Context currentContext = new ContextHandler().getCurrentContext(); + assertEquals(ContextPriority.XCLOUD_HEADER, new ContextHandler().getCurrentContextPriority()); + assertEquals(HTTP_REQUEST, currentContext.getHttpRequest()); + assertEquals(TRACE_ID, currentContext.getTraceId()); + assertEquals(SPAN_ID, currentContext.getSpanId()); + assertEquals(TRACE_SAMPLED, currentContext.getTraceSampled()); + } + + @Test + public void testSetContextFromOpenTelemetry() { + Context newContext = + Context.newBuilder() + .setRequest(HTTP_REQUEST) + .setTraceId(TRACE_ID) + .setSpanId(SPAN_ID) + .setTraceSampled(TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(newContext, ContextPriority.OTEL_EXTRACTED); + Context currentContext = new ContextHandler().getCurrentContext(); + assertEquals(ContextPriority.OTEL_EXTRACTED, new ContextHandler().getCurrentContextPriority()); + assertEquals(HTTP_REQUEST, currentContext.getHttpRequest()); + assertEquals(TRACE_ID, currentContext.getTraceId()); + assertEquals(SPAN_ID, currentContext.getSpanId()); + assertEquals(TRACE_SAMPLED, currentContext.getTraceSampled()); + } + + @Test + public void testOverrideW3CContextFromOpenTelemetry() { + Context oldContext = + Context.newBuilder() + .setRequest(OLD_HTTP_REQUEST) + .setTraceId(OLD_TRACE_ID) + .setSpanId(OLD_SPAN_ID) + .setTraceSampled(OLD_TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(oldContext, ContextPriority.W3C_HEADER); + Context newContext = + Context.newBuilder() + .setRequest(HTTP_REQUEST) + .setTraceId(TRACE_ID) + .setSpanId(SPAN_ID) + .setTraceSampled(TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(newContext, ContextPriority.OTEL_EXTRACTED); + // Expects context being overridden when context was set with higher priority. + assertEquals(ContextPriority.OTEL_EXTRACTED, new ContextHandler().getCurrentContextPriority()); + Context currentContext = new ContextHandler().getCurrentContext(); + assertEquals(HTTP_REQUEST, currentContext.getHttpRequest()); + assertEquals(TRACE_ID, currentContext.getTraceId()); + assertEquals(SPAN_ID, currentContext.getSpanId()); + assertEquals(TRACE_SAMPLED, currentContext.getTraceSampled()); + } + + @Test + public void testOverrideXCTCContextFromOpenTelemetry() { + Context oldContext = + Context.newBuilder() + .setRequest(OLD_HTTP_REQUEST) + .setTraceId(OLD_TRACE_ID) + .setSpanId(OLD_SPAN_ID) + .setTraceSampled(OLD_TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(oldContext, ContextPriority.XCLOUD_HEADER); + + Context newContext = + Context.newBuilder() + .setRequest(HTTP_REQUEST) + .setTraceId(TRACE_ID) + .setSpanId(SPAN_ID) + .setTraceSampled(TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(newContext, ContextPriority.OTEL_EXTRACTED); + // Expects context being overridden when context was set with higher priority. + assertEquals(ContextPriority.OTEL_EXTRACTED, new ContextHandler().getCurrentContextPriority()); + Context currentContext = new ContextHandler().getCurrentContext(); + assertEquals(HTTP_REQUEST, currentContext.getHttpRequest()); + assertEquals(TRACE_ID, currentContext.getTraceId()); + assertEquals(SPAN_ID, currentContext.getSpanId()); + assertEquals(TRACE_SAMPLED, currentContext.getTraceSampled()); + } + + @Test + public void testOverrideOtelContextFromDefaultSetContext() { + Context oldContext = + Context.newBuilder() + .setRequest(OLD_HTTP_REQUEST) + .setTraceId(OLD_TRACE_ID) + .setSpanId(OLD_SPAN_ID) + .setTraceSampled(OLD_TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(oldContext, ContextPriority.OTEL_EXTRACTED); + Context newContext = + Context.newBuilder() + .setRequest(HTTP_REQUEST) + .setTraceId(TRACE_ID) + .setSpanId(SPAN_ID) + .setTraceSampled(TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(newContext); + // Expects open telemetry context not being overridden when context was set with lower priority. + assertEquals(ContextPriority.OTEL_EXTRACTED, new ContextHandler().getCurrentContextPriority()); + Context currentContext = new ContextHandler().getCurrentContext(); + assertEquals(OLD_HTTP_REQUEST, currentContext.getHttpRequest()); + assertEquals(OLD_TRACE_ID, currentContext.getTraceId()); + assertEquals(OLD_SPAN_ID, currentContext.getSpanId()); + assertEquals(OLD_TRACE_SAMPLED, currentContext.getTraceSampled()); + } + + @Test + public void testOverrideOtelContextFromW3C() { + Context oldContext = + Context.newBuilder() + .setRequest(OLD_HTTP_REQUEST) + .setTraceId(OLD_TRACE_ID) + .setSpanId(OLD_SPAN_ID) + .setTraceSampled(OLD_TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(oldContext, ContextPriority.OTEL_EXTRACTED); + Context newContext = + Context.newBuilder() + .setRequest(HTTP_REQUEST) + .setTraceId(TRACE_ID) + .setSpanId(SPAN_ID) + .setTraceSampled(TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(newContext, ContextPriority.W3C_HEADER); + // Expects open telemetry context not being overridden when context was set with lower priority. + assertEquals(ContextPriority.OTEL_EXTRACTED, new ContextHandler().getCurrentContextPriority()); + Context currentContext = new ContextHandler().getCurrentContext(); + assertEquals(OLD_HTTP_REQUEST, currentContext.getHttpRequest()); + assertEquals(OLD_TRACE_ID, currentContext.getTraceId()); + assertEquals(OLD_SPAN_ID, currentContext.getSpanId()); + assertEquals(OLD_TRACE_SAMPLED, currentContext.getTraceSampled()); + } + + @Test + public void testOverrideOtelContextFromXCTC() { + Context oldContext = + Context.newBuilder() + .setRequest(OLD_HTTP_REQUEST) + .setTraceId(OLD_TRACE_ID) + .setSpanId(OLD_SPAN_ID) + .setTraceSampled(OLD_TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(oldContext, ContextPriority.OTEL_EXTRACTED); + Context newContext = + Context.newBuilder() + .setRequest(HTTP_REQUEST) + .setTraceId(TRACE_ID) + .setSpanId(SPAN_ID) + .setTraceSampled(TRACE_SAMPLED) + .build(); + new ContextHandler().setCurrentContext(newContext, ContextPriority.XCLOUD_HEADER); + // Expects open telemetry context not being overridden when context was set with lower priority. + assertEquals(ContextPriority.OTEL_EXTRACTED, new ContextHandler().getCurrentContextPriority()); + Context currentContext = new ContextHandler().getCurrentContext(); + assertEquals(OLD_HTTP_REQUEST, currentContext.getHttpRequest()); + assertEquals(OLD_TRACE_ID, currentContext.getTraceId()); + assertEquals(OLD_SPAN_ID, currentContext.getSpanId()); + assertEquals(OLD_TRACE_SAMPLED, currentContext.getTraceSampled()); + } +} diff --git a/google-cloud-logging/src/test/java/com/google/cloud/logging/ContextTest.java b/google-cloud-logging/src/test/java/com/google/cloud/logging/ContextTest.java index 512c99aa8..7ef8f90de 100644 --- a/google-cloud-logging/src/test/java/com/google/cloud/logging/ContextTest.java +++ b/google-cloud-logging/src/test/java/com/google/cloud/logging/ContextTest.java @@ -21,6 +21,14 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; +import io.opentelemetry.api.trace.*; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -37,6 +45,7 @@ public class ContextTest { // DO NOT use dash in trace and span id because W3C traceparent format uses dash as a delimieter private static final String TEST_TRACE_ID = "test_trace_id"; private static final String TEST_SPAN_ID = "test_span_id"; + private static final boolean TEST_TRACE_SAMPLED = true; private static final HttpRequest REQUEST = HttpRequest.newBuilder() @@ -68,6 +77,7 @@ public class ContextTest { .setRequest(PARTIAL_REQUEST) .setTraceId(TEST_TRACE_ID) .setSpanId(TEST_SPAN_ID) + .setTraceSampled(TEST_TRACE_SAMPLED) .build(); @Test @@ -87,6 +97,7 @@ public void testCompareContexts() { .setServerIp(SERVER_IP) .setTraceId(TEST_TRACE_ID) .setSpanId(TEST_SPAN_ID) + .setTraceSampled(TEST_TRACE_SAMPLED) .build(); assertNotEquals(TEST_CONTEXT, context1); @@ -103,9 +114,11 @@ public void testContextBuilder() { assertEquals(PARTIAL_REQUEST, TEST_CONTEXT.getHttpRequest()); assertEquals(TEST_TRACE_ID, TEST_CONTEXT.getTraceId()); assertEquals(TEST_SPAN_ID, TEST_CONTEXT.getSpanId()); + assertEquals(TEST_TRACE_SAMPLED, TEST_CONTEXT.getTraceSampled()); assertNull(emptyContext.getHttpRequest()); assertNull(emptyContext.getTraceId()); assertNull(emptyContext.getSpanId()); + assertFalse(emptyContext.getTraceSampled()); assertEquals(TEST_CONTEXT, anotherContext); } @@ -114,40 +127,78 @@ public void testParsingCloudTraceContext() { final String X_CLOUD_TRACE_NO_TRACE = "/SPAN_ID;o=TRACE_TRUE"; final String X_CLOUD_TRACE_ONLY = TEST_TRACE_ID; final String X_CLOUD_TRACE_WITH_SPAN = TEST_TRACE_ID + "/" + TEST_SPAN_ID; - final String X_CLOUD_TRACE_FULL = TEST_TRACE_ID + "/" + TEST_SPAN_ID + ";o=TRACE_TRUE"; + final String X_CLOUD_TRACE_FULL = TEST_TRACE_ID + "/" + TEST_SPAN_ID + ";o=1"; Context.Builder builder = Context.newBuilder(); builder.loadCloudTraceContext(null); - assertTraceAndSpan(builder.build(), null, null); + assertTraceSpanAndSampled(builder.build(), null, null, false); builder.loadCloudTraceContext(""); - assertTraceAndSpan(builder.build(), null, null); + assertTraceSpanAndSampled(builder.build(), null, null, false); builder.loadCloudTraceContext(X_CLOUD_TRACE_NO_TRACE); - assertTraceAndSpan(builder.build(), null, null); + assertTraceSpanAndSampled(builder.build(), null, null, false); builder.loadCloudTraceContext(X_CLOUD_TRACE_ONLY); - assertTraceAndSpan(builder.build(), TEST_TRACE_ID, null); + assertTraceSpanAndSampled(builder.build(), TEST_TRACE_ID, null, false); builder.loadCloudTraceContext(X_CLOUD_TRACE_WITH_SPAN); - assertTraceAndSpan(builder.build(), TEST_TRACE_ID, TEST_SPAN_ID); + assertTraceSpanAndSampled(builder.build(), TEST_TRACE_ID, TEST_SPAN_ID, false); builder.loadCloudTraceContext(X_CLOUD_TRACE_FULL); - assertTraceAndSpan(builder.build(), TEST_TRACE_ID, TEST_SPAN_ID); + assertTraceSpanAndSampled(builder.build(), TEST_TRACE_ID, TEST_SPAN_ID, TEST_TRACE_SAMPLED); } @Test public void testParsingW3CTraceParent() { final String W3C_TEST_TRACE_ID = "12345678901234567890123456789012"; final String W3C_TEST_SPAN_ID = "1234567890123456"; - final String W3C_TRACE_CONTEXT = "00-" + W3C_TEST_TRACE_ID + "-" + W3C_TEST_SPAN_ID + "-00"; + final String W3C_TEST_TRACE_SAMPLED = "0f"; + final String W3C_TRACE_CONTEXT = + "00-" + W3C_TEST_TRACE_ID + "-" + W3C_TEST_SPAN_ID + "-" + W3C_TEST_TRACE_SAMPLED; Context.Builder builder = Context.newBuilder(); builder.loadW3CTraceParentContext(null); - assertTraceAndSpan(builder.build(), null, null); + assertTraceSpanAndSampled(builder.build(), null, null, false); builder.loadW3CTraceParentContext(W3C_TRACE_CONTEXT); - assertTraceAndSpan(builder.build(), W3C_TEST_TRACE_ID, W3C_TEST_SPAN_ID); + assertTraceSpanAndSampled(builder.build(), W3C_TEST_TRACE_ID, W3C_TEST_SPAN_ID, true); } - private void assertTraceAndSpan(Context context, String expectedTraceId, String expectedSpanId) { + @Test + public void testParsingOpenTelemetryContext() { + InMemorySpanExporter testExporter = InMemorySpanExporter.create(); + SpanProcessor inMemorySpanProcessor = SimpleSpanProcessor.create(testExporter); + OpenTelemetrySdk openTelemetrySdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder().addSpanProcessor(inMemorySpanProcessor).build()) + .buildAndRegisterGlobal(); + + Tracer tracer = openTelemetrySdk.getTracer("ContextTest"); + Span otelSpan = tracer.spanBuilder("Example Span Attributes").startSpan(); + SpanContext currentOtelContext; + Context.Builder builder = Context.newBuilder(); + try (Scope scope = otelSpan.makeCurrent()) { + otelSpan.setAttribute("Attribute 1", "first attribute value"); + currentOtelContext = otelSpan.getSpanContext(); + builder.loadOpenTelemetryContext(); + assertTraceSpanAndSampled( + builder.build(), + currentOtelContext.getTraceId(), + currentOtelContext.getSpanId(), + currentOtelContext.isSampled()); + } catch (Throwable t) { + otelSpan.recordException(t); + throw t; + } finally { + otelSpan.end(); + } + } + + private void assertTraceSpanAndSampled( + Context context, + String expectedTraceId, + String expectedSpanId, + boolean expectedTraceSampled) { assertEquals(expectedTraceId, context.getTraceId()); assertEquals(expectedSpanId, context.getSpanId()); + assertEquals(expectedTraceSampled, context.getTraceSampled()); } } diff --git a/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingHandlerTest.java b/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingHandlerTest.java index c6267e22e..abd88053e 100644 --- a/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingHandlerTest.java +++ b/google-cloud-logging/src/test/java/com/google/cloud/logging/LoggingHandlerTest.java @@ -165,6 +165,8 @@ public class LoggingHandlerTest { .addLabel("levelName", "FINEST") .addLabel("levelValue", String.valueOf(Level.FINEST.intValue())) .setTrace("projects/projectId/traces/traceId") + .setSpanId("test_span_id") + .setTraceSampled(true) .setTimestamp(123456789L) .build(); private static final LogEntry DIAGNOSTIC_ENTRY = @@ -454,6 +456,8 @@ public void testTraceEnhancedLogEntry() { replay(options, logging); LoggingEnhancer enhancer = new TraceLoggingEnhancer(); TraceLoggingEnhancer.setCurrentTraceId("projects/projectId/traces/traceId"); + TraceLoggingEnhancer.setCurrentSpanId("test_span_id"); + TraceLoggingEnhancer.setCurrentTraceSampled(true); Handler handler = new LoggingHandler(LOG_NAME, options, DEFAULT_RESOURCE, ImmutableList.of(enhancer)); handler.setLevel(Level.ALL); diff --git a/google-cloud-logging/src/test/java/com/google/cloud/logging/it/ITTracingLogsTest.java b/google-cloud-logging/src/test/java/com/google/cloud/logging/it/ITTracingLogsTest.java new file mode 100644 index 000000000..cdd52af39 --- /dev/null +++ b/google-cloud-logging/src/test/java/com/google/cloud/logging/it/ITTracingLogsTest.java @@ -0,0 +1,244 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.logging.it; + +import static com.google.cloud.logging.testing.RemoteLoggingHelper.formatForTest; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.*; + +import com.google.cloud.MonitoredResource; +import com.google.cloud.logging.*; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.logging.v2.LogName; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.Iterator; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Test; + +public class ITTracingLogsTest extends BaseSystemTest { + + private static final String LOG_ID = formatForTest("test-write-log-entries-log"); + private static final Payload.StringPayload STRING_PAYLOAD = + Payload.StringPayload.of("stringPayload"); + private static final Payload.JsonPayload OTEL_PAYLOAD = + Payload.JsonPayload.of(ImmutableMap.of("jsonKey", "jsonValue")); + + private static final MonitoredResource GLOBAL_RESOURCE = + MonitoredResource.newBuilder("global").build(); + private static final MonitoredResource[] MONITORED_RESOURCES_IN_TEST = + new MonitoredResource[] {GLOBAL_RESOURCE}; + + private static final ContextHandler contextHandler = new ContextHandler(); + + private static final String W3C_TEST_TRACE_ID = "12345678901234567890123456789012"; + private static final String W3C_TEST_SPAN_ID = "1234567890123456"; + private static final String W3C_TEST_TRACE_SAMPLED = "0f"; + private static final String W3C_TRACE_CONTEXT = + "00-" + W3C_TEST_TRACE_ID + "-" + W3C_TEST_SPAN_ID + "-" + W3C_TEST_TRACE_SAMPLED; + + private static final String XCTC_TEST_TRACE_ID = "98765432101234569876543210123456"; + private static final String XCTC_TEST_SPAN_ID = "9876543210123456"; + private static final String X_CLOUD_TRACE_CONTEXT = + XCTC_TEST_TRACE_ID + "/" + XCTC_TEST_SPAN_ID + ";o=1"; + + private static String otelTraceId; + private static String otelSpanId; + private static boolean isSampled; + private static Tracer tracer; + private static LogEntry w3cEntry; + private static LogEntry xctcEntry; + private static LogEntry otelEntry; + private static LogName logName; + + @BeforeClass + public static void prepareLogs() throws InterruptedException { + LoggingOptions loggingOptions = logging.getOptions(); + logName = LogName.ofProjectLogName(loggingOptions.getProjectId(), LOG_ID); + logging.setWriteSynchronicity(Synchronicity.SYNC); + w3cEntry = + LogEntry.newBuilder(STRING_PAYLOAD) + .setLogName(LOG_ID) + .addLabel("tracing_source", "w3c") + .setHttpRequest(HttpRequest.newBuilder().setStatus(500).build()) + .setResource(GLOBAL_RESOURCE) + .build(); + xctcEntry = + LogEntry.newBuilder(STRING_PAYLOAD) + .setLogName(LOG_ID) + .addLabel("tracing_source", "xctc") + .setHttpRequest(HttpRequest.newBuilder().setRequestUrl("www.google.com").build()) + .setResource(GLOBAL_RESOURCE) + .build(); + otelEntry = + LogEntry.newBuilder(OTEL_PAYLOAD) + .addLabel("tracing_source", "otel") + .setLogName(LOG_ID) + .setResource(GLOBAL_RESOURCE) + .build(); + + // Initializes open telemetry SDK + InMemorySpanExporter testExporter = InMemorySpanExporter.create(); + SpanProcessor inMemorySpanProcessor = SimpleSpanProcessor.create(testExporter); + OpenTelemetrySdk openTelemetrySdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder().addSpanProcessor(inMemorySpanProcessor).build()) + .build(); + tracer = openTelemetrySdk.getTracer("ContextTest"); + } + + @After + public void cleanUpLogs() throws InterruptedException { + assertTrue(cleanupLog(LOG_ID)); + } + + @Test(timeout = 600_000) + public void testDetectW3CTraceId() throws InterruptedException { + // Loads w3c tracing context and writes a log entry + Context.Builder builder = Context.newBuilder(); + builder.loadW3CTraceParentContext(W3C_TRACE_CONTEXT); + contextHandler.setCurrentContext(builder.build()); + logging.write(ImmutableList.of(w3cEntry)); + logging.flush(); + + // Find the log name and wait until we have at least 1 entry + Iterator iterator = waitForLogs(logName, MONITORED_RESOURCES_IN_TEST, 1); + assertThat(iterator.hasNext()).isTrue(); + + LogEntry entry = iterator.next(); + assertEquals(LOG_ID, entry.getLogName()); + assertEquals(ImmutableMap.of("tracing_source", "w3c"), entry.getLabels()); + assertEquals(HttpRequest.newBuilder().setStatus(500).build(), entry.getHttpRequest()); + assertEquals(W3C_TEST_TRACE_ID, entry.getTrace()); + assertEquals(W3C_TEST_SPAN_ID, entry.getSpanId()); + assertEquals(true, entry.getTraceSampled()); + } + + @Test(timeout = 600_000) + public void testDetectXCTCTraceId() throws InterruptedException { + // Loads cloud trace context and writes a log entry + Context.Builder builder = Context.newBuilder(); + builder.loadCloudTraceContext(X_CLOUD_TRACE_CONTEXT); + contextHandler.setCurrentContext(builder.build()); + logging.write(ImmutableList.of(xctcEntry)); + logging.flush(); + + // Find the log name and wait until we have at least 1 entry + Iterator iterator = waitForLogs(logName, MONITORED_RESOURCES_IN_TEST, 1); + assertThat(iterator.hasNext()).isTrue(); + + LogEntry entry = iterator.next(); + assertEquals(LOG_ID, entry.getLogName()); + assertEquals(ImmutableMap.of("tracing_source", "xctc"), entry.getLabels()); + assertEquals( + HttpRequest.newBuilder().setRequestUrl("www.google.com").build(), entry.getHttpRequest()); + assertEquals(XCTC_TEST_TRACE_ID, entry.getTrace()); + assertEquals(XCTC_TEST_SPAN_ID, entry.getSpanId()); + assertEquals(true, entry.getTraceSampled()); + } + + @Test(timeout = 600_000) + public void testDetectOtelTraceId() throws InterruptedException { + // Writes a log entry in open telemetry context + writeLogEntryWithOtelContext(otelEntry); + + // Find the log name and wait until we have at least 1 entry + Iterator iterator = waitForLogs(logName, MONITORED_RESOURCES_IN_TEST, 1); + assertThat(iterator.hasNext()).isTrue(); + + LogEntry entry = iterator.next(); + assertEquals(LOG_ID, entry.getLogName()); + assertEquals(OTEL_PAYLOAD, entry.getPayload()); + assertEquals(ImmutableMap.of("tracing_source", "otel"), entry.getLabels()); + assertNull(entry.getHttpRequest()); + assertEquals(otelTraceId, entry.getTrace()); + assertEquals(otelSpanId, entry.getSpanId()); + assertEquals(isSampled, entry.getTraceSampled()); + } + + @Test(timeout = 600_000) + public void testW3CTraceIdWithOtelContext() throws InterruptedException { + // Writes a log entry with W3C context and Open Telemetry context + Context.Builder builder = Context.newBuilder(); + builder.loadW3CTraceParentContext(W3C_TRACE_CONTEXT); + contextHandler.setCurrentContext(builder.build()); + writeLogEntryWithOtelContext(w3cEntry); + + // Find the log name and wait until we have at least 1 entry + Iterator iterator = waitForLogs(logName, MONITORED_RESOURCES_IN_TEST, 1); + assertThat(iterator.hasNext()).isTrue(); + + LogEntry entry = iterator.next(); + assertEquals(LOG_ID, entry.getLogName()); + assertEquals(HttpRequest.newBuilder().setStatus(500).build(), entry.getHttpRequest()); + // Expect to get trace Id, span Id and isSampled flag from Open Telemetry context when it + // exists. + assertEquals(otelTraceId, entry.getTrace()); + assertEquals(otelSpanId, entry.getSpanId()); + assertEquals(isSampled, entry.getTraceSampled()); + } + + @Test(timeout = 600_000) + public void testXCTCTraceIdWithOtelContext() throws InterruptedException { + // Writes a log entry with cloud trace context and Open Telemetry context + Context.Builder builder = Context.newBuilder(); + builder.loadCloudTraceContext(X_CLOUD_TRACE_CONTEXT); + contextHandler.setCurrentContext(builder.build()); + writeLogEntryWithOtelContext(xctcEntry); + + // Find the log name and wait until we have at least 1 entry + Iterator iterator = waitForLogs(logName, MONITORED_RESOURCES_IN_TEST, 1); + assertThat(iterator.hasNext()).isTrue(); + + LogEntry entry = iterator.next(); + assertEquals(LOG_ID, entry.getLogName()); + // Expect to get trace Id, span Id and isSampled flag from Open telemetry context when it + // exists. + assertEquals(otelTraceId, entry.getTrace()); + assertEquals(otelSpanId, entry.getSpanId()); + assertEquals(isSampled, entry.getTraceSampled()); + } + + // Writes a log entry with otel context + private static void writeLogEntryWithOtelContext(LogEntry entry) throws InterruptedException { + Span otelSpan = tracer.spanBuilder("Example Span").startSpan(); + SpanContext currentOtelContext; + try (Scope scope = otelSpan.makeCurrent()) { + currentOtelContext = otelSpan.getSpanContext(); + otelTraceId = currentOtelContext.getTraceId(); + otelSpanId = currentOtelContext.getSpanId(); + isSampled = currentOtelContext.isSampled(); + logging.write(ImmutableList.of(entry)); + } catch (Throwable t) { + otelSpan.recordException(t); + throw t; + } finally { + otelSpan.end(); + } + logging.flush(); + } +} diff --git a/pom.xml b/pom.xml index 482cbdab3..eba80135a 100644 --- a/pom.xml +++ b/pom.xml @@ -56,8 +56,27 @@ google-cloud-logging-parent + + + io.opentelemetry + opentelemetry-bom + 1.38.0 + pom + import + + + + + + + + + + + + com.google.api.grpc proto-google-cloud-logging-v2 @@ -117,6 +136,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + io.opentelemetry + opentelemetry-semconv + 1.1.0-alpha + test + + + + + + + + + + + + + + + com.google.cloud.opentelemetry + exporter-trace + 0.15.0 + test + +