diff --git a/gcp/logback-logstash-encoder-gcp/src/main/java/no/entur/logging/cloud/gcp/logback/logstash/StackdriverOpenTelemetryTraceMdcJsonProvider.java b/gcp/logback-logstash-encoder-gcp/src/main/java/no/entur/logging/cloud/gcp/logback/logstash/StackdriverOpenTelemetryTraceMdcJsonProvider.java
index 101fdf81..bf73e49a 100644
--- a/gcp/logback-logstash-encoder-gcp/src/main/java/no/entur/logging/cloud/gcp/logback/logstash/StackdriverOpenTelemetryTraceMdcJsonProvider.java
+++ b/gcp/logback-logstash-encoder-gcp/src/main/java/no/entur/logging/cloud/gcp/logback/logstash/StackdriverOpenTelemetryTraceMdcJsonProvider.java
@@ -8,27 +8,53 @@
import java.util.Map;
/**
+ * An MDC provider that maps OpenTelemetry trace fields to the special JSON fields
+ * recognized by Google Cloud Logging.
*
- * A simple MDC provider. Renames MDC field name traceId to trace.
+ *
When the Google Cloud Logging agent ingests structured JSON written to stdout, it promotes
+ * recognized JSON fields into the corresponding {@code LogEntry} fields. In particular:
+ *
+ * - {@code logging.googleapis.com/trace} becomes {@code LogEntry.trace}
+ * - {@code logging.googleapis.com/spanId} becomes {@code LogEntry.spanId}
+ *
+ * Unrecognized fields remain in {@code LogEntry.jsonPayload}.
*
+ * @see
+ * Special fields in structured payloads
+ *
*/
-
public class StackdriverOpenTelemetryTraceMdcJsonProvider extends AbstractJsonProvider {
+ static final String OPENTELEMETRY_TRACE_ID_KEY = "traceId";
+ static final String OPENTELEMETRY_SPAN_ID_KEY = "spanId";
+ static final String GCP_TRACE_KEY = "logging.googleapis.com/trace";
+ static final String GCP_SPAN_ID_KEY = "logging.googleapis.com/spanId";
+
@Override
public void writeTo(JsonGenerator generator, ILoggingEvent event) {
Map mdcProperties = event.getMDCPropertyMap();
if (mdcProperties != null && !mdcProperties.isEmpty()) {
- String traceId = mdcProperties.get("traceId");
- if(traceId != null) {
- generator.writeStringProperty("trace", traceId);
- }
+ String traceId = mdcProperties.get(OPENTELEMETRY_TRACE_ID_KEY);
+ String spanId = mdcProperties.get(OPENTELEMETRY_SPAN_ID_KEY);
+
for (Map.Entry entry : mdcProperties.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if(key == null || value == null) {
continue;
}
+
+ if ((GCP_TRACE_KEY.equals(key) && traceId != null)
+ || (GCP_SPAN_ID_KEY.equals(key) && spanId != null)) {
+ continue;
+ }
+
+ if (OPENTELEMETRY_TRACE_ID_KEY.equals(key)) {
+ key = GCP_TRACE_KEY;
+ } else if (OPENTELEMETRY_SPAN_ID_KEY.equals(key)) {
+ key = GCP_SPAN_ID_KEY;
+ }
+
generator.writeStringProperty(key, value);
}
}
diff --git a/gcp/logback-logstash-encoder-gcp/src/test/java/no/entur/logging/cloud/gcp/logback/logstash/StackdriverOpenTelemetryTraceMdcJsonProviderTest.java b/gcp/logback-logstash-encoder-gcp/src/test/java/no/entur/logging/cloud/gcp/logback/logstash/StackdriverOpenTelemetryTraceMdcJsonProviderTest.java
new file mode 100644
index 00000000..1ccaf2ff
--- /dev/null
+++ b/gcp/logback-logstash-encoder-gcp/src/test/java/no/entur/logging/cloud/gcp/logback/logstash/StackdriverOpenTelemetryTraceMdcJsonProviderTest.java
@@ -0,0 +1,84 @@
+package no.entur.logging.cloud.gcp.logback.logstash;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.core.json.JsonFactory;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.json.JsonMapper;
+
+import java.io.StringWriter;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class StackdriverOpenTelemetryTraceMdcJsonProviderTest {
+
+ private static final JsonMapper MAPPER = JsonMapper.builder().build();
+
+ @Test
+ void writeTo_openTelemetryTraceFields_mappedToGcpSpecialFields() throws Exception {
+ Map mdc = new LinkedHashMap<>();
+ mdc.put("traceId", "06796866738c859f2f19b7cfb3214824");
+ mdc.put("spanId", "000000000000004a");
+ mdc.put("correlationId", "abc123");
+
+ JsonNode root = write(mdc);
+
+ assertThat(root.get("logging.googleapis.com/trace").asText())
+ .isEqualTo("06796866738c859f2f19b7cfb3214824");
+ assertThat(root.get("logging.googleapis.com/spanId").asText())
+ .isEqualTo("000000000000004a");
+ assertThat(root.get("correlationId").asText()).isEqualTo("abc123");
+ assertThat(root.has("trace")).isFalse();
+ assertThat(root.has("traceId")).isFalse();
+ assertThat(root.has("spanId")).isFalse();
+ }
+
+ @Test
+ void writeTo_existingGcpTraceFields_openTelemetryValuesTakePrecedenceWithoutDuplicates() throws Exception {
+ Map mdc = new LinkedHashMap<>();
+ mdc.put("logging.googleapis.com/trace", "legacy-trace");
+ mdc.put("logging.googleapis.com/spanId", "legacy-span");
+ mdc.put("traceId", "06796866738c859f2f19b7cfb3214824");
+ mdc.put("spanId", "000000000000004a");
+
+ JsonNode root = write(mdc);
+
+ assertThat(root.get("logging.googleapis.com/trace").asText())
+ .isEqualTo("06796866738c859f2f19b7cfb3214824");
+ assertThat(root.get("logging.googleapis.com/spanId").asText())
+ .isEqualTo("000000000000004a");
+ assertThat(root.size()).isEqualTo(2);
+ }
+
+ @Test
+ void writeTo_existingGcpTraceFields_preservedWithoutOpenTelemetryValues() throws Exception {
+ Map mdc = new LinkedHashMap<>();
+ mdc.put("logging.googleapis.com/trace", "existing-trace");
+ mdc.put("logging.googleapis.com/spanId", "existing-span");
+
+ JsonNode root = write(mdc);
+
+ assertThat(root.get("logging.googleapis.com/trace").asText()).isEqualTo("existing-trace");
+ assertThat(root.get("logging.googleapis.com/spanId").asText()).isEqualTo("existing-span");
+ }
+
+ private static JsonNode write(Map mdcMap) throws Exception {
+ StackdriverOpenTelemetryTraceMdcJsonProvider provider =
+ new StackdriverOpenTelemetryTraceMdcJsonProvider();
+ ILoggingEvent event = Mockito.mock(ILoggingEvent.class);
+ Mockito.when(event.getMDCPropertyMap()).thenReturn(mdcMap);
+
+ StringWriter stringWriter = new StringWriter();
+ JsonFactory factory = new JsonFactory();
+ try (JsonGenerator generator = factory.createGenerator(stringWriter)) {
+ generator.writeStartObject();
+ provider.writeTo(generator, event);
+ generator.writeEndObject();
+ }
+ return MAPPER.readTree(stringWriter.toString());
+ }
+}