Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>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:
* <ul>
* <li>{@code logging.googleapis.com/trace} becomes {@code LogEntry.trace}</li>
* <li>{@code logging.googleapis.com/spanId} becomes {@code LogEntry.spanId}</li>
* </ul>
* Unrecognized fields remain in {@code LogEntry.jsonPayload}.
*
* @see <a href="https://docs.cloud.google.com/logging/docs/agent/logging/configuration#special-fields">
* Special fields in structured payloads
* </a>
*/

public class StackdriverOpenTelemetryTraceMdcJsonProvider extends AbstractJsonProvider<ILoggingEvent> {

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<String, String> 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<String, String> 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);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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());
}
}