From 6c81106cb75faf2bd30eebd5232fbc9469eff6ab Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Thu, 5 Feb 2026 15:31:29 +0100 Subject: [PATCH 01/13] Fix client credentials grant token invalidation - Fixes gh-1335 Signed-off-by: Daniel Garnier-Moiroux --- .../reactor/tokenprovider/AbstractUaaTokenProvider.java | 2 +- .../tokenprovider/_ClientCredentialsGrantTokenProvider.java | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/AbstractUaaTokenProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/AbstractUaaTokenProvider.java index 6cfc85a486..10e34a4b0b 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/AbstractUaaTokenProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/AbstractUaaTokenProvider.java @@ -74,7 +74,7 @@ public abstract class AbstractUaaTokenProvider implements TokenProvider { private static final ZoneId UTC = ZoneId.of("UTC"); - private final ConcurrentMap> accessTokens = + protected final ConcurrentMap> accessTokens = new ConcurrentHashMap<>(1); private final ConcurrentMap refreshTokenStreams = diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_ClientCredentialsGrantTokenProvider.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_ClientCredentialsGrantTokenProvider.java index bd7a7061db..2bbbbaa101 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_ClientCredentialsGrantTokenProvider.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/_ClientCredentialsGrantTokenProvider.java @@ -16,6 +16,7 @@ package org.cloudfoundry.reactor.tokenprovider; +import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.immutables.value.Value; import reactor.netty.http.client.HttpClientForm; @@ -36,4 +37,9 @@ void tokenRequestTransformer(HttpClientRequest request, HttpClientForm form) { .attr("response_type", "token"); } + @Override + public void invalidate(ConnectionContext connectionContext) { + this.accessTokens.remove(connectionContext); + } + } From 082ff9022689a542d2c00971c408998c388fe290 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Fri, 6 Feb 2026 09:48:44 +0100 Subject: [PATCH 02/13] v5.17.0.BUILD-SNAPSHOT Development --- cloudfoundry-client-reactor/pom.xml | 2 +- cloudfoundry-client/pom.xml | 2 +- cloudfoundry-operations/pom.xml | 2 +- cloudfoundry-util/pom.xml | 2 +- integration-test/pom.xml | 2 +- pom.xml | 2 +- test-log-cache/pom.xml | 2 +- test-service-broker/pom.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cloudfoundry-client-reactor/pom.xml b/cloudfoundry-client-reactor/pom.xml index 10962f393a..ca35551000 100644 --- a/cloudfoundry-client-reactor/pom.xml +++ b/cloudfoundry-client-reactor/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.15.0.BUILD-SNAPSHOT + 5.17.0.BUILD-SNAPSHOT cloudfoundry-client-reactor diff --git a/cloudfoundry-client/pom.xml b/cloudfoundry-client/pom.xml index 5828f8c589..056181d1de 100644 --- a/cloudfoundry-client/pom.xml +++ b/cloudfoundry-client/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.15.0.BUILD-SNAPSHOT + 5.17.0.BUILD-SNAPSHOT cloudfoundry-client diff --git a/cloudfoundry-operations/pom.xml b/cloudfoundry-operations/pom.xml index 5ab004f136..7ae21357ba 100644 --- a/cloudfoundry-operations/pom.xml +++ b/cloudfoundry-operations/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.15.0.BUILD-SNAPSHOT + 5.17.0.BUILD-SNAPSHOT cloudfoundry-operations diff --git a/cloudfoundry-util/pom.xml b/cloudfoundry-util/pom.xml index 749f52e746..b4450fd0ed 100644 --- a/cloudfoundry-util/pom.xml +++ b/cloudfoundry-util/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.15.0.BUILD-SNAPSHOT + 5.17.0.BUILD-SNAPSHOT cloudfoundry-util diff --git a/integration-test/pom.xml b/integration-test/pom.xml index ca0b9009f1..6b04dd88d3 100644 --- a/integration-test/pom.xml +++ b/integration-test/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.15.0.BUILD-SNAPSHOT + 5.17.0.BUILD-SNAPSHOT integration-test diff --git a/pom.xml b/pom.xml index e8bd2cb83a..f8d9fd1d24 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ cloudfoundry-java-client Cloud Foundry Java Client Parent A Java language binding for interacting with a Cloud Foundry instance - 5.15.0.BUILD-SNAPSHOT + 5.17.0.BUILD-SNAPSHOT pom https://github.com/cloudfoundry/cf-java-client diff --git a/test-log-cache/pom.xml b/test-log-cache/pom.xml index c872e9a072..fc197c23af 100644 --- a/test-log-cache/pom.xml +++ b/test-log-cache/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.15.0.BUILD-SNAPSHOT + 5.17.0.BUILD-SNAPSHOT test-log-cache diff --git a/test-service-broker/pom.xml b/test-service-broker/pom.xml index 7eaf5c5940..13174cd836 100644 --- a/test-service-broker/pom.xml +++ b/test-service-broker/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.15.0.BUILD-SNAPSHOT + 5.17.0.BUILD-SNAPSHOT test-service-broker From 77fb427d36ef2251d5653d747081770cef8cf16c Mon Sep 17 00:00:00 2001 From: Joris Baum Date: Mon, 23 Mar 2026 15:37:10 +0100 Subject: [PATCH 03/13] Fix test-service-broker crash on JDK 17 by adding --add-opens flags The test-service-broker.jar (Spring Boot 1.5.16 / Spring Framework 4.3.19) uses CGLIB proxying which requires reflective access to java.lang.ClassLoader.defineClass(). JDK 17's module system blocks this, causing the app to crash on startup with InaccessibleObjectException and all ApplicationsTest methods to fail with DelayTimeoutException during serviceBrokerId bean creation. --- .../src/test/java/org/cloudfoundry/ServiceBrokerUtils.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/integration-test/src/test/java/org/cloudfoundry/ServiceBrokerUtils.java b/integration-test/src/test/java/org/cloudfoundry/ServiceBrokerUtils.java index e3677bd7ba..f4766314f3 100644 --- a/integration-test/src/test/java/org/cloudfoundry/ServiceBrokerUtils.java +++ b/integration-test/src/test/java/org/cloudfoundry/ServiceBrokerUtils.java @@ -145,6 +145,10 @@ public static Mono pushServiceBrokerApplic Map env = new HashMap<>(); env.put("SERVICE_NAME", serviceName); env.put("PLAN_NAME", planName); + env.put( + "JAVA_OPTS", + "--add-opens java.base/java.lang=ALL-UNNAMED" + + " --add-opens java.base/java.io=ALL-UNNAMED"); return ApplicationUtils.pushApplication( cloudFoundryClient, From 415f07c63c4a8ebc2b33e62e06583df8266978f5 Mon Sep 17 00:00:00 2001 From: Joris Baum Date: Tue, 17 Feb 2026 17:20:47 +0100 Subject: [PATCH 04/13] Support JWT GrantType --- .../reactor/uaa/clients/ReactorClientsTest.java | 2 ++ .../resources/fixtures/uaa/clients/GET_{id}_response.json | 3 ++- .../main/java/org/cloudfoundry/uaa/tokens/GrantType.java | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/clients/ReactorClientsTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/clients/ReactorClientsTest.java index c36ac4f443..a9d730008a 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/clients/ReactorClientsTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/clients/ReactorClientsTest.java @@ -24,6 +24,7 @@ import static io.netty.handler.codec.http.HttpResponseStatus.OK; import static org.cloudfoundry.uaa.tokens.GrantType.AUTHORIZATION_CODE; import static org.cloudfoundry.uaa.tokens.GrantType.CLIENT_CREDENTIALS; +import static org.cloudfoundry.uaa.tokens.GrantType.JWT_BEARER; import static org.cloudfoundry.uaa.tokens.GrantType.REFRESH_TOKEN; import java.time.Duration; @@ -620,6 +621,7 @@ void get() { .allowedProviders("uaa", "ldap", "my-saml-provider") .authorities("clients.read", "clients.write") .authorizedGrantType(CLIENT_CREDENTIALS) + .authorizedGrantType(JWT_BEARER) .autoApprove("true") .clientId("4Z3t1r") .lastModified(1468364445592L) diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/GET_{id}_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/GET_{id}_response.json index d8b2187e47..2a05df3d70 100644 --- a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/GET_{id}_response.json +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/clients/GET_{id}_response.json @@ -8,7 +8,8 @@ "none" ], "authorized_grant_types": [ - "client_credentials" + "client_credentials", + "urn:ietf:params:oauth:grant-type:jwt-bearer" ], "redirect_uri": [ "http*://ant.path.wildcard/**/passback/*", diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/tokens/GrantType.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/tokens/GrantType.java index 6dd626c52d..0d84a4b238 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/tokens/GrantType.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/tokens/GrantType.java @@ -39,6 +39,11 @@ public enum GrantType { */ IMPLICIT("implicit"), + /** + * The JWT bearer grant type + */ + JWT_BEARER("urn:ietf:params:oauth:grant-type:jwt-bearer"), + /** * The password grant type */ @@ -68,6 +73,8 @@ public static GrantType from(String s) { return PASSWORD; case "refresh_token": return REFRESH_TOKEN; + case "urn:ietf:params:oauth:grant-type:jwt-bearer": + return JWT_BEARER; default: throw new IllegalArgumentException(String.format("Unknown grant type: %s", s)); } From 19318f8783fee8f4afcf9d5fe39185a8bd167571 Mon Sep 17 00:00:00 2001 From: Joris Baum Date: Tue, 17 Feb 2026 17:21:21 +0100 Subject: [PATCH 05/13] Support default identity provider property --- .../uaa/identityzones/ReactorIdentityZonesTest.java | 2 ++ .../fixtures/uaa/identity-zones/GET_response.json | 1 + .../uaa/identityzones/_IdentityZoneConfiguration.java | 7 +++++++ 3 files changed, 10 insertions(+) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/identityzones/ReactorIdentityZonesTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/identityzones/ReactorIdentityZonesTest.java index 62674816d2..f0a5a60d1f 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/identityzones/ReactorIdentityZonesTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/identityzones/ReactorIdentityZonesTest.java @@ -832,6 +832,8 @@ void list() { + " /passcode)") .build()) .ldapDiscoveryEnabled(false) + .defaultIdentityProvider( + "test-identity-provider") .accountChooserEnabled(false) .build()) .name("The Twiglet Zone") diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/identity-zones/GET_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/identity-zones/GET_response.json index a5980a1815..817cb18927 100644 --- a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/identity-zones/GET_response.json +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/identity-zones/GET_response.json @@ -97,6 +97,7 @@ "text": "One Time Code (Get on at /passcode)" } ], + "defaultIdentityProvider": "test-identity-provider", "idpDiscoveryEnabled": false, "accountChooserEnabled": false }, diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/identityzones/_IdentityZoneConfiguration.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/identityzones/_IdentityZoneConfiguration.java index 0c98a54a24..a3cc988642 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/identityzones/_IdentityZoneConfiguration.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/identityzones/_IdentityZoneConfiguration.java @@ -65,6 +65,13 @@ abstract class _IdentityZoneConfiguration { @Nullable abstract CorsPolicy getCorsPolicy(); + /** + * The default identity provider for this zone + */ + @JsonProperty("defaultIdentityProvider") + @Nullable + abstract String getDefaultIdentityProvider(); + /** * The issuer of this zone */ From 3ef49d65777428248850af31a1460d86eb0b01db Mon Sep 17 00:00:00 2001 From: Joris Baum Date: Mon, 23 Feb 2026 16:19:06 +0100 Subject: [PATCH 06/13] Accept "access_denied: Access Denied" from UAA Some UAA seem to send a slightly different reply. This accepts and tests for both. --- .../src/test/java/org/cloudfoundry/uaa/TokensTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-test/src/test/java/org/cloudfoundry/uaa/TokensTest.java b/integration-test/src/test/java/org/cloudfoundry/uaa/TokensTest.java index 40b02850f0..39d956f801 100644 --- a/integration-test/src/test/java/org/cloudfoundry/uaa/TokensTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/uaa/TokensTest.java @@ -84,7 +84,7 @@ public void checkTokenNotAuthorized() { t -> assertThat(t) .isInstanceOf(UaaException.class) - .hasMessage("access_denied: Access is denied")) + .hasMessageContainingAll("access_denied", "Access")) .verify(Duration.ofMinutes(5)); } From f6f566fec16dc552266c00cb61833bf515830b4f Mon Sep 17 00:00:00 2001 From: Joris Baum Date: Mon, 23 Feb 2026 16:27:49 +0100 Subject: [PATCH 07/13] Support defaultIdpName of newer UAA See https://github.com/cloudfoundry/uaa/blob/284dd502db2316bfbdb05c47d9a6d23e3c82ba6a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java#L277 --- .../serverinformation/ReactorServerInformationTest.java | 1 + .../test/resources/fixtures/uaa/info/GET_response.json | 3 ++- .../uaa/serverinformation/_GetInfoResponse.java | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/serverinformation/ReactorServerInformationTest.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/serverinformation/ReactorServerInformationTest.java index a0e921ef75..e49bb077a8 100644 --- a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/serverinformation/ReactorServerInformationTest.java +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/uaa/serverinformation/ReactorServerInformationTest.java @@ -151,6 +151,7 @@ void getInfo() { .showLoginLinks(true) .timestamp("2017-09-08T23:11:58+0000") .zoneName("uaa") + .defaultIdpName("test-idp-name") .build()) .expectComplete() .verify(Duration.ofSeconds(5)); diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/info/GET_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/info/GET_response.json index 6c6784819c..2d681f7b5f 100644 --- a/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/info/GET_response.json +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/uaa/info/GET_response.json @@ -30,5 +30,6 @@ "One Time Code ( Get one at http://localhost:8080/uaa/passcode )" ] }, - "timestamp": "2017-09-08T23:11:58+0000" + "timestamp": "2017-09-08T23:11:58+0000", + "defaultIdpName": "test-idp-name" } \ No newline at end of file diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/serverinformation/_GetInfoResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/serverinformation/_GetInfoResponse.java index 018661777a..4b571ac709 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/serverinformation/_GetInfoResponse.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/uaa/serverinformation/_GetInfoResponse.java @@ -93,4 +93,12 @@ abstract class _GetInfoResponse { @Nullable abstract String getZoneName(); + /** + * The default identity provider name + */ + @JsonProperty("defaultIdpName") + @Nullable + abstract String getDefaultIdpName(); + + } From d100933b0f0a5bc426ff6e10953c59ebd3b2e684 Mon Sep 17 00:00:00 2001 From: Joris Baum Date: Mon, 23 Feb 2026 16:42:07 +0100 Subject: [PATCH 08/13] Support uaa setups without password reset endpoint Tests login endpoint instead, which should usually be present --- .../java/org/cloudfoundry/uaa/ServerInformationTest.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/integration-test/src/test/java/org/cloudfoundry/uaa/ServerInformationTest.java b/integration-test/src/test/java/org/cloudfoundry/uaa/ServerInformationTest.java index f12fa616dc..fb1298c916 100644 --- a/integration-test/src/test/java/org/cloudfoundry/uaa/ServerInformationTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/uaa/ServerInformationTest.java @@ -95,13 +95,17 @@ public void getInfo() { this.uaaClient .serverInformation() .getInfo(GetInfoRequest.builder().build()) - .map(response -> response.getLinks().getPassword()) + .map(response -> response.getLinks().getLogin()) .as(StepVerifier::create) - .consumeNextWith(endsWithExpectation("password")) + .consumeNextWith(containsExpectation("login")) .expectComplete() .verify(Duration.ofMinutes(5)); } + private static Consumer containsExpectation(String substring) { + return actual -> assertThat(actual).contains(substring); + } + private static Consumer endsWithExpectation(String suffix) { return actual -> assertThat(actual).endsWith(suffix); } From 21d6d25c57de163ae239381fa4fa08063ccbd836 Mon Sep 17 00:00:00 2001 From: Joris Baum Date: Fri, 27 Mar 2026 18:16:49 +0100 Subject: [PATCH 09/13] feat: Replace deprecated Doppler recentLogs with Log Cache client (#1348) See gh-1237, gh-1348 --- README.md | 41 ++++++++ .../cloudfoundry/doppler/DopplerClient.java | 8 +- .../_DefaultCloudFoundryOperations.java | 16 ++- .../operations/applications/Applications.java | 5 +- .../applications/DefaultApplications.java | 97 +++++++++++++++---- .../operations/AbstractOperationsTest.java | 3 + .../applications/DefaultApplicationsTest.java | 87 +++++++++++++++-- .../IntegrationTestConfiguration.java | 3 + .../operations/ApplicationsTest.java | 56 +---------- 9 files changed, 234 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index ffed2f36f4..058431f921 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,44 @@ The `cf-java-client` project is a Java language binding for interacting with a C ## Versions The Cloud Foundry Java Client has two active versions. The `5.x` line is compatible with Spring Boot `2.4.x - 2.6.x` just to manage its dependencies, while the `4.x` line uses Spring Boot `2.3.x`. +## Deprecations + +### `DopplerClient.recentLogs()` — Recent Logs via Doppler + +> [!WARNING] +> **Deprecated since cf-java-client `5.17.x`** +> +> The `DopplerClient.recentLogs()` endpoint (and the related `RecentLogsRequest` / `LogMessage` types from the `org.cloudfoundry.doppler` package) are **deprecated** and will be removed in a future release. +> +> This API relies on the [Loggregator][loggregator] Doppler/Traffic Controller endpoint `/apps/{id}/recentlogs`, which was removed in **Loggregator ≥ 107.0**. +> The affected platform versions are: +> +> | Platform | Last version with Doppler recent-logs support | +> | -------- | --------------------------------------------- | +> | CF Deployment (CFD) | `< 24.3` | +> | Tanzu Application Service (TAS) | `< 4.0` | +> +> **Migration:** Replace any call to `DopplerClient.recentLogs()` with [`LogCacheClient.read()`][log-cache-api] (available via `org.cloudfoundry.logcache.v1.LogCacheClient`). +> +> ```java +> // Before (deprecated) +> dopplerClient.recentLogs(RecentLogsRequest.builder() +> .applicationId(appId) +> .build()); +> +> // After +> logCacheClient.read(ReadRequest.builder() +> .sourceId(appId) +> .envelopeTypes(EnvelopeType.LOG) +> .build()); +> ``` + +> [!NOTE] +> **Operations API users:** `Applications.logs(ApplicationLogsRequest)` now uses Log Cache under the hood for recent logs (the default). No migration is needed at the Operations layer. + +[loggregator]: https://github.com/cloudfoundry/loggregator +[log-cache-api]: https://github.com/cloudfoundry/log-cache + ## Dependencies Most projects will need two dependencies; the Operations API and an implementation of the Client API. For Maven, the dependencies would be defined like this: @@ -76,6 +114,9 @@ Both the `cloudfoundry-operations` and `cloudfoundry-client` projects follow a [ ### `CloudFoundryClient`, `DopplerClient`, `UaaClient` Builders +> [!NOTE] +> **`DopplerClient` — partial deprecation:** The `recentLogs()` method on `DopplerClient` is deprecated and only works against Loggregator \< 107.0 (CFD \< 24.3 / TAS \< 4.0). See the [Deprecations](#deprecations) section above for the migration path to `LogCacheClient`. + The lowest-level building blocks of the API are `ConnectionContext` and `TokenProvider`. These types are intended to be shared between instances of the clients, and come with out of the box implementations. To instantiate them, you configure them with builders: ```java diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/doppler/DopplerClient.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/doppler/DopplerClient.java index a9c03441cf..3d61922734 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/doppler/DopplerClient.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/doppler/DopplerClient.java @@ -42,9 +42,15 @@ public interface DopplerClient { /** * Makes the Recent Logs request * + * @deprecated Use {@link org.cloudfoundry.logcache.v1.LogCacheClient#read(org.cloudfoundry.logcache.v1.ReadRequest)} instead. + * Only works with {@code Loggregator < 107.0}, shipped in {@code CFD < 24.3} and {@code TAS < 4.0}. * @param request the Recent Logs request - * @return the events from the recent logs + * @return a flux of events from the recent logs + * @see Loggregator + * @see Log Cache + * @see org.cloudfoundry.logcache.v1.LogCacheClient#read(org.cloudfoundry.logcache.v1.ReadRequest) */ + @Deprecated Flux recentLogs(RecentLogsRequest request); /** diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/_DefaultCloudFoundryOperations.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/_DefaultCloudFoundryOperations.java index 299b4bf5e4..62e442a53d 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/_DefaultCloudFoundryOperations.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/_DefaultCloudFoundryOperations.java @@ -23,6 +23,7 @@ import org.cloudfoundry.client.v3.spaces.ListSpacesRequest; import org.cloudfoundry.client.v3.spaces.SpaceResource; import org.cloudfoundry.doppler.DopplerClient; +import org.cloudfoundry.logcache.v1.LogCacheClient; import org.cloudfoundry.networking.NetworkingClient; import org.cloudfoundry.operations.advanced.Advanced; import org.cloudfoundry.operations.advanced.DefaultAdvanced; @@ -79,7 +80,7 @@ public Advanced advanced() { @Override @Value.Derived public Applications applications() { - return new DefaultApplications(getCloudFoundryClientPublisher(), getDopplerClientPublisher(), getSpaceId()); + return new DefaultApplications(getCloudFoundryClientPublisher(), getDopplerClientPublisher(), getLogCacheClientPublisher(), getSpaceId()); } @Override @@ -185,6 +186,19 @@ Mono getDopplerClientPublisher() { .orElse(Mono.error(new IllegalStateException("DopplerClient must be set"))); } + /** + * The {@link LogCacheClient} to use for operations functionality + */ + @Nullable + abstract LogCacheClient getLogCacheClient(); + + @Value.Derived + Mono getLogCacheClientPublisher() { + return Optional.ofNullable(getLogCacheClient()) + .map(Mono::just) + .orElse(Mono.error(new IllegalStateException("LogCacheClient must be set"))); + } + /** * The {@link NetworkingClient} to use for operations functionality */ diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/Applications.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/Applications.java index 5196fef6c8..56aba6af64 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/Applications.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/Applications.java @@ -128,8 +128,9 @@ public interface Applications { /** * List the applications logs. - * Only works with {@code Loggregator < 107.0}, shipped in {@code CFD < 24.3} - * and {@code TAS < 4.0}. + * Uses Log Cache under the hood when {@link ApplicationLogsRequest#getRecent()} is {@code true}. + * Log streaming still uses Doppler, which is not available in CF deployments following + * shared-nothing architecture. * * @param request the application logs request * @return the applications logs diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java index e51ddbb472..bb492fffe7 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java @@ -154,6 +154,9 @@ import org.cloudfoundry.doppler.LogMessage; import org.cloudfoundry.doppler.RecentLogsRequest; import org.cloudfoundry.doppler.StreamRequest; +import org.cloudfoundry.logcache.v1.EnvelopeBatch; +import org.cloudfoundry.logcache.v1.LogCacheClient; +import org.cloudfoundry.logcache.v1.ReadRequest; import org.cloudfoundry.operations.util.OperationsLogging; import org.cloudfoundry.util.DateUtils; import org.cloudfoundry.util.DelayTimeoutException; @@ -200,6 +203,10 @@ public final class DefaultApplications implements Applications { private static final Comparator LOG_MESSAGE_COMPARATOR = Comparator.comparing(LogMessage::getTimestamp); + private static final Comparator + LOG_MESSAGE_COMPARATOR_LOG_CACHE = + Comparator.comparing(org.cloudfoundry.logcache.v1.Envelope::getTimestamp); + private static final Duration LOG_MESSAGE_TIMESPAN = Duration.ofMillis(500); private static final int MAX_NUMBER_OF_RECENT_EVENTS = 50; @@ -214,6 +221,8 @@ public final class DefaultApplications implements Applications { private final Mono dopplerClient; + private final Mono logCacheClient; + private final RandomWords randomWords; private final Mono spaceId; @@ -221,17 +230,20 @@ public final class DefaultApplications implements Applications { public DefaultApplications( Mono cloudFoundryClient, Mono dopplerClient, + Mono logCacheClient, Mono spaceId) { - this(cloudFoundryClient, dopplerClient, new WordListRandomWords(), spaceId); + this(cloudFoundryClient, dopplerClient, logCacheClient, new WordListRandomWords(), spaceId); } DefaultApplications( Mono cloudFoundryClient, Mono dopplerClient, + Mono logCacheClient, RandomWords randomWords, Mono spaceId) { this.cloudFoundryClient = cloudFoundryClient; this.dopplerClient = dopplerClient; + this.logCacheClient = logCacheClient; this.randomWords = randomWords; this.spaceId = spaceId; } @@ -529,6 +541,7 @@ public Flux listTasks(ListApplicationTasksRequest request) { .checkpoint(); } + @Deprecated @Override public Flux logs(LogsRequest request) { return Mono.zip(this.cloudFoundryClient, this.spaceId) @@ -546,22 +559,23 @@ public Flux logs(LogsRequest request) { @Override public Flux logs(ApplicationLogsRequest request) { - return logs(LogsRequest.builder() - .name(request.getName()) - .recent(request.getRecent()) - .build()) - .map( - logMessage -> - ApplicationLog.builder() - .sourceId(logMessage.getApplicationId()) - .sourceType(logMessage.getSourceType()) - .instanceId(logMessage.getSourceInstance()) - .message(logMessage.getMessage()) - .timestamp(logMessage.getTimestamp()) - .logType( - ApplicationLogType.from( - logMessage.getMessageType().name())) - .build()); + if (request.getRecent() == null || request.getRecent()) { + return Mono.zip(this.cloudFoundryClient, this.spaceId) + .flatMap( + function( + (cloudFoundryClient, spaceId) -> + getApplicationId( + cloudFoundryClient, + request.getName(), + spaceId))) + .flatMapMany( + applicationId -> getLogsLogCache(this.logCacheClient, applicationId)) + .transform(OperationsLogging.log("Get Application Logs")) + .checkpoint(); + } else { + return logs(LogsRequest.builder().name(request.getName()).recent(false).build()) + .map(DefaultApplications::toApplicationLog); + } } @Override @@ -673,7 +687,6 @@ public Mono pushManifestV3(PushManifestV3Request request) { } catch (IOException e) { throw new RuntimeException("Could not serialize manifest", e); } - return Mono.zip(this.cloudFoundryClient, this.spaceId) .flatMap( function( @@ -1617,6 +1630,32 @@ private static Flux getLogs( } } + private static Flux getLogsLogCache( + Mono logCacheClient, String applicationId) { + return requestLogsRecentLogCache(logCacheClient, applicationId) + .filter(e -> e.getLog() != null) + .sort(LOG_MESSAGE_COMPARATOR_LOG_CACHE) + .map( + envelope -> + ApplicationLog.builder() + .sourceId( + Optional.ofNullable(envelope.getSourceId()) + .orElse("")) + .sourceType( + envelope.getTags().getOrDefault("source_type", "")) + .instanceId( + Optional.ofNullable(envelope.getInstanceId()) + .orElse("")) + .message(envelope.getLog().getPayloadAsText()) + .timestamp( + Optional.ofNullable(envelope.getTimestamp()) + .orElse(0L)) + .logType( + ApplicationLogType.from( + envelope.getLog().getType().name())) + .build()); + } + @SuppressWarnings("unchecked") private static Map getMetadataRequest(EventEntity entity) { Map> metadata = @@ -2501,6 +2540,7 @@ private static Flux requestListTasks( .build())); } + @Deprecated private static Flux requestLogsRecent( Mono dopplerClient, String applicationId) { return dopplerClient.flatMapMany( @@ -2509,6 +2549,16 @@ private static Flux requestLogsRecent( RecentLogsRequest.builder().applicationId(applicationId).build())); } + private static Flux requestLogsRecentLogCache( + Mono logCacheClient, String applicationId) { + return logCacheClient + .flatMap( + client -> + client.read(ReadRequest.builder().sourceId(applicationId).build())) + .flatMap(response -> Mono.justOrEmpty(response.getEnvelopes())) + .flatMapIterable(EnvelopeBatch::getBatch); + } + private static Flux requestLogsStream( Mono dopplerClient, String applicationId) { return dopplerClient.flatMapMany( @@ -2914,6 +2964,17 @@ private static Mono stopApplicationIfNotStopped( : Mono.just(resource); } + private static ApplicationLog toApplicationLog(LogMessage logMessage) { + return ApplicationLog.builder() + .sourceId(logMessage.getApplicationId()) + .sourceType(logMessage.getSourceType()) + .instanceId(logMessage.getSourceInstance()) + .message(logMessage.getMessage()) + .timestamp(logMessage.getTimestamp()) + .logType(ApplicationLogType.from(logMessage.getMessageType().name())) + .build(); + } + private static ApplicationDetail toApplicationDetail( List buildpacks, SummaryApplicationResponse summaryApplicationResponse, diff --git a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/AbstractOperationsTest.java b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/AbstractOperationsTest.java index d864ed497e..5a0854f48b 100644 --- a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/AbstractOperationsTest.java +++ b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/AbstractOperationsTest.java @@ -51,6 +51,7 @@ import org.cloudfoundry.client.v3.spaces.SpacesV3; import org.cloudfoundry.client.v3.tasks.Tasks; import org.cloudfoundry.doppler.DopplerClient; +import org.cloudfoundry.logcache.v1.LogCacheClient; import org.cloudfoundry.routing.RoutingClient; import org.cloudfoundry.routing.v1.routergroups.RouterGroups; import org.cloudfoundry.uaa.UaaClient; @@ -101,6 +102,8 @@ public abstract class AbstractOperationsTest { protected final DopplerClient dopplerClient = mock(DopplerClient.class, RETURNS_SMART_NULLS); + protected final LogCacheClient logCacheClient = mock(LogCacheClient.class, RETURNS_SMART_NULLS); + protected final Events events = mock(Events.class, RETURNS_SMART_NULLS); protected final FeatureFlags featureFlags = mock(FeatureFlags.class, RETURNS_SMART_NULLS); diff --git a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java index cdc9619d2d..5d21f8e584 100644 --- a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java +++ b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java @@ -25,9 +25,11 @@ import static org.mockito.Mockito.when; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.Duration; import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -139,11 +141,17 @@ import org.cloudfoundry.client.v3.tasks.CreateTaskResponse; import org.cloudfoundry.client.v3.tasks.TaskResource; import org.cloudfoundry.doppler.DopplerClient; -import org.cloudfoundry.doppler.Envelope; import org.cloudfoundry.doppler.EventType; import org.cloudfoundry.doppler.LogMessage; import org.cloudfoundry.doppler.RecentLogsRequest; import org.cloudfoundry.doppler.StreamRequest; +import org.cloudfoundry.logcache.v1.Envelope; +import org.cloudfoundry.logcache.v1.EnvelopeBatch; +import org.cloudfoundry.logcache.v1.Log; +import org.cloudfoundry.logcache.v1.LogCacheClient; +import org.cloudfoundry.logcache.v1.LogType; +import org.cloudfoundry.logcache.v1.ReadRequest; +import org.cloudfoundry.logcache.v1.ReadResponse; import org.cloudfoundry.operations.AbstractOperationsTest; import org.cloudfoundry.util.DateUtils; import org.cloudfoundry.util.FluentMap; @@ -163,6 +171,7 @@ final class DefaultApplicationsTest extends AbstractOperationsTest { new DefaultApplications( Mono.just(this.cloudFoundryClient), Mono.just(this.dopplerClient), + Mono.just(this.logCacheClient), this.randomWords, Mono.just(TEST_SPACE_ID)); @@ -1306,8 +1315,9 @@ void listTasks() { .verify(Duration.ofSeconds(5)); } + @SuppressWarnings("deprecation") @Test - void logs() { + void logsDoppler() { requestApplications( this.cloudFoundryClient, "test-application-name", @@ -1318,13 +1328,14 @@ void logs() { this.applications .logs(LogsRequest.builder().name("test-application-name").recent(false).build()) .as(StepVerifier::create) - .expectNext(fill(LogMessage.builder(), "log-message-").build()) + .expectNextMatches(log -> log.getMessage().equals("test-log-message-message")) .expectComplete() .verify(Duration.ofSeconds(5)); } + @SuppressWarnings("deprecation") @Test - void logsNoApp() { + void logsNoAppDoppler() { requestApplicationsEmpty(this.cloudFoundryClient, "test-application-name", TEST_SPACE_ID); this.applications @@ -1339,8 +1350,9 @@ void logsNoApp() { .verify(Duration.ofSeconds(5)); } + @SuppressWarnings("deprecation") @Test - void logsRecent() { + void logsRecentDoppler() { requestApplications( this.cloudFoundryClient, "test-application-name", @@ -1351,13 +1363,38 @@ void logsRecent() { this.applications .logs(LogsRequest.builder().name("test-application-name").recent(true).build()) .as(StepVerifier::create) - .expectNext(fill(LogMessage.builder(), "log-message-").build()) + .expectNextMatches(log -> log.getMessage().equals("test-log-message-message")) .expectComplete() .verify(Duration.ofSeconds(5)); } @Test - void logsRecentNotSet() { + void logsLogCache() { + requestApplications( + this.cloudFoundryClient, + "test-application-name", + TEST_SPACE_ID, + "test-metadata-id"); + requestLogsRecentLogCache(this.logCacheClient, "test-metadata-id"); + + this.applications + .logs(ApplicationLogsRequest.builder().name("test-application-name").build()) + .as(StepVerifier::create) + .expectNextMatches( + log -> + log.getMessage().equals("test-payload") + && log.getLogType() == ApplicationLogType.OUT + && log.getSourceId().equals("test-sourceId") + && log.getInstanceId().equals("test-instanceId") + && log.getSourceType().equals("APP/PROC/WEB") + && log.getTimestamp() == 1L) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @SuppressWarnings("deprecation") + @Test + void logsRecentNotSetDoppler() { requestApplications( this.cloudFoundryClient, "test-application-name", @@ -5317,12 +5354,13 @@ private static void requestListTasksEmpty( .build())); } + @SuppressWarnings("deprecation") private static void requestLogsRecent(DopplerClient dopplerClient, String applicationId) { when(dopplerClient.recentLogs( RecentLogsRequest.builder().applicationId(applicationId).build())) .thenReturn( Flux.just( - Envelope.builder() + org.cloudfoundry.doppler.Envelope.builder() .eventType(EventType.LOG_MESSAGE) .logMessage( fill(LogMessage.builder(), "log-message-").build()) @@ -5330,11 +5368,42 @@ private static void requestLogsRecent(DopplerClient dopplerClient, String applic .build())); } + private static void requestLogsRecentLogCache(LogCacheClient logCacheClient, String sourceId) { + String base64Payload = + Base64.getEncoder().encodeToString("test-payload".getBytes(StandardCharsets.UTF_8)); + when(logCacheClient.read(ReadRequest.builder().sourceId(sourceId).build())) + .thenReturn( + Mono.just( + fill(ReadResponse.builder()) + .envelopes( + fill(EnvelopeBatch.builder()) + .batch( + Arrays.asList( + fill(Envelope.builder()) + .tags( + Collections + .singletonMap( + "source_type", + "APP/PROC/WEB")) + .log( + Log + .builder() + .payload( + base64Payload) + .type( + LogType + .OUT) + .build()) + .build())) + .build()) + .build())); + } + private static void requestLogsStream(DopplerClient dopplerClient, String applicationId) { when(dopplerClient.stream(StreamRequest.builder().applicationId(applicationId).build())) .thenReturn( Flux.just( - Envelope.builder() + org.cloudfoundry.doppler.Envelope.builder() .eventType(EventType.LOG_MESSAGE) .logMessage( fill(LogMessage.builder(), "log-message-").build()) diff --git a/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java index c0b3f44b30..da390ac204 100644 --- a/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java +++ b/integration-test/src/test/java/org/cloudfoundry/IntegrationTestConfiguration.java @@ -48,6 +48,7 @@ import org.cloudfoundry.client.v2.stacks.StackResource; import org.cloudfoundry.client.v2.userprovidedserviceinstances.CreateUserProvidedServiceInstanceRequest; import org.cloudfoundry.doppler.DopplerClient; +import org.cloudfoundry.logcache.v1.LogCacheClient; import org.cloudfoundry.logcache.v1.TestLogCacheEndpoints; import org.cloudfoundry.networking.NetworkingClient; import org.cloudfoundry.operations.DefaultCloudFoundryOperations; @@ -266,6 +267,7 @@ ReactorCloudFoundryClient cloudFoundryClient( DefaultCloudFoundryOperations cloudFoundryOperations( CloudFoundryClient cloudFoundryClient, DopplerClient dopplerClient, + LogCacheClient logCacheClient, NetworkingClient networkingClient, RoutingClient routingClient, UaaClient uaaClient, @@ -274,6 +276,7 @@ DefaultCloudFoundryOperations cloudFoundryOperations( return DefaultCloudFoundryOperations.builder() .cloudFoundryClient(cloudFoundryClient) .dopplerClient(dopplerClient) + .logCacheClient(logCacheClient) .networkingClient(networkingClient) .routingClient(routingClient) .uaaClient(uaaClient) diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java index 36e1bd9456..37c701dbbe 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java @@ -30,14 +30,6 @@ import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; import org.cloudfoundry.client.CloudFoundryClient; -import org.cloudfoundry.logcache.v1.Envelope; -import org.cloudfoundry.logcache.v1.EnvelopeBatch; -import org.cloudfoundry.logcache.v1.EnvelopeType; -import org.cloudfoundry.logcache.v1.Log; -import org.cloudfoundry.logcache.v1.LogCacheClient; -import org.cloudfoundry.logcache.v1.LogType; -import org.cloudfoundry.logcache.v1.ReadRequest; -import org.cloudfoundry.logcache.v1.ReadResponse; import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.ApplicationEnvironments; import org.cloudfoundry.operations.applications.ApplicationEvent; @@ -108,7 +100,6 @@ public final class ApplicationsTest extends AbstractIntegrationTest { @Autowired private String serviceName; - @Autowired private LogCacheClient logCacheClient; @Autowired private CloudFoundryClient cloudFoundryClient; // To create a service in #pushBindService, the Service Broker must be installed first. @@ -508,11 +499,13 @@ public void listTasks() throws IOException { } /** - * Doppler was dropped in PCF 4.x in favor of logcache. This test does not work - * on TAS 4.x. + * Exercise the LogCache client via {@code logs(ApplicationLogsRequest)}. + * LogCache has been a default cf-deployment component since v3.0.0 (July 2018), + * with the {@code /api/v1/read} endpoint available since log-cache-release v2.0.0 + * (October 2018). */ @Test - @IfCloudFoundryVersion(lessThan = CloudFoundryVersion.PCF_4_v2) + @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_2_3) public void logs() throws IOException { String applicationName = this.nameFactory.getApplicationName(); @@ -537,45 +530,6 @@ public void logs() throws IOException { .verify(Duration.ofMinutes(5)); } - /** - * Exercise the LogCache client. Serves as a reference for using the logcache client, - * and will help with the transition to the new - * {@link org.cloudfoundry.operations.applications.Applications#logs(ApplicationLogsRequest)}. - */ - @Test - public void logCacheLogs() throws IOException { - String applicationName = this.nameFactory.getApplicationName(); - - createApplication( - this.cloudFoundryOperations, - new ClassPathResource("test-application.zip").getFile().toPath(), - applicationName, - false) - .then( - this.cloudFoundryOperations - .applications() - .get(GetApplicationRequest.builder().name(applicationName).build())) - .map(ApplicationDetail::getId) - .flatMapMany( - appGuid -> - this.logCacheClient.read( - ReadRequest.builder() - .sourceId(appGuid) - .envelopeType(EnvelopeType.LOG) - .limit(1) - .build())) - .map(ReadResponse::getEnvelopes) - .map(EnvelopeBatch::getBatch) - .flatMap(Flux::fromIterable) - .map(Envelope::getLog) - .map(Log::getType) - .next() - .as(StepVerifier::create) - .expectNext(LogType.OUT) - .expectComplete() - .verify(Duration.ofMinutes(5)); - } - @Test public void pushBindServices() throws IOException { String applicationName = this.nameFactory.getApplicationName(); From 05bf0e390e669e16dd6ed22849f83e79ec6bdf01 Mon Sep 17 00:00:00 2001 From: Joris Baum Date: Tue, 14 Apr 2026 16:08:39 +0200 Subject: [PATCH 10/13] fix: Restore null-recent default to Doppler streaming PR #1348 changed the null-recent behavior from Doppler streaming to Log Cache. This restores the original default so null behaves like false (streaming), matching the prior logs(LogsRequest) contract. --- .../operations/applications/DefaultApplications.java | 2 +- .../operations/applications/DefaultApplicationsTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java index bb492fffe7..c56ee52554 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/DefaultApplications.java @@ -559,7 +559,7 @@ public Flux logs(LogsRequest request) { @Override public Flux logs(ApplicationLogsRequest request) { - if (request.getRecent() == null || request.getRecent()) { + if (request.getRecent() != null && request.getRecent()) { return Mono.zip(this.cloudFoundryClient, this.spaceId) .flatMap( function( diff --git a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java index 5d21f8e584..93f83236aa 100644 --- a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java +++ b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java @@ -1378,7 +1378,7 @@ void logsLogCache() { requestLogsRecentLogCache(this.logCacheClient, "test-metadata-id"); this.applications - .logs(ApplicationLogsRequest.builder().name("test-application-name").build()) + .logs(ApplicationLogsRequest.builder().name("test-application-name").recent(true).build()) .as(StepVerifier::create) .expectNextMatches( log -> From 3e79d083cf3eb5f64e10c8621d72c245e42d0039 Mon Sep 17 00:00:00 2001 From: Joris Baum Date: Wed, 15 Apr 2026 15:08:45 +0200 Subject: [PATCH 11/13] Be more verbose about types returned by read() --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 058431f921..715033c14e 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,21 @@ The Cloud Foundry Java Client has two active versions. The `5.x` line is compati > .envelopeTypes(EnvelopeType.LOG) > .build()); > ``` +> +> The return type and envelope objects differ between the two APIs: +> +> | | Doppler (`org.cloudfoundry.doppler`) | Log Cache (`org.cloudfoundry.logcache.v1`) | +> |---|---|---| +> | **Return type** | `Flux` | `Mono` → unpack via `response.getEnvelopes().getBatch()` | +> | **Log access** | `envelope.getLogMessage()` → `LogMessage` | `envelope.getLog()` → `Log` | +> | **Message text** | `logMessage.getMessage()` | `log.getPayloadAsText()` | +> | **Message type** | `MessageType.OUT` / `ERR` | `LogType.OUT` / `ERR` | +> | **Source metadata** | `logMessage.getSourceType()`, `.getSourceInstance()` | `envelope.getTags().get("source_type")`, `envelope.getInstanceId()` | +> +> See the [`org.cloudfoundry.doppler`][doppler-pkg] and [`org.cloudfoundry.logcache.v1`][logcache-pkg] Javadoc for full type details. + +[doppler-pkg]: https://javadoc.io/doc/org.cloudfoundry/cloudfoundry-client/latest/org/cloudfoundry/doppler/package-summary.html +[logcache-pkg]: https://javadoc.io/doc/org.cloudfoundry/cloudfoundry-client/latest/org/cloudfoundry/logcache/v1/package-summary.html > [!NOTE] > **Operations API users:** `Applications.logs(ApplicationLogsRequest)` now uses Log Cache under the hood for recent logs (the default). No migration is needed at the Operations layer. From 259860e0a6548d6d8b967b7f96bd8097ab3cfbb2 Mon Sep 17 00:00:00 2001 From: Joris Baum Date: Thu, 23 Apr 2026 12:55:14 +0200 Subject: [PATCH 12/13] Revert "v5.17.0.BUILD-SNAPSHOT Development" This reverts commit 082ff9022689a542d2c00971c408998c388fe290. --- cloudfoundry-client-reactor/pom.xml | 2 +- cloudfoundry-client/pom.xml | 2 +- cloudfoundry-operations/pom.xml | 2 +- cloudfoundry-util/pom.xml | 2 +- integration-test/pom.xml | 2 +- pom.xml | 2 +- test-log-cache/pom.xml | 2 +- test-service-broker/pom.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cloudfoundry-client-reactor/pom.xml b/cloudfoundry-client-reactor/pom.xml index ca35551000..10962f393a 100644 --- a/cloudfoundry-client-reactor/pom.xml +++ b/cloudfoundry-client-reactor/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.17.0.BUILD-SNAPSHOT + 5.15.0.BUILD-SNAPSHOT cloudfoundry-client-reactor diff --git a/cloudfoundry-client/pom.xml b/cloudfoundry-client/pom.xml index 056181d1de..5828f8c589 100644 --- a/cloudfoundry-client/pom.xml +++ b/cloudfoundry-client/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.17.0.BUILD-SNAPSHOT + 5.15.0.BUILD-SNAPSHOT cloudfoundry-client diff --git a/cloudfoundry-operations/pom.xml b/cloudfoundry-operations/pom.xml index 7ae21357ba..5ab004f136 100644 --- a/cloudfoundry-operations/pom.xml +++ b/cloudfoundry-operations/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.17.0.BUILD-SNAPSHOT + 5.15.0.BUILD-SNAPSHOT cloudfoundry-operations diff --git a/cloudfoundry-util/pom.xml b/cloudfoundry-util/pom.xml index b4450fd0ed..749f52e746 100644 --- a/cloudfoundry-util/pom.xml +++ b/cloudfoundry-util/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.17.0.BUILD-SNAPSHOT + 5.15.0.BUILD-SNAPSHOT cloudfoundry-util diff --git a/integration-test/pom.xml b/integration-test/pom.xml index 6b04dd88d3..ca0b9009f1 100644 --- a/integration-test/pom.xml +++ b/integration-test/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.17.0.BUILD-SNAPSHOT + 5.15.0.BUILD-SNAPSHOT integration-test diff --git a/pom.xml b/pom.xml index f8d9fd1d24..e8bd2cb83a 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ cloudfoundry-java-client Cloud Foundry Java Client Parent A Java language binding for interacting with a Cloud Foundry instance - 5.17.0.BUILD-SNAPSHOT + 5.15.0.BUILD-SNAPSHOT pom https://github.com/cloudfoundry/cf-java-client diff --git a/test-log-cache/pom.xml b/test-log-cache/pom.xml index fc197c23af..c872e9a072 100644 --- a/test-log-cache/pom.xml +++ b/test-log-cache/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.17.0.BUILD-SNAPSHOT + 5.15.0.BUILD-SNAPSHOT test-log-cache diff --git a/test-service-broker/pom.xml b/test-service-broker/pom.xml index 13174cd836..7eaf5c5940 100644 --- a/test-service-broker/pom.xml +++ b/test-service-broker/pom.xml @@ -25,7 +25,7 @@ org.cloudfoundry cloudfoundry-java-client - 5.17.0.BUILD-SNAPSHOT + 5.15.0.BUILD-SNAPSHOT test-service-broker From 4658d06b0b22dd7d7b7da3e4cb657ec70d34af47 Mon Sep 17 00:00:00 2001 From: Joris Baum Date: Thu, 23 Apr 2026 13:14:29 +0200 Subject: [PATCH 13/13] Apply spotless --- .../operations/applications/DefaultApplicationsTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java index 93f83236aa..f137ac2d84 100644 --- a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java +++ b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java @@ -1378,7 +1378,11 @@ void logsLogCache() { requestLogsRecentLogCache(this.logCacheClient, "test-metadata-id"); this.applications - .logs(ApplicationLogsRequest.builder().name("test-application-name").recent(true).build()) + .logs( + ApplicationLogsRequest.builder() + .name("test-application-name") + .recent(true) + .build()) .as(StepVerifier::create) .expectNextMatches( log ->