From e57a1943a5afc52f83e7db02f2f3232f22b75580 Mon Sep 17 00:00:00 2001 From: Jakub Balhar Date: Mon, 15 Jun 2026 10:48:41 +0200 Subject: [PATCH 1/8] feat: add ANONYMOUS_USER_ID constant and anonymousUserId() method (#4704) - Added public static ANONYMOUS_USER_ID = 'anonymous' constant - Added anonymousUserId() method that sets user.id='anonymous' without uppercasing - Preserves existing userId() uppercasing behavior for authenticated flows --- .../zowe/apiml/product/opentelemetry/OtelRequestContext.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java b/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java index dc5059b90b..5cbf69e56b 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java +++ b/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java @@ -34,6 +34,7 @@ public final class OtelRequestContext { private static final String OK = "OK"; private static final String ERROR = "ERROR"; public static final String BASIC_AUTH_TYPE = "BASIC"; + public static final String ANONYMOUS_USER_ID = "anonymous"; private static final String OTEL_ATTRIBUTE_METHOD = "http.request.method"; private static final String OTEL_ATTRIBUTE_SCHEME = "url.scheme"; @@ -124,6 +125,10 @@ public OtelRequestContext userId(String userId) { return put(OTEL_ATTRIBUTE_USER_ID, StringUtils.upperCase(userId)); } + public OtelRequestContext anonymousUserId() { + return put(OTEL_ATTRIBUTE_USER_ID, ANONYMOUS_USER_ID); + } + public OtelRequestContext distributedIds(List distributedIds) { attributesBuilder.put(OTEL_ATTRIBUTE_DISTRIBUTED_USER_ID, distributedIds.toArray(new String[0])); return this; From 237cf3d6e6067bb68da2a2fa4c2cbe8f60ea6c11 Mon Sep 17 00:00:00 2001 From: Jakub Balhar Date: Mon, 15 Jun 2026 10:49:56 +0200 Subject: [PATCH 2/8] feat: add anonymousUserId() and authenticationSuccess() to bypass filter chain (#4704) - Chain .anonymousUserId() to set user.id='anonymous' for bypass routes - Chain .authenticationSuccess() to set auth.status='OK' for bypass routes - Both appended after .instanceId() in the apply() builder chain --- .../zowe/apiml/gateway/filters/OtelServiceFilterFactory.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactory.java index ce50a6b97e..d366f2966e 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactory.java @@ -33,7 +33,9 @@ public GatewayFilter apply(Config config) { OtelRequestContext.of(exchange) .authMethod(AuthenticationScheme.BYPASS) .serviceId(config.serviceId) - .instanceId(config.instanceId); + .instanceId(config.instanceId) + .anonymousUserId() + .authenticationSuccess(); return chain.filter(exchange); }; } From f14c7f593fbf0ea61aeb518c8e4d8758eb0d72b6 Mon Sep 17 00:00:00 2001 From: Jakub Balhar Date: Mon, 15 Jun 2026 10:54:09 +0200 Subject: [PATCH 3/8] test: add unit tests for anonymousUserId() and bypass filter attributes (#4704) OtelRequestContextTest: - ANONYMOUS_USER_ID constant equals 'anonymous' - anonymousUserId() stores lowercase 'anonymous' (not uppercased) - verify anonymousUserId() does NOT store uppercased value OtelServiceFilterFactoryTest: - verify user.id='anonymous' and auth.status='OK' for bypass routes --- .../filters/OtelServiceFilterFactoryTest.java | 2 ++ .../opentelemetry/OtelRequestContextTest.java | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java index afee11ebab..a6feafa5b8 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java @@ -41,6 +41,8 @@ void givenConfiguredFilter_whenApply_thenSetBypassServiceIdAndInstanceId() { assertEquals("bypass", attributes.get(AttributeKey.stringKey("auth.service.auth.method"))); assertEquals(SERVICE_ID.toLowerCase(), attributes.get(AttributeKey.stringKey("service.id"))); assertEquals(INSTANCE_ID.toLowerCase(), attributes.get(AttributeKey.stringKey("service.instance.id"))); + assertEquals("anonymous", attributes.get(AttributeKey.stringKey("user.id"))); + assertEquals("OK", attributes.get(AttributeKey.stringKey("auth.status"))); } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/product/opentelemetry/OtelRequestContextTest.java b/gateway-service/src/test/java/org/zowe/apiml/product/opentelemetry/OtelRequestContextTest.java index e16f595652..58851b51c0 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/product/opentelemetry/OtelRequestContextTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/product/opentelemetry/OtelRequestContextTest.java @@ -140,6 +140,23 @@ void givenOtelContext_whenSetUserId_thenStoreUpperCase() { assertEquals("USERID", getValue("user.id")); } + @Test + void givenAnonymousUserIdConstant_whenRead_thenEqualsAnonymous() { + assertEquals("anonymous", OtelRequestContext.ANONYMOUS_USER_ID); + } + + @Test + void givenOtelContext_whenSetAnonymousUserId_thenStoreLowerCaseAnonymous() { + OtelRequestContext.of(exchange).anonymousUserId(); + assertEquals("anonymous", getValue("user.id")); + } + + @Test + void givenOtelContext_whenSetAnonymousUserId_thenNotUppercase() { + OtelRequestContext.of(exchange).anonymousUserId(); + assertNotEquals("ANONYMOUS", getValue("user.id")); + } + @Test void givenOtelContext_whenSetAuthSourceType_thenStoreIt() { OtelRequestContext.of(exchange).authSourceType("JWT"); From 897541330087a0a5be34f166b2806634ded61568 Mon Sep 17 00:00:00 2001 From: Jakub Balhar Date: Mon, 15 Jun 2026 11:24:34 +0200 Subject: [PATCH 4/8] fix: update bypass assertions in OpenTelemetryResourceAttributesZosTest (#4704) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change assertNull to assertEquals('anonymous') for user.id and assertEquals('OK') for auth.status in the WhenServiceBypass > thenLog test — the OtelServiceFilterFactory now sets these attributes for bypass routes via anonymousUserId() and authenticationSuccess(). --- .../OpenTelemetryAnonymousBypassTest.java | 204 ++++++++++++++++++ ...penTelemetryResourceAttributesZosTest.java | 4 +- 2 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryAnonymousBypassTest.java diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryAnonymousBypassTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryAnonymousBypassTest.java new file mode 100644 index 0000000000..edc73ec2b7 --- /dev/null +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryAnonymousBypassTest.java @@ -0,0 +1,204 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.acceptance; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.zowe.apiml.auth.AuthenticationScheme; +import org.zowe.apiml.gateway.MockService; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.awaitility.Awaitility.await; +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.*; + +class OpenTelemetryAnonymousBypassTest { + + @Nested + @AcceptanceTest + @ActiveProfiles({"OpenTelemetryTest"}) + @TestPropertySource( + properties = { + "otel.sdk.disabled=false", + "otel.metrics.exporter=none", + "otel.traces.exporter=none", + "otel.logs.exporter=none" + } + ) + @DirtiesContext + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class WhenAnonymousBypassRequest extends AcceptanceTestWithMockServices { + + @Autowired + private LogRecordExporter logExporter; + + @LocalServerPort + private int port; + + private MockService mockServiceBypass; + + @BeforeAll + void init() { + mockServiceBypass = mockService("testservicebp") + .scope(MockService.Scope.CLASS) + .authenticationScheme(AuthenticationScheme.BYPASS) + .addEndpoint("/testservicebp/200") + .responseCode(200) + .and() + .addEndpoint("/testservicebp/500") + .responseCode(500) + .and().start(); + } + + @BeforeEach + void setUp() { + assertTrue(logExporter instanceof InMemoryLogRecordExporter, + "Expected InMemoryLogRecordExporter, got " + logExporter.getClass().getName()); + ((InMemoryLogRecordExporter) logExporter).reset(); + } + + private List assertLogsExported() { + List logs = new ArrayList<>(); + await("Log export") + .atMost(Duration.ofSeconds(10)) + .until(() -> { + var exporter = (InMemoryLogRecordExporter) logExporter; + var l = exporter.getFinishedLogRecordItems(); + if (l.size() > 0) { + logs.addAll(l); + } + exporter.reset(); + return l.size() > 0; + }); + return logs; + } + + private LogRecordData assertOneLogRecordExported(String expectedUrl) { + var logs = assertLogsExported(); + + var logRecord = logs.stream() + .filter(log -> log.getBodyValue().asString().contains(expectedUrl)) + .findFirst() + .orElseThrow(() -> new AssertionError( + "Expected log record with URL " + expectedUrl + " not found in logs: " + + logs.stream().map(LogRecordData::getBodyValue).map(String::valueOf).collect(Collectors.joining(", ")))); + + assertEquals("INFO", logRecord.getSeverityText(), + "Expected INFO log level, was " + logRecord.getSeverityText()); + + var logBody = logRecord.getBodyValue().asString(); + assertTrue(StringUtils.isNotBlank(logBody)); + + return logRecord; + } + + private Object getAttribute(String logBody, String attributeName) { + var objectMapper = new ObjectMapper(); + try { + return objectMapper.readValue(logBody, Map.class).get(attributeName); + } catch (JsonProcessingException e) { + fail("Invalid JSON", e); + return null; + } + } + + @Test + void thenLogWithAnonymousUserIdAndAuthOk() { + given() + .get(basePath + "/testservicebp/api/v1/200") + .then() + .statusCode(200); + + var logRecord = assertOneLogRecordExported("/testservicebp/api/v1/200"); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + + // Full attribute set verification + assertEquals("GET", getAttribute(logBody, "http.request.method"), + "http.request.method should be GET"); + assertEquals("https", getAttribute(logBody, "url.scheme"), + "url.scheme should be https"); + assertEquals("/testservicebp/api/v1/200", getAttribute(logBody, "url.path"), + "url.path should match request path"); + assertEquals("testservicebp", getAttribute(logBody, "service.id"), + "service.id should be testservicebp"); + assertEquals("localhost:testservicebp:" + mockServiceBypass.getPort(), + getAttribute(logBody, "service.instance.id"), + "service.instance.id should match mock service"); + assertEquals("bypass", getAttribute(logBody, "auth.service.auth.method"), + "auth.service.auth.method should be bypass"); + assertEquals("200", getAttribute(logBody, "service.response_code"), + "service.response_code should be 200"); + + // NEW behavior: anonymous user ID and OK auth status + assertEquals("anonymous", getAttribute(logBody, "user.id"), + "user.id should be 'anonymous' for bypass routes"); + assertEquals("OK", getAttribute(logBody, "auth.status"), + "auth.status should be 'OK' for bypass routes"); + + // Error attributes should NOT be present for successful bypass + assertNull(getAttribute(logBody, "auth.error.type"), + "auth.error.type should be null for successful bypass"); + assertNull(getAttribute(logBody, "auth.error.message"), + "auth.error.message should be null for successful bypass"); + } + + @Test + void thenLogWithErrorAttributesOnRoutingError() { + given() + .get(basePath + "/testservicebp/api/v1/500") + .then() + .statusCode(500); + + var logRecord = assertOneLogRecordExported("/testservicebp/api/v1/500"); + @SuppressWarnings("null") + var logBody = logRecord.getBodyValue().asString(); + + // Standard attributes still present + assertEquals("GET", getAttribute(logBody, "http.request.method")); + assertEquals("https", getAttribute(logBody, "url.scheme")); + assertEquals("/testservicebp/api/v1/500", getAttribute(logBody, "url.path")); + assertEquals("testservicebp", getAttribute(logBody, "service.id")); + assertEquals("localhost:testservicebp:" + mockServiceBypass.getPort(), + getAttribute(logBody, "service.instance.id")); + assertEquals("bypass", getAttribute(logBody, "auth.service.auth.method")); + assertEquals("500", getAttribute(logBody, "service.response_code"), + "service.response_code should be 500 for error endpoint"); + + // Anonymous user ID and OK auth status still apply (bypass auth succeeded) + assertEquals("anonymous", getAttribute(logBody, "user.id"), + "user.id should be 'anonymous' for bypass routes even on error"); + assertEquals("OK", getAttribute(logBody, "auth.status"), + "auth.status should be 'OK' for bypass routes even on error"); + + // Error attributes: service error but auth succeeded, so no auth.error attributes + assertNull(getAttribute(logBody, "auth.error.type"), + "auth.error.type should be null — bypass auth always succeeds"); + assertNull(getAttribute(logBody, "auth.error.message"), + "auth.error.message should be null — bypass auth always succeeds"); + } + } +} diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index f4ecffb8fd..591dde1343 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -538,10 +538,10 @@ void thenLog() { assertAttributesBase(logRecord.getResource().getAttributes(), port); @SuppressWarnings("null") var logBody = logRecord.getBodyValue().asString(); - assertNull(getAttribute(logBody, "user.id")); + assertEquals("anonymous", getAttribute(logBody, "user.id")); assertEquals("testservicebp", getAttribute(logBody, "service.id")); assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertNull(getAttribute(logBody, "auth.status")); + assertEquals("OK", getAttribute(logBody, "auth.status")); assertEquals("localhost:testservicebp:" + mockServiceBypass.getPort(), getAttribute(logBody, "service.instance.id")); assertEquals("200", getAttribute(logBody, "service.response_code")); assertEquals("/testservicebp/api/v1/200", getAttribute(logBody, "url.path")); From f8442635046d08a06a56a2bfeeb663c620137d5e Mon Sep 17 00:00:00 2001 From: Jakub Balhar Date: Mon, 15 Jun 2026 12:18:22 +0200 Subject: [PATCH 5/8] fix: conditionally apply bypass attributes in OtelServiceFilterFactory (#4704) OtelServiceFilterFactory was unconditionally setting user.id='anonymous' and auth.status='OK' for ALL routes, overwriting values set by the real auth scheme filter (AbstractAuthSchemeFactory, etc.). Add authenticationScheme config field to OtelServiceFilterFactory.Config and only chain .anonymousUserId() + .authenticationSuccess() for BYPASS routes. Pass the scheme from RouteLocator.getPostRoutingFilters() via the new Authentication auth parameter. --- .../filters/OtelServiceFilterFactory.java | 12 ++++++---- .../apiml/gateway/service/RouteLocator.java | 7 ++++-- .../filters/OtelServiceFilterFactoryTest.java | 1 + .../gateway/service/RouteLocatorTest.java | 22 +++++++++---------- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactory.java index d366f2966e..c01d94ee9b 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactory.java @@ -30,12 +30,15 @@ public OtelServiceFilterFactory() { @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { - OtelRequestContext.of(exchange) + var ctx = OtelRequestContext.of(exchange) .authMethod(AuthenticationScheme.BYPASS) .serviceId(config.serviceId) - .instanceId(config.instanceId) - .anonymousUserId() - .authenticationSuccess(); + .instanceId(config.instanceId); + + if (AuthenticationScheme.BYPASS.name().equalsIgnoreCase(config.authenticationScheme)) { + ctx.anonymousUserId() + .authenticationSuccess(); + } return chain.filter(exchange); }; } @@ -46,6 +49,7 @@ public static class Config { private String instanceId; private String serviceId; + private String authenticationScheme; } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/RouteLocator.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/RouteLocator.java index 7093fb5666..d1396217a2 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/service/RouteLocator.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/service/RouteLocator.java @@ -111,7 +111,7 @@ static List join(List a, List b) { return output; } - List getPostRoutingFilters(ServiceInstance serviceInstance, RoutedService routedService) { + List getPostRoutingFilters(ServiceInstance serviceInstance, RoutedService routedService, Authentication auth) { List serviceRelated = new LinkedList<>(); if (forwardingClientCertEnabled && Optional.ofNullable(serviceInstance.getMetadata().get(SERVICE_SUPPORTING_CLIENT_CERT_FORWARDING)) @@ -162,6 +162,9 @@ List getPostRoutingFilters(ServiceInstance serviceInstance, Ro otelRequestBasicFilter.setName("OtelServiceFilterFactory"); otelRequestBasicFilter.addArg("serviceId", serviceInstance.getServiceId()); otelRequestBasicFilter.addArg("instanceId", serviceInstance.getInstanceId()); + if (auth != null && auth.getScheme() != null) { + otelRequestBasicFilter.addArg("authenticationScheme", auth.getScheme().name()); + } serviceRelated.add(otelRequestBasicFilter); } @@ -186,7 +189,7 @@ private List getAuthFilterPerRoute( // generate a new routing rule by a specific produces RouteDefinition routeDefinition = rdp.get(serviceInstance, routedService); routeDefinition.setOrder(orderHolder.getAndIncrement()); - routeDefinition.getFilters().addAll(getPostRoutingFilters(serviceInstance, routedService)); + routeDefinition.getFilters().addAll(getPostRoutingFilters(serviceInstance, routedService, auth)); setAuth(serviceInstance, routeDefinition, auth); return routeDefinition; diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java index a6feafa5b8..b9186a10a0 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java @@ -34,6 +34,7 @@ void givenConfiguredFilter_whenApply_thenSetBypassServiceIdAndInstanceId() { var config = new OtelServiceFilterFactory.Config(); config.setServiceId(SERVICE_ID); config.setInstanceId(INSTANCE_ID); + config.setAuthenticationScheme("BYPASS"); new OtelServiceFilterFactory().apply(config).filter(exchange, e -> Mono.empty().then()); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java index ce0cb99403..386d2d6246 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java @@ -269,7 +269,7 @@ void enableForwarding() { void givenServiceAllowingCertForwarding_whenGetPostRoutingFilters_thenAddClientCertFilterFactory() { ServiceInstance serviceInstance = createServiceInstance(Boolean.TRUE, null, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService, null); assertEquals(3, filterDefinitions.size()); // common filters + PageRedirectionFilterFactory assertEquals("ForwardClientCertFilterFactory", filterDefinitions.get(1).getName()); } @@ -278,7 +278,7 @@ void givenServiceAllowingCertForwarding_whenGetPostRoutingFilters_thenAddClientC void givenServiceNotAllowingCertForwarding_whenGetPostRoutingFilters_thenReturnJustCommon() { ServiceInstance serviceInstance = createServiceInstance(Boolean.FALSE, null, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService, null); assertTrue(filterDefinitions.containsAll(COMMON_FILTERS), "Not all common filters are defined"); assertEquals(2, filterDefinitions.size()); assertTrue(filterDefinitions.stream().noneMatch(filter -> "ForwardClientCertFilterFactory".equals(filter.getName()))); @@ -289,7 +289,7 @@ void givenServiceNotAllowingCertForwarding_whenGetPostRoutingFilters_thenReturnJ void givenServiceWithoutCertForwardingConfig_whenGetPostRoutingFilters_thenReturnJustCommon() { ServiceInstance serviceInstance = createServiceInstance(null, null, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService, null); assertTrue(filterDefinitions.containsAll(COMMON_FILTERS), "Not all common filters are defined"); assertEquals(2, filterDefinitions.size()); assertTrue(filterDefinitions.stream().noneMatch(filter -> "ForwardClientCertFilterFactory".equals(filter.getName()))); @@ -309,7 +309,7 @@ void disableForwarding() { void givenAnyService_whenGetPostRoutingFilters_thenReturnJustCommon() { ServiceInstance serviceInstance = createServiceInstance(Boolean.TRUE, null, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService, null); assertTrue(filterDefinitions.containsAll(COMMON_FILTERS), "Not all common filters are defined"); assertEquals(2, filterDefinitions.size()); assertTrue(filterDefinitions.stream().noneMatch(filter -> "ForwardClientCertFilterFactory".equals(filter.getName()))); @@ -323,7 +323,7 @@ class EncodedCharacters { @Test void givenServiceAllowingEncodedCharacters_whenGetPostRoutingFilters_thenReturnJustCommon() { ServiceInstance serviceInstance = createServiceInstance(null, Boolean.TRUE, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService, null); assertTrue(filterDefinitions.containsAll(COMMON_FILTERS), "Not all common filters are defined"); assertEquals(2, filterDefinitions.size()); assertTrue(filterDefinitions.stream().noneMatch(filter -> "ForbidEncodedCharactersFilterFactory".equals(filter.getName()))); @@ -332,7 +332,7 @@ void givenServiceAllowingEncodedCharacters_whenGetPostRoutingFilters_thenReturnJ @Test void givenServiceNotAllowingEncodedCharacters_whenGetPostRoutingFilters_thenAddEncodedCharacterFilterFactory() { ServiceInstance serviceInstance = createServiceInstance(null, Boolean.FALSE, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService, null); assertEquals(3, filterDefinitions.size()); assertEquals("ForbidEncodedCharactersFilterFactory", filterDefinitions.get(1).getName()); } @@ -340,7 +340,7 @@ void givenServiceNotAllowingEncodedCharacters_whenGetPostRoutingFilters_thenAddE @Test void givenServiceWithoutAllowingEncodedCharacters_whenGetPostRoutingFilters_thenAddEncodedCharacterFilterFactory() { ServiceInstance serviceInstance = createServiceInstance(null, null, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService, null); assertTrue(filterDefinitions.containsAll(COMMON_FILTERS), "Not all common filters are defined"); assertEquals(2, filterDefinitions.size()); assertTrue(filterDefinitions.stream().noneMatch(filter -> "ForbidEncodedCharactersFilterFactory".equals(filter.getName()))); @@ -354,7 +354,7 @@ class RateLimiter { @Test void givenServiceNotAllowingRateLimiter_whenGetPostRoutingFilters_thenReturnJustCommon() { ServiceInstance serviceInstance = createServiceInstance(null, null, Boolean.FALSE); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService, null); assertTrue(filterDefinitions.containsAll(COMMON_FILTERS), "Not all common filters are defined"); assertEquals(2, filterDefinitions.size()); assertTrue(filterDefinitions.stream().noneMatch(filter -> "InMemoryRateLimiterFilterFactory".equals(filter.getName()))); @@ -363,7 +363,7 @@ void givenServiceNotAllowingRateLimiter_whenGetPostRoutingFilters_thenReturnJust @Test void givenServiceAllowingRateLimiter_whenGetPostRoutingFilters_thenAddInMemoryRateLimiterFilterFactory() { ServiceInstance serviceInstance = createServiceInstance(null, null, Boolean.TRUE); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService, null); assertEquals(3, filterDefinitions.size()); assertEquals("InMemoryRateLimiterFilterFactory", filterDefinitions.get(1).getName()); } @@ -371,7 +371,7 @@ void givenServiceAllowingRateLimiter_whenGetPostRoutingFilters_thenAddInMemoryRa @Test void givenServiceWithoutAllowingRateLimiter_whenGetPostRoutingFilters_thenDoNotAddInMemoryRateLimiterFilterFactory() { ServiceInstance serviceInstance = createServiceInstance(null, null, null); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService, null); assertTrue(filterDefinitions.containsAll(COMMON_FILTERS), "Not all common filters are defined"); assertEquals(2, filterDefinitions.size()); assertTrue(filterDefinitions.stream().noneMatch(filter -> "InMemoryRateLimiterFilterFactory".equals(filter.getName()))); @@ -390,7 +390,7 @@ void setUp() { @Test void givenEnabledOtel_whenGetPostRoutingFilters_thenOtelServiceFilterFactoryIsCreated() { ServiceInstance serviceInstance = createServiceInstance(null, null, Boolean.TRUE); - List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService, null); assertEquals(4, filterDefinitions.size()); var filter = filterDefinitions.get(3); From 3b566b2d81604958a3537d5e570a6429876cb876 Mon Sep 17 00:00:00 2001 From: Jakub Balhar Date: Tue, 16 Jun 2026 14:12:50 +0200 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20address=20PR=20#4716=20review=20comm?= =?UTF-8?q?ents=20=E2=80=94=20coverage,=20constants,=20test=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make AUTH_STATUS_OK/AUTH_STATUS_ERROR public constants (non-blocking feedback) - Remove redundant thenNotUppercase test - Add test for non-BYPASS path in OtelServiceFilterFactory (coverage gap) - Add test for auth!=null branch in RouteLocator (coverage gap) --- .../opentelemetry/OtelRequestContext.java | 8 +++--- .../filters/OtelServiceFilterFactoryTest.java | 25 +++++++++++++++++-- .../gateway/service/RouteLocatorTest.java | 14 +++++++++++ .../opentelemetry/OtelRequestContextTest.java | 10 ++------ 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java b/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java index 5cbf69e56b..c33f98ffde 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java +++ b/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java @@ -31,8 +31,8 @@ public final class OtelRequestContext { public static final String OTEL_CONTEXT = "otel-context"; - private static final String OK = "OK"; - private static final String ERROR = "ERROR"; + public static final String AUTH_STATUS_OK = "OK"; + public static final String AUTH_STATUS_ERROR = "ERROR"; public static final String BASIC_AUTH_TYPE = "BASIC"; public static final String ANONYMOUS_USER_ID = "anonymous"; @@ -106,7 +106,7 @@ public OtelRequestContext authMethod(String authenticationScheme) { } public OtelRequestContext authenticationFailed() { - return put(OTEL_ATTRIBUTE_AUTH_STATUS, ERROR); + return put(OTEL_ATTRIBUTE_AUTH_STATUS, AUTH_STATUS_ERROR); } public OtelRequestContext authErrorType(String authErrorType) { @@ -118,7 +118,7 @@ public OtelRequestContext authErrorMessage(String authErrorMessage) { } public OtelRequestContext authenticationSuccess() { - return put(OTEL_ATTRIBUTE_AUTH_STATUS, OK); + return put(OTEL_ATTRIBUTE_AUTH_STATUS, AUTH_STATUS_OK); } public OtelRequestContext userId(String userId) { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java index b9186a10a0..26b86d2056 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java @@ -19,7 +19,7 @@ import org.zowe.apiml.product.opentelemetry.OtelRequestContext; import reactor.core.publisher.Mono; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; class OtelServiceFilterFactoryTest { @@ -43,7 +43,28 @@ void givenConfiguredFilter_whenApply_thenSetBypassServiceIdAndInstanceId() { assertEquals(SERVICE_ID.toLowerCase(), attributes.get(AttributeKey.stringKey("service.id"))); assertEquals(INSTANCE_ID.toLowerCase(), attributes.get(AttributeKey.stringKey("service.instance.id"))); assertEquals("anonymous", attributes.get(AttributeKey.stringKey("user.id"))); - assertEquals("OK", attributes.get(AttributeKey.stringKey("auth.status"))); + assertEquals(OtelRequestContext.AUTH_STATUS_OK, attributes.get(AttributeKey.stringKey("auth.status"))); + } + + @Test + void givenConfiguredFilterWithoutBypass_whenApply_thenDoNotSetAnonymousUserAndAuthStatus() { + MockServerHttpRequest request = MockServerHttpRequest.get("/aPath").build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + + var config = new OtelServiceFilterFactory.Config(); + config.setServiceId(SERVICE_ID); + config.setInstanceId(INSTANCE_ID); + // authenticationScheme left unset (null) — not a BYPASS route + + new OtelServiceFilterFactory().apply(config).filter(exchange, e -> Mono.empty().then()); + + var attributes = ((AttributesBuilder) ReflectionTestUtils.getField(OtelRequestContext.of(exchange), "attributesBuilder")).build(); + assertEquals("bypass", attributes.get(AttributeKey.stringKey("auth.service.auth.method"))); + assertEquals(SERVICE_ID.toLowerCase(), attributes.get(AttributeKey.stringKey("service.id"))); + assertEquals(INSTANCE_ID.toLowerCase(), attributes.get(AttributeKey.stringKey("service.instance.id"))); + // user.id and auth.status should remain unset for non-BYPASS routes + assertNull(attributes.get(AttributeKey.stringKey("user.id"))); + assertNull(attributes.get(AttributeKey.stringKey("auth.status"))); } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java index 386d2d6246..2e7b94ecff 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/service/RouteLocatorTest.java @@ -399,6 +399,20 @@ void givenEnabledOtel_whenGetPostRoutingFilters_thenOtelServiceFilterFactoryIsCr assertEquals("dummy:instance:80", filter.getArgs().get("instanceId")); } + @Test + void givenEnabledOtelWithBypassAuth_whenGetPostRoutingFilters_thenIncludeAuthenticationSchemeArg() { + ServiceInstance serviceInstance = createServiceInstance(null, null, Boolean.TRUE); + var auth = new Authentication(AuthenticationScheme.BYPASS, null); + List filterDefinitions = routeLocator.getPostRoutingFilters(serviceInstance, routedService, auth); + assertEquals(4, filterDefinitions.size()); + + var filter = filterDefinitions.get(3); + assertEquals("OtelServiceFilterFactory", filter.getName()); + assertEquals("dummy", filter.getArgs().get("serviceId")); + assertEquals("dummy:instance:80", filter.getArgs().get("instanceId")); + assertEquals("BYPASS", filter.getArgs().get("authenticationScheme")); + } + } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/product/opentelemetry/OtelRequestContextTest.java b/gateway-service/src/test/java/org/zowe/apiml/product/opentelemetry/OtelRequestContextTest.java index 58851b51c0..f0e2616b44 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/product/opentelemetry/OtelRequestContextTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/product/opentelemetry/OtelRequestContextTest.java @@ -113,7 +113,7 @@ void givenOtelContext_whenSetZoweJwtAuthMethod_thenTransformToString() { @Test void givenOtelContext_whenAuthenticationFailed_thenStoreFailedStringAsStatus() { OtelRequestContext.of(exchange).authenticationFailed(); - assertEquals("ERROR", getValue("auth.status")); + assertEquals(OtelRequestContext.AUTH_STATUS_ERROR, getValue("auth.status")); } @Test @@ -131,7 +131,7 @@ void givenOtelContext_whenAuthErrorType_thenStoreErrorTypeAsAuthErrorType() { @Test void givenOtelContext_whenauthenticationSuccess_thenStoreOkStringAsStatus() { OtelRequestContext.of(exchange).authenticationSuccess(); - assertEquals("OK", getValue("auth.status")); + assertEquals(OtelRequestContext.AUTH_STATUS_OK, getValue("auth.status")); } @Test @@ -151,12 +151,6 @@ void givenOtelContext_whenSetAnonymousUserId_thenStoreLowerCaseAnonymous() { assertEquals("anonymous", getValue("user.id")); } - @Test - void givenOtelContext_whenSetAnonymousUserId_thenNotUppercase() { - OtelRequestContext.of(exchange).anonymousUserId(); - assertNotEquals("ANONYMOUS", getValue("user.id")); - } - @Test void givenOtelContext_whenSetAuthSourceType_thenStoreIt() { OtelRequestContext.of(exchange).authSourceType("JWT"); From 084ebfda79aea1925509d631caf4a26e168fc269 Mon Sep 17 00:00:00 2001 From: Jakub Balhar Date: Wed, 17 Jun 2026 13:06:39 +0200 Subject: [PATCH 7/8] review: Rename BASIC_AUTH_TYPE constant, add Javadoc, add non-BYPASS test - Rename BASIC_AUTH_TYPE -> AUTH_TYPE_BASIC for naming consistency with AUTH_STATUS_OK, AUTH_STATUS_ERROR, and ANONYMOUS_USER_ID constants - Add Javadoc for Config.authenticationScheme field documenting its purpose - Add test for explicitly non-BYPASS authenticationScheme (e.g. ZOWE_JWT) to complement the existing null-case test --- .../zowe/apiml/filter/BasicLoginFilter.java | 2 +- .../filters/OtelServiceFilterFactory.java | 1 + .../opentelemetry/OtelRequestContext.java | 2 +- .../filters/OtelServiceFilterFactoryTest.java | 20 +++++++++++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/apiml/src/main/java/org/zowe/apiml/filter/BasicLoginFilter.java b/apiml/src/main/java/org/zowe/apiml/filter/BasicLoginFilter.java index 472159f9c6..b6d70a41f4 100644 --- a/apiml/src/main/java/org/zowe/apiml/filter/BasicLoginFilter.java +++ b/apiml/src/main/java/org/zowe/apiml/filter/BasicLoginFilter.java @@ -79,7 +79,7 @@ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { .switchIfEmpty(Mono.defer(() -> chain.filter(exchange).then(Mono.empty()))) .flatMap(credentials -> { var otelContext = OtelRequestContext.of(exchange); - otelContext.authSourceType(OtelRequestContext.BASIC_AUTH_TYPE); + otelContext.authSourceType(OtelRequestContext.AUTH_TYPE_BASIC); return authenticationManager.authenticate(credentials) .flatMap(authentication -> chain.filter(exchange) .contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication))); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactory.java index c01d94ee9b..a7558621c2 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactory.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactory.java @@ -49,6 +49,7 @@ public static class Config { private String instanceId; private String serviceId; + /** The {@link AuthenticationScheme#name()} for this route, or null if unavailable. */ private String authenticationScheme; } diff --git a/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java b/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java index c33f98ffde..857fa14fcd 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java +++ b/gateway-service/src/main/java/org/zowe/apiml/product/opentelemetry/OtelRequestContext.java @@ -33,7 +33,7 @@ public final class OtelRequestContext { public static final String AUTH_STATUS_OK = "OK"; public static final String AUTH_STATUS_ERROR = "ERROR"; - public static final String BASIC_AUTH_TYPE = "BASIC"; + public static final String AUTH_TYPE_BASIC = "BASIC"; public static final String ANONYMOUS_USER_ID = "anonymous"; private static final String OTEL_ATTRIBUTE_METHOD = "http.request.method"; diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java index 26b86d2056..c8a4e00ccc 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/OtelServiceFilterFactoryTest.java @@ -67,4 +67,24 @@ void givenConfiguredFilterWithoutBypass_whenApply_thenDoNotSetAnonymousUserAndAu assertNull(attributes.get(AttributeKey.stringKey("auth.status"))); } + @Test + void givenConfiguredFilterWithNonBypassAuth_whenApply_thenDoNotSetAnonymousUserAndAuthStatus() { + MockServerHttpRequest request = MockServerHttpRequest.get("/aPath").build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + + var config = new OtelServiceFilterFactory.Config(); + config.setServiceId(SERVICE_ID); + config.setInstanceId(INSTANCE_ID); + config.setAuthenticationScheme("ZOWE_JWT"); // explicitly non-BYPASS + + new OtelServiceFilterFactory().apply(config).filter(exchange, e -> Mono.empty().then()); + + var attributes = ((AttributesBuilder) ReflectionTestUtils.getField(OtelRequestContext.of(exchange), "attributesBuilder")).build(); + assertEquals("bypass", attributes.get(AttributeKey.stringKey("auth.service.auth.method"))); + assertEquals(SERVICE_ID.toLowerCase(), attributes.get(AttributeKey.stringKey("service.id"))); + assertEquals(INSTANCE_ID.toLowerCase(), attributes.get(AttributeKey.stringKey("service.instance.id"))); + assertNull(attributes.get(AttributeKey.stringKey("user.id"))); + assertNull(attributes.get(AttributeKey.stringKey("auth.status"))); + } + } From 3ab3e0c385317f8c2bee0733b6b9d0f85d0c2a72 Mon Sep 17 00:00:00 2001 From: Jakub Balhar Date: Thu, 18 Jun 2026 10:03:28 +0200 Subject: [PATCH 8/8] review: Address PR comments on OpenTelemetryAnonymousBypassTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add class-level Javadoc documenting the test purpose - Remove @DirtiesContext (not needed — exporter reset in setUp() prevents leaks) - Type logExporter field as InMemoryLogRecordExporter, eliminating cast in setUp() - Fix redundant getBodyValue() in error message: use .asString() consistently - Replace getAttribute() with parseLogBody() that parses JSON once into Map and assert from the map, avoiding ~10 redundant ObjectMapper creations --- .../OpenTelemetryAnonymousBypassTest.java | 87 ++++++++++--------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryAnonymousBypassTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryAnonymousBypassTest.java index edc73ec2b7..979edef984 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryAnonymousBypassTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryAnonymousBypassTest.java @@ -13,13 +13,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.sdk.logs.export.LogRecordExporter; import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.auth.AuthenticationScheme; @@ -35,6 +33,12 @@ import static io.restassured.RestAssured.given; import static org.junit.jupiter.api.Assertions.*; +/** + * Verifies that bypass-authentication routes emit OTel log signals with + * {@code user.id=anonymous} and {@code auth.status=OK} instead of null + * attributes (see #4704). Covers both successful (200) and downstream + * error (500) scenarios. + */ class OpenTelemetryAnonymousBypassTest { @Nested @@ -48,12 +52,11 @@ class OpenTelemetryAnonymousBypassTest { "otel.logs.exporter=none" } ) - @DirtiesContext @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WhenAnonymousBypassRequest extends AcceptanceTestWithMockServices { @Autowired - private LogRecordExporter logExporter; + private InMemoryLogRecordExporter logExporter; @LocalServerPort private int port; @@ -75,9 +78,7 @@ void init() { @BeforeEach void setUp() { - assertTrue(logExporter instanceof InMemoryLogRecordExporter, - "Expected InMemoryLogRecordExporter, got " + logExporter.getClass().getName()); - ((InMemoryLogRecordExporter) logExporter).reset(); + logExporter.reset(); } private List assertLogsExported() { @@ -85,12 +86,11 @@ private List assertLogsExported() { await("Log export") .atMost(Duration.ofSeconds(10)) .until(() -> { - var exporter = (InMemoryLogRecordExporter) logExporter; - var l = exporter.getFinishedLogRecordItems(); + var l = logExporter.getFinishedLogRecordItems(); if (l.size() > 0) { logs.addAll(l); } - exporter.reset(); + logExporter.reset(); return l.size() > 0; }); return logs; @@ -102,9 +102,13 @@ private LogRecordData assertOneLogRecordExported(String expectedUrl) { var logRecord = logs.stream() .filter(log -> log.getBodyValue().asString().contains(expectedUrl)) .findFirst() - .orElseThrow(() -> new AssertionError( - "Expected log record with URL " + expectedUrl + " not found in logs: " - + logs.stream().map(LogRecordData::getBodyValue).map(String::valueOf).collect(Collectors.joining(", ")))); + .orElseThrow(() -> { + var availableUrls = logs.stream() + .map(log -> log.getBodyValue().asString()) + .collect(Collectors.joining(", ")); + return new AssertionError( + "Expected log record with URL " + expectedUrl + " not found in logs: " + availableUrls); + }); assertEquals("INFO", logRecord.getSeverityText(), "Expected INFO log level, was " + logRecord.getSeverityText()); @@ -115,13 +119,12 @@ private LogRecordData assertOneLogRecordExported(String expectedUrl) { return logRecord; } - private Object getAttribute(String logBody, String attributeName) { - var objectMapper = new ObjectMapper(); + private Map parseLogBody(String logBody) { try { - return objectMapper.readValue(logBody, Map.class).get(attributeName); + return new ObjectMapper().readValue(logBody, Map.class); } catch (JsonProcessingException e) { - fail("Invalid JSON", e); - return null; + fail("Invalid JSON: " + logBody, e); + return Map.of(); } } @@ -134,35 +137,35 @@ void thenLogWithAnonymousUserIdAndAuthOk() { var logRecord = assertOneLogRecordExported("/testservicebp/api/v1/200"); @SuppressWarnings("null") - var logBody = logRecord.getBodyValue().asString(); + Map logBody = parseLogBody(logRecord.getBodyValue().asString()); // Full attribute set verification - assertEquals("GET", getAttribute(logBody, "http.request.method"), + assertEquals("GET", logBody.get("http.request.method"), "http.request.method should be GET"); - assertEquals("https", getAttribute(logBody, "url.scheme"), + assertEquals("https", logBody.get("url.scheme"), "url.scheme should be https"); - assertEquals("/testservicebp/api/v1/200", getAttribute(logBody, "url.path"), + assertEquals("/testservicebp/api/v1/200", logBody.get("url.path"), "url.path should match request path"); - assertEquals("testservicebp", getAttribute(logBody, "service.id"), + assertEquals("testservicebp", logBody.get("service.id"), "service.id should be testservicebp"); assertEquals("localhost:testservicebp:" + mockServiceBypass.getPort(), - getAttribute(logBody, "service.instance.id"), + logBody.get("service.instance.id"), "service.instance.id should match mock service"); - assertEquals("bypass", getAttribute(logBody, "auth.service.auth.method"), + assertEquals("bypass", logBody.get("auth.service.auth.method"), "auth.service.auth.method should be bypass"); - assertEquals("200", getAttribute(logBody, "service.response_code"), + assertEquals("200", logBody.get("service.response_code"), "service.response_code should be 200"); // NEW behavior: anonymous user ID and OK auth status - assertEquals("anonymous", getAttribute(logBody, "user.id"), + assertEquals("anonymous", logBody.get("user.id"), "user.id should be 'anonymous' for bypass routes"); - assertEquals("OK", getAttribute(logBody, "auth.status"), + assertEquals("OK", logBody.get("auth.status"), "auth.status should be 'OK' for bypass routes"); // Error attributes should NOT be present for successful bypass - assertNull(getAttribute(logBody, "auth.error.type"), + assertNull(logBody.get("auth.error.type"), "auth.error.type should be null for successful bypass"); - assertNull(getAttribute(logBody, "auth.error.message"), + assertNull(logBody.get("auth.error.message"), "auth.error.message should be null for successful bypass"); } @@ -175,29 +178,29 @@ void thenLogWithErrorAttributesOnRoutingError() { var logRecord = assertOneLogRecordExported("/testservicebp/api/v1/500"); @SuppressWarnings("null") - var logBody = logRecord.getBodyValue().asString(); + Map logBody = parseLogBody(logRecord.getBodyValue().asString()); // Standard attributes still present - assertEquals("GET", getAttribute(logBody, "http.request.method")); - assertEquals("https", getAttribute(logBody, "url.scheme")); - assertEquals("/testservicebp/api/v1/500", getAttribute(logBody, "url.path")); - assertEquals("testservicebp", getAttribute(logBody, "service.id")); + assertEquals("GET", logBody.get("http.request.method")); + assertEquals("https", logBody.get("url.scheme")); + assertEquals("/testservicebp/api/v1/500", logBody.get("url.path")); + assertEquals("testservicebp", logBody.get("service.id")); assertEquals("localhost:testservicebp:" + mockServiceBypass.getPort(), - getAttribute(logBody, "service.instance.id")); - assertEquals("bypass", getAttribute(logBody, "auth.service.auth.method")); - assertEquals("500", getAttribute(logBody, "service.response_code"), + logBody.get("service.instance.id")); + assertEquals("bypass", logBody.get("auth.service.auth.method")); + assertEquals("500", logBody.get("service.response_code"), "service.response_code should be 500 for error endpoint"); // Anonymous user ID and OK auth status still apply (bypass auth succeeded) - assertEquals("anonymous", getAttribute(logBody, "user.id"), + assertEquals("anonymous", logBody.get("user.id"), "user.id should be 'anonymous' for bypass routes even on error"); - assertEquals("OK", getAttribute(logBody, "auth.status"), + assertEquals("OK", logBody.get("auth.status"), "auth.status should be 'OK' for bypass routes even on error"); // Error attributes: service error but auth succeeded, so no auth.error attributes - assertNull(getAttribute(logBody, "auth.error.type"), + assertNull(logBody.get("auth.error.type"), "auth.error.type should be null — bypass auth always succeeds"); - assertNull(getAttribute(logBody, "auth.error.message"), + assertNull(logBody.get("auth.error.message"), "auth.error.message should be null — bypass auth always succeeds"); } }