Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
22db5ec
fix(gcp): map OpenTelemetry trace fields
viliket Aug 7, 2026
33cb79f
Opentelemetry support
skjolber Aug 10, 2026
9faea6c
Clone examples with otel
skjolber Aug 11, 2026
dd36a53
Add agent otel detector
skjolber Aug 12, 2026
0b3f1fd
Autodetect otel agent, if not fall back to micrometer
skjolber Aug 12, 2026
6ab32d3
Delete / adjust tests, attempt to add sampled flag
skjolber Aug 12, 2026
80096d9
Adjust provider, add example, add some assertions
skjolber Aug 12, 2026
e8725e1
Adjust readme
skjolber Aug 12, 2026
2183d09
More examples
skjolber Aug 13, 2026
65e75b5
Merge branch 'main' of github.com:entur/cloud-logging into opentelemetry
skjolber Aug 13, 2026
24480f0
More examples, azure improvements, better spring otel detection, ++
skjolber Aug 14, 2026
7ca8d3a
Mark as deprecated
skjolber Aug 14, 2026
8b24027
Merge branch 'main' into opentelemetry
skjolber Aug 14, 2026
25474bb
Fix tests
skjolber Aug 14, 2026
149ff00
Merge branch 'opentelemetry' of github.com:entur/cloud-logging into o…
skjolber Aug 14, 2026
e56a2d2
Potential fix for pull request finding
skjolber Aug 14, 2026
efee91c
Potential fix for pull request finding
skjolber Aug 14, 2026
c79fbdf
Remove unfinished micrometer sampled implementation
skjolber Aug 14, 2026
885c150
Merge branch 'opentelemetry' of github.com:entur/cloud-logging into o…
skjolber Aug 14, 2026
9a06b17
Fix trace_sampled MDC flag for micrometer+OTel starter (PR 451) (#452)
Copilot Aug 15, 2026
dbfb69b
feat(gcp): move TraceSampledMdcHandler into spring-boot-autoconfigure…
Copilot Aug 15, 2026
351c6bd
feat(examples): add downstream self-call + trace ID propagation tests…
Copilot Aug 15, 2026
b2e04ff
feat(examples): add gcp-grpc-spring-otel-agent-example and gcp-grpc-s…
Copilot Aug 15, 2026
c0dd9b0
test: verify StackdriverLogstashEncoder selects correct MDC trace pro…
Copilot Aug 15, 2026
f1aaf2a
refactor: reuse main java/proto sources from base modules in *-otel-*…
Copilot Aug 16, 2026
72ebf9c
Add workflow for publishing snapshots
skjolber Aug 17, 2026
e50c1e7
Merge branch 'opentelemetry' of github.com:entur/cloud-logging into o…
skjolber Aug 17, 2026
b17db79
Merge branch 'main' of github.com:entur/cloud-logging into opentelemetry
skjolber Aug 17, 2026
9fb8628
Reuse NoOpenTelemetryAgentCondition (#460)
skjolber Aug 17, 2026
8281161
Adjust publishing
skjolber Aug 17, 2026
bb1725c
Adjust publishing
skjolber Aug 17, 2026
df3c173
Opentelemetry: Restore project id in trace (#462)
skjolber Aug 19, 2026
454c826
Potential fix for pull request finding
skjolber Aug 24, 2026
eeaa4f6
Potential fix for pull request finding
skjolber Aug 24, 2026
982c89a
Fix AzureLogstashEncoder to traverse nested providers when replacing …
Copilot Aug 24, 2026
fd3d87d
Futureproof sampled parser
skjolber Aug 24, 2026
6a75022
Merge branch 'main' into opentelemetry
skjolber Aug 24, 2026
a6958e4
Potential fix for pull request finding
skjolber Aug 24, 2026
e222400
Add AzureOpenTelemetryTraceMdcJsonProviderTest
Copilot Aug 24, 2026
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
8 changes: 6 additions & 2 deletions .github/workflows/deploy-manual-snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ on:

jobs:
deploy-snapshot-gradle:
uses: entur/gha-maven-central/.github/workflows/gradle-publish-snapshot.yml@copilot/gradle-publish-snapshot
uses: entur/gha-maven-central/.github/workflows/gradle-publish.yml@v1
secrets: inherit
permissions:
contents: read
contents: write
issues: write
with:
java_version: 25
next_version: ${{ inputs.version-increment }}
version_strategy: tag
snapshot: true
push_to_repo: false
Comment thread
Copilot marked this conversation as resolved.

Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,49 @@

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.joran.spi.DefaultClass;
import net.logstash.logback.composite.AbstractNestedJsonProvider;
import net.logstash.logback.composite.JsonProvider;
import net.logstash.logback.composite.JsonProviders;
import net.logstash.logback.composite.loggingevent.LoggingEventJsonProviders;
import net.logstash.logback.composite.loggingevent.MdcJsonProvider;
import net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder;

public class AzureLogstashEncoder extends LoggingEventCompositeJsonEncoder {

@Override
@DefaultClass(LoggingEventJsonProviders.class)
public void setProviders(JsonProviders<ILoggingEvent> jsonProviders) {

if(AzureOpenTelemetryTraceMdcJsonProvider.isOtelAgent()) {
// replace MDC provider with our own which translates agent trace_id and span_id to traceId and spanId
// see https://docs.azure.cn/en-us/spring-apps/basic-standard/structured-app-log
replaceMdcProvider(jsonProviders);
}

AzureServiceContextJsonProvider azureServiceContextJsonProvider = new AzureServiceContextJsonProvider();
azureServiceContextJsonProvider.autodetectService();
jsonProviders.addProvider(azureServiceContextJsonProvider);
super.setProviders(jsonProviders);
}

@SuppressWarnings("unchecked")
private boolean replaceMdcProvider(JsonProviders<ILoggingEvent> providers) {
for (JsonProvider<ILoggingEvent> jsonProvider : providers.getProviders()) {
if (jsonProvider instanceof MdcJsonProvider) {
providers.removeProvider(jsonProvider);
providers.addProvider(new AzureOpenTelemetryTraceMdcJsonProvider());
return true;
}
if (jsonProvider instanceof AbstractNestedJsonProvider) {
JsonProviders<ILoggingEvent> nested = ((AbstractNestedJsonProvider<ILoggingEvent>) jsonProvider).getProviders();
if (replaceMdcProvider(nested)) {
return true;
}
}
}
return false;
}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package no.entur.logging.cloud.azure.logback.logstash;

import ch.qos.logback.classic.spi.ILoggingEvent;
import net.logstash.logback.composite.AbstractJsonProvider;
import tools.jackson.core.JsonGenerator;

import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.Map;

/**
* An MDC provider that maps OpenTelemetry trace fields to the special JSON fields
* recognized by Azure: https://docs.azure.cn/en-us/spring-apps/basic-standard/structured-app-log
*
*/
public class AzureOpenTelemetryTraceMdcJsonProvider extends AbstractJsonProvider<ILoggingEvent> {

public static final String OPENTELEMETRY_TRACE_ID_KEY = "trace_id";
public static final String OPENTELEMETRY_SPAN_ID_KEY = "span_id";

public static final String AZURE_TRACE_KEY = "traceId";
public static final String AZURE_SPAN_ID_KEY = "spanId";

@Override
public void writeTo(JsonGenerator generator, ILoggingEvent event) {
Map<String, String> mdcProperties = event.getMDCPropertyMap();
if (mdcProperties == null || mdcProperties.isEmpty()) {
return;
}

// map OTel MDC keys to Azure trace fields; write all others as-is.
for (Map.Entry<String, String> entry : mdcProperties.entrySet()) {
String key = entry.getKey();
if (key == null) continue;
String value = entry.getValue();
if (value == null) continue;

switch (key) {
case OPENTELEMETRY_TRACE_ID_KEY -> generator.writeStringProperty(AZURE_TRACE_KEY, value);
case OPENTELEMETRY_SPAN_ID_KEY -> generator.writeStringProperty(AZURE_SPAN_ID_KEY, value);
default -> generator.writeStringProperty(key, value);
Comment thread
skjolber marked this conversation as resolved.
}
}
}

public static boolean isOtelAgent() {
// 1. Check direct JVM command-line arguments (-javaagent)
List<String> jvmArgs = ManagementFactory.getRuntimeMXBean().getInputArguments();
for (String arg : jvmArgs) {
if (isOtelArgument(arg)) {
return true;
}
}

// 2. Backup check for environment variables that inject JVM arguments
String javaToolOptions = System.getenv("JAVA_TOOL_OPTIONS");
if (javaToolOptions != null && isOtelArgument(javaToolOptions)) {
return true;
}

return false;
}

private static boolean isOtelArgument(String argument) {
String lowerArg = argument.toLowerCase();
return lowerArg.contains("-javaagent:") && lowerArg.contains("opentelemetry");
Comment thread
skjolber marked this conversation as resolved.
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package no.entur.logging.cloud.azure.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 AzureOpenTelemetryTraceMdcJsonProviderTest {

private static final JsonMapper MAPPER = JsonMapper.builder().build();

@Test
void writeTo_openTelemetryTraceFields_mappedToAzureFields() throws Exception {
Map<String, String> mdc = new LinkedHashMap<>();
mdc.put(AzureOpenTelemetryTraceMdcJsonProvider.OPENTELEMETRY_TRACE_ID_KEY, "06796866738c859f2f19b7cfb3214824");
mdc.put(AzureOpenTelemetryTraceMdcJsonProvider.OPENTELEMETRY_SPAN_ID_KEY, "000000000000004a");

JsonNode root = write(mdc);

assertThat(root.get(AzureOpenTelemetryTraceMdcJsonProvider.AZURE_TRACE_KEY).asText())
.isEqualTo("06796866738c859f2f19b7cfb3214824");
assertThat(root.get(AzureOpenTelemetryTraceMdcJsonProvider.AZURE_SPAN_ID_KEY).asText())
.isEqualTo("000000000000004a");
assertThat(root.has(AzureOpenTelemetryTraceMdcJsonProvider.OPENTELEMETRY_TRACE_ID_KEY)).isFalse();
assertThat(root.has(AzureOpenTelemetryTraceMdcJsonProvider.OPENTELEMETRY_SPAN_ID_KEY)).isFalse();
}

@Test
void writeTo_unrelatedMdcFields_preserved() throws Exception {
Map<String, String> mdc = new LinkedHashMap<>();
mdc.put(AzureOpenTelemetryTraceMdcJsonProvider.OPENTELEMETRY_TRACE_ID_KEY, "abc");
mdc.put("correlationId", "xyz123");
mdc.put("userId", "user42");

JsonNode root = write(mdc);

assertThat(root.get("correlationId").asText()).isEqualTo("xyz123");
assertThat(root.get("userId").asText()).isEqualTo("user42");
}

@Test
void writeTo_collisionBetweenOtelAndAzureKey_otelKeyMappedFirst() throws Exception {
// When MDC contains both the OTel key (trace_id) and the Azure target key (traceId),
// the OTel key is translated to traceId and appears in the output.
Map<String, String> mdc = new LinkedHashMap<>();
mdc.put(AzureOpenTelemetryTraceMdcJsonProvider.OPENTELEMETRY_TRACE_ID_KEY, "otel-trace-value");
mdc.put(AzureOpenTelemetryTraceMdcJsonProvider.AZURE_TRACE_KEY, "existing-azure-trace");

JsonNode root = write(mdc);

// The OTel trace_id is mapped to traceId; readTree retains the last value on duplicate keys.
assertThat(root.get(AzureOpenTelemetryTraceMdcJsonProvider.AZURE_TRACE_KEY)).isNotNull();
assertThat(root.has(AzureOpenTelemetryTraceMdcJsonProvider.OPENTELEMETRY_TRACE_ID_KEY)).isFalse();
}

@Test
void writeTo_emptyMdc_writesNothing() throws Exception {
JsonNode root = write(new LinkedHashMap<>());
assertThat(root.size()).isEqualTo(0);
}

@Test
void writeTo_nullMdc_writesNothing() throws Exception {
AzureOpenTelemetryTraceMdcJsonProvider provider = new AzureOpenTelemetryTraceMdcJsonProvider();
ILoggingEvent event = Mockito.mock(ILoggingEvent.class);
Mockito.when(event.getMDCPropertyMap()).thenReturn(null);

StringWriter stringWriter = new StringWriter();
JsonFactory factory = new JsonFactory();
try (JsonGenerator generator = factory.createGenerator(stringWriter)) {
generator.writeStartObject();
provider.writeTo(generator, event);
generator.writeEndObject();
}
JsonNode root = MAPPER.readTree(stringWriter.toString());
assertThat(root.size()).isEqualTo(0);
}

private static JsonNode write(Map<String, String> mdcMap) throws Exception {
return MAPPER.readTree(writeRaw(mdcMap));
}

private static String writeRaw(Map<String, String> mdcMap) throws Exception {
AzureOpenTelemetryTraceMdcJsonProvider provider = new AzureOpenTelemetryTraceMdcJsonProvider();
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 stringWriter.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Default logback configuration provided for import by spring, modified to give me
<conversionRule conversionWord="clr" class="org.springframework.boot.logging.logback.ColorConverter" />
<conversionRule conversionWord="wEx" class="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />

<property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(---){faint} %clr([%15.15t]){faint} %clr(${LOG_CORRELATION_PATTERN:-}){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<property name="CONSOLE_LOG_CHARSET" value="${CONSOLE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/>
<property name="CONSOLE_LOG_THRESHOLD" value="${CONSOLE_LOG_THRESHOLD:-TRACE}"/>

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ configure(libraryProjects()) {
}

configure(jvmProjects()) {
apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'io.github.ben-manes.versions'

test {
useJUnitPlatform {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package no.entur.grpc.example;


import no.entur.logging.cloud.gcp.trace.spring.grpc.interceptor.OrderedTraceIdGrpcMdcContextServerInterceptor;
import no.entur.logging.cloud.spring.rr.grpc.OrderedGrpcLoggingServerInterceptor;
import no.entur.logging.cloud.spring.rr.grpc.RequestResponseGrpcExceptionHandlerInterceptor;
import no.entur.logging.cloud.trace.spring.grpc.interceptor.OrderedCorrelationIdGrpcMdcContextServerInterceptor;
Expand All @@ -12,7 +11,6 @@
@GrpcService(interceptors = {
// Trace
OrderedCorrelationIdGrpcMdcContextServerInterceptor.class, // add trace headers (correlation-id and such)
OrderedTraceIdGrpcMdcContextServerInterceptor.class, // add trace headers (correlation-id and such)

// logging
OrderedGrpcLoggingServerInterceptor.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package no.entur.grpc.example;


import no.entur.logging.cloud.gcp.trace.spring.grpc.interceptor.OrderedTraceIdGrpcMdcContextServerInterceptor;
import no.entur.logging.cloud.spring.ondemand.grpc.scope.GrpcLoggingScopeContextInterceptor;
import no.entur.logging.cloud.spring.rr.grpc.OrderedGrpcLoggingServerInterceptor;
import no.entur.logging.cloud.spring.rr.grpc.RequestResponseGrpcExceptionHandlerInterceptor;
Expand All @@ -14,7 +13,6 @@
GrpcLoggingScopeContextInterceptor.class,
// Trace
OrderedCorrelationIdGrpcMdcContextServerInterceptor.class, // add trace headers (correlation-id and such)
OrderedTraceIdGrpcMdcContextServerInterceptor.class, // add trace headers (correlation-id and such)

// logging
OrderedGrpcLoggingServerInterceptor.class,
Expand Down
4 changes: 4 additions & 0 deletions examples/gcp-grpc-spring-otel-agent-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# gcp-grpc-spring-otel-agent-example
Simple GRPC service example with OpenTelemetry Java agent.

This emulates the deployed application (i.e. machine-readable JSON).
93 changes: 93 additions & 0 deletions examples/gcp-grpc-spring-otel-agent-example/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
plugins {
id 'org.springframework.boot' version '4.1.0'
id "com.google.protobuf" version "0.10.0"
}

configurations {
otelAgent
}

test {
useJUnitPlatform {
includeEngines 'junit-jupiter'
}

doFirst {
jvmArgs "-javaagent:${configurations.otelAgent.singleFile}"
}

systemProperty 'otel.traces.exporter', 'logging'
systemProperty 'otel.metrics.exporter', 'none'
systemProperty 'otel.logs.exporter', 'none'
systemProperty 'otel.service.name', 'junit5-tests'

systemProperty 'otel.instrumentation.http.server.exclude-paths', '/actuator/**'
}

dependencies {
otelAgent "io.opentelemetry.javaagent:opentelemetry-javaagent:2.30.0"

implementation project(':on-demand:on-demand-spring-boot-starter-grpc')
implementation project(':gcp:spring-boot-starter-gcp-grpc-spring')
implementation project(':gcp:request-response-spring-boot-starter-gcp-grpc-spring')
implementation project(':trace:server:correlation-id-trace-grpc-netty')
implementation project(':request-response:request-response-spring-boot-autoconfigure-grpc-spring')

implementation project(':trace:mdc-context-grpc-netty')

implementation("io.grpc:grpc-api:$grpcVersion")
implementation("io.grpc:grpc-core:$grpcVersion")
implementation("io.grpc:grpc-context:$grpcVersion")
implementation("io.grpc:grpc-stub:$grpcVersion")
//implementation("io.grpc:grpc-inprocess:$grpcVersion")
implementation("io.grpc:grpc-services:$grpcVersion")
implementation("io.grpc:grpc-netty:$grpcVersion")
implementation("io.grpc:grpc-util:$grpcVersion")
implementation("org.springframework.boot:spring-boot-starter")

testImplementation project(":gcp:spring-boot-starter-gcp-grpc-spring-test")
testImplementation project(':gcp:request-response-spring-boot-starter-gcp-grpc-spring-test')
testImplementation("org.springframework.boot:spring-boot-starter-test")

// added due to grpc plugin dependency resolution problem
testImplementation "org.junit.platform:junit-platform-launcher"

testImplementation ("com.google.truth:truth:${googleTruthVersion}")
testImplementation ("com.google.truth.extensions:truth-java8-extension:${googleTruthVersion}")

testImplementation project(":test:test-logback-junit")
}

sourceSets {
main {
java {
srcDirs '../gcp-grpc-spring-without-test-artifacts-example/src/main/java'
}
proto {
srcDirs '../gcp-grpc-spring-without-test-artifacts-example/src/main/proto'
}
resources {
srcDirs '../gcp-grpc-spring-without-test-artifacts-example/src/main/resources'
}
}
test.java.srcDirs += "${protobuf.generatedFilesBaseDir}/test/java"
test.java.srcDirs += "${protobuf.generatedFilesBaseDir}/test/grpc"
}

tasks.compileTestJava { dependsOn("generateTestProto") }

protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${grpcProtobufVersion}"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion"
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}
Loading