Skip to content

feat(runtime): add session.id OTel baggage propagation - #142

Open
vaquarkhan wants to merge 1 commit into
spring-ai-community:mainfrom
vaquarkhan:feature/session-baggage-propagation
Open

feat(runtime): add session.id OTel baggage propagation#142
vaquarkhan wants to merge 1 commit into
spring-ai-community:mainfrom
vaquarkhan:feature/session-baggage-propagation

Conversation

@vaquarkhan

Copy link
Copy Markdown
Contributor

Read X-Amzn-Bedrock-AgentCore-Runtime-Session-Id from inbound requests and inject into OTel Baggage as session.id. This enables downstream services to receive session context via W3C baggage header, and completes the circuit for SessionBaggageSpanProcessor in the OTel extension (PR #112) which reads session.id from baggage.

  • AgentCoreSessionBaggageFilter: servlet filter that sets baggage
  • AgentCoreBaggagePropagationAutoConfiguration: activates only when OTel API is on classpath, targets /invocations only
  • Disableable via spring.ai.agentcore.baggage.enabled=false
  • 10 tests covering filter behavior and auto-configuration

Closes #71 (baggage propagation component)
Related: #72, #112

@vaquarkhan
vaquarkhan force-pushed the feature/session-baggage-propagation branch 2 times, most recently from 6b946b9 to 1416683 Compare June 17, 2026 05:18
Read X-Amzn-Bedrock-AgentCore-Runtime-Session-Id from inbound requests
and inject into OTel Baggage as session.id. This enables downstream
services to receive session context via W3C baggage header, and
completes the circuit for SessionBaggageSpanProcessor in the OTel
extension (PR spring-ai-community#112) which reads session.id from baggage.

- AgentCoreSessionBaggageFilter: servlet filter that sets baggage
- AgentCoreBaggagePropagationAutoConfiguration: activates only when
  OTel API is on classpath, targets /invocations only
- Disableable via spring.ai.agentcore.baggage.enabled=false
- 10 tests covering filter behavior and auto-configuration

Closes spring-ai-community#71 (baggage propagation component)
Related: spring-ai-community#72, spring-ai-community#112
@vaquarkhan
vaquarkhan force-pushed the feature/session-baggage-propagation branch from 1416683 to 3b498c9 Compare June 17, 2026 05:18
@vaquarkhan

Copy link
Copy Markdown
Contributor Author

End-to-end validation
Beyond the unit tests in this PR, I validated the full propagation circuit at runtime against a real servlet container (not mocks).

Setup: JDK 21 + Maven, branch built clean (BUILD SUCCESS). Added an integration test that boots Tomcat on a random port (@SpringBootTest(webEnvironment = RANDOM_PORT)) and registers the production AgentCoreSessionBaggageFilter plus the real W3CBaggagePropagator.

What the test exercises:

Inbound - sends a real HTTP POST with X-Amzn-Bedrock-AgentCore-Runtime-Session-Id: sess-e2e-12345. The filter injects it into OTel Baggage as session.id, and the endpoint reads it back from Baggage.current().

Outbound - the endpoint calls W3CBaggagePropagator.inject() on the current context, exactly as an OTel-instrumented HTTP client would for a downstream call. The serialized header comes out as baggage: session.id=sess-e2e-12345.
Negative case - a request with no session header produces no baggage and an empty downstream header.
This confirms the complete chain: inbound header → OTel Baggage → downstream W3C baggage header. That is the circuit SessionBaggageSpanProcessor (#112) depends on to enrich spans with session.id across service hops.

Result: all tests green.

AgentCoreSessionBaggageFilterTests ............... 5/5
AgentCoreBaggagePropagationAutoConfigurationTests 5/5
BaggageEndToEndIntegrationTests .................. 2/2
Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Note: when the test endpoint was first mapped to /invocations it collided with the starter's own AgentCoreInvocationsController, so I mapped the integration endpoint to a dedicated path and registered the real filter there. The production auto-configuration still targets /invocations only — that was a test-harness detail, not a behavior change.

I can fold this integration test into the PR if you'd like it kept as a permanent regression guard.

Integration test source

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class BaggageEndToEndIntegrationTests {

private static final String SESSION_HEADER = "X-Amzn-Bedrock-AgentCore-Runtime-Session-Id";
private static final String SESSION_VALUE = "sess-e2e-12345";

@LocalServerPort
private int port;

private final TestRestTemplate rest = new TestRestTemplate();

@Test
void sessionHeaderPropagatesIntoBaggageAndDownstreamHeader() {
	HttpHeaders headers = new HttpHeaders();
	headers.add(SESSION_HEADER, SESSION_VALUE);

	ResponseEntity<String> response = this.rest.exchange("http://localhost:" + this.port + "/e2e-invoke",
			HttpMethod.POST, new HttpEntity<>("{}", headers), String.class);

	assertThat(response.getStatusCode().value()).isEqualTo(200);
	assertThat(response.getBody()).isEqualTo(SESSION_VALUE + "|session.id=" + SESSION_VALUE);
}

@Test
void requestWithoutSessionHeaderHasNoBaggage() {
	ResponseEntity<String> response = this.rest.exchange("http://localhost:" + this.port + "/e2e-invoke",
			HttpMethod.POST, new HttpEntity<>("{}", new HttpHeaders()), String.class);

	assertThat(response.getStatusCode().value()).isEqualTo(200);
	assertThat(response.getBody()).isEqualTo("none|");
}

@SpringBootApplication
static class TestApp {

	@Bean
	FilterRegistrationBean<AgentCoreSessionBaggageFilter> e2eFilter() {
		FilterRegistrationBean<AgentCoreSessionBaggageFilter> registration = new FilterRegistrationBean<>();
		registration.setFilter(new AgentCoreSessionBaggageFilter());
		registration.addUrlPatterns("/e2e-invoke");
		registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 10);
		return registration;
	}

	@RestController
	static class E2eController {

		private static final W3CBaggagePropagator PROPAGATOR = W3CBaggagePropagator.getInstance();

		@PostMapping("/e2e-invoke")
		String invoke() {
			String fromBaggage = Baggage.current().getEntryValue(AgentCoreSessionBaggageFilter.BAGGAGE_KEY);
			String observed = (fromBaggage != null) ? fromBaggage : "none";

			Map<String, String> carrier = new HashMap<>();
			TextMapSetter<Map<String, String>> setter = Map::put;
			PROPAGATOR.inject(Context.current(), carrier, setter);
			String downstreamHeader = carrier.getOrDefault("baggage", "");

			return observed + "|" + downstreamHeader;
		}
	}
}

}

@ashakirin

Copy link
Copy Markdown
Collaborator

@vaquarkhan: two points:

  1. Filter is servlet based. What will happens in case, if user will configure reactive stack: Netty?
  2. Wouldn't be spring-ai-agentcore-otel-extension module better fit for this filter?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Proposal] spring-ai-agentcore-observability (tighter scope): AgentCore-specific OTel enrichment

2 participants