From ca705fb77d605583dbc2da7b663b8ff17ee5b403 Mon Sep 17 00:00:00 2001 From: Joris Baum Date: Fri, 20 Mar 2026 16:45:06 +0100 Subject: [PATCH 1/2] Migrate logs(ApplicationLogsRequest) from Doppler to LogCache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use LogCacheClient.read() for recent logs (the default), fall back to Doppler streaming when recent is explicitly false. Remove logCacheLogs() integration test — it was a temporary reference for the direct LogCache API, now redundant since logs() exercises the same path. Remove logsRecent() and its helpers likewise. --- .../operations/applications/Applications.java | 15 +-- .../applications/DefaultApplications.java | 89 ++++++++------- .../applications/DefaultApplicationsTest.java | 29 +++-- .../operations/ApplicationsTest.java | 102 ------------------ 4 files changed, 76 insertions(+), 159 deletions(-) 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 53317ccb63..8e77502bb3 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 @@ -17,8 +17,6 @@ package org.cloudfoundry.operations.applications; import org.cloudfoundry.doppler.LogMessage; -import org.cloudfoundry.logcache.v1.Log; -import org.cloudfoundry.logcache.v1.ReadRequest; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -129,18 +127,7 @@ public interface Applications { Flux logs(LogsRequest request); /** - * List the applications logs from logCacheClient. - * If no messages are available, an empty Flux is returned. - * - * @param request the application logs request - * @return the applications logs - */ - Flux logsRecent(ReadRequest request); - - /** - * List the applications logs. - * Only works with {@code Loggregator < 107.0}, shipped in {@code CFD < 24.3} - * and {@code TAS < 4.0}. + * List the applications logs. Uses Log Cache under the hood. * * @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 b247261d10..12a705979b 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 @@ -155,7 +155,6 @@ import org.cloudfoundry.doppler.RecentLogsRequest; import org.cloudfoundry.doppler.StreamRequest; import org.cloudfoundry.logcache.v1.EnvelopeBatch; -import org.cloudfoundry.logcache.v1.Log; import org.cloudfoundry.logcache.v1.LogCacheClient; import org.cloudfoundry.logcache.v1.ReadRequest; import org.cloudfoundry.operations.util.OperationsLogging; @@ -558,31 +557,25 @@ public Flux logs(LogsRequest request) { .checkpoint(); } - @Override - public Flux logsRecent(ReadRequest request) { - return getRecentLogsLogCache(this.logCacheClient, request) - .transform(OperationsLogging.log("Get Application Logs")) - .checkpoint(); - } - @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 (Optional.ofNullable(request.getRecent()).orElse(true)) { + 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 @@ -1637,12 +1630,33 @@ private static Flux getLogs( } } - private static Flux getRecentLogsLogCache( - Mono logCacheClient, ReadRequest readRequest) { - return requestLogsRecentLogCache(logCacheClient, readRequest) + private static Flux getLogsLogCache( + Mono logCacheClient, String sourceId) { + return logCacheClient + .flatMap(client -> client.read(ReadRequest.builder().sourceId(sourceId).build())) + .flatMap(response -> Mono.justOrEmpty(response.getEnvelopes())) .flatMapIterable(EnvelopeBatch::getBatch) + .filter(e -> e.getLog() != null) .sort(LOG_MESSAGE_COMPARATOR_LOG_CACHE) - .mapNotNull(org.cloudfoundry.logcache.v1.Envelope::getLog); + .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") @@ -2538,14 +2552,6 @@ private static Flux requestLogsRecent( RecentLogsRequest.builder().applicationId(applicationId).build())); } - private static Mono requestLogsRecentLogCache( - Mono logCacheClient, ReadRequest readRequest) { - return logCacheClient.flatMap( - client -> - client.read(readRequest) - .flatMap(response -> Mono.justOrEmpty(response.getEnvelopes()))); - } - private static Flux requestLogsStream( Mono dopplerClient, String applicationId) { return dopplerClient.flatMapMany( @@ -2951,6 +2957,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/applications/DefaultApplicationsTest.java b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/DefaultApplicationsTest.java index a5724a5efa..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; @@ -1367,18 +1369,25 @@ void logsRecentDoppler() { } @Test - void logsRecentLogCache() { + void logsLogCache() { requestApplications( this.cloudFoundryClient, "test-application-name", TEST_SPACE_ID, "test-metadata-id"); - requestLogsRecentLogCache(this.logCacheClient, "test-metadata-id", "test-payload"); + requestLogsRecentLogCache(this.logCacheClient, "test-metadata-id"); this.applications - .logsRecent(ReadRequest.builder().sourceId("test-metadata-id").build()) + .logs(ApplicationLogsRequest.builder().name("test-application-name").build()) .as(StepVerifier::create) - .expectNext(fill(Log.builder()).type(LogType.OUT).build()) + .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)); } @@ -5359,8 +5368,9 @@ private static void requestLogsRecent(DopplerClient dopplerClient, String applic .build())); } - private static void requestLogsRecentLogCache( - LogCacheClient logCacheClient, String sourceId, String payload) { + 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( @@ -5370,11 +5380,16 @@ private static void requestLogsRecentLogCache( .batch( Arrays.asList( fill(Envelope.builder()) + .tags( + Collections + .singletonMap( + "source_type", + "APP/PROC/WEB")) .log( Log .builder() .payload( - payload) + base64Payload) .type( LogType .OUT) 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 d5455d5b1e..8d0b81e49f 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java @@ -25,20 +25,11 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.logging.Level; import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CleanupCloudFoundryAfterClass; 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; @@ -87,7 +78,6 @@ import org.cloudfoundry.operations.services.CreateUserProvidedServiceInstanceRequest; import org.cloudfoundry.operations.services.GetServiceInstanceRequest; import org.cloudfoundry.operations.services.ServiceInstance; -import org.cloudfoundry.operations.util.OperationsLogging; import org.cloudfoundry.util.FluentMap; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -95,7 +85,6 @@ import org.springframework.core.io.ClassPathResource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.SignalType; import reactor.test.StepVerifier; @CleanupCloudFoundryAfterClass @@ -111,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. @@ -514,7 +502,6 @@ 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. */ - @Deprecated @Test @IfCloudFoundryVersion(lessThan = CloudFoundryVersion.PCF_4_v2) public void logs() throws IOException { @@ -541,72 +528,6 @@ public void logs() throws IOException { .verify(Duration.ofMinutes(5)); } - @Test - @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_4_v2) - public void logsRecent() throws IOException { - String applicationName = this.nameFactory.getApplicationName(); - Mono applicationGuid = - getAppGuidFromAppName(cloudFoundryOperations, applicationName); - createApplication( - this.cloudFoundryOperations, - new ClassPathResource("test-application.zip").getFile().toPath(), - applicationName, - false) - .then( - applicationGuid - .map(ApplicationsTest::getReadRequest) - .flatMapMany( - readRequest -> - callLogsRecent( - this.cloudFoundryOperations, - readRequest) - .log(null, Level.ALL, SignalType.ON_NEXT)) - .map(ApplicationsTest::checkOneLogEntry) - .then()) - .as(StepVerifier::create) - .expectComplete() - .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(); @@ -2187,27 +2108,4 @@ private static Mono requestSshEnabled( .applications() .sshEnabled(ApplicationSshEnabledRequest.builder().name(applicationName).build()); } - - private static ReadRequest getReadRequest(String applicationId) { - return ReadRequest.builder().sourceId(applicationId).build(); - } - - private static Flux callLogsRecent( - CloudFoundryOperations cloudFoundryOperations, ReadRequest readRequest) { - return cloudFoundryOperations.applications().logsRecent(readRequest); - } - - private static Mono getAppGuidFromAppName( - CloudFoundryOperations cloudFoundryOperations, String applicationName) { - return cloudFoundryOperations - .applications() - .get(GetApplicationRequest.builder().name(applicationName).build()) - .map(ApplicationDetail::getId); - } - - private static Log checkOneLogEntry(Log log) { - OperationsLogging.log("one log entry: " + log.getType() + " " + log.getPayloadAsText()); - assertThat(log.getType()).isIn(LogType.OUT, LogType.ERR); - return log; - } } From b7800f76de9defdad19005da6d9099feb2f350db Mon Sep 17 00:00:00 2001 From: Joris Baum Date: Fri, 20 Mar 2026 16:45:20 +0100 Subject: [PATCH 2/2] Update logs() integration test javadoc and version gate LogCache has been available since cf-deployment v3.0.0 (July 2018). Lower the version gate from PCF_4_v2 to PCF_2_3 and update the javadoc to reflect that the test now exercises the LogCache-backed path. --- .../org/cloudfoundry/operations/ApplicationsTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 8d0b81e49f..37c701dbbe 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java @@ -499,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();