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: + *

+ * 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()); + } +}