diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationLogType.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationLogType.java new file mode 100644 index 0000000000..1c49f4ba42 --- /dev/null +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationLogType.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.operations.applications; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public enum ApplicationLogType { + /** + * {@code STDERR} + */ + ERR("ERR"), + + /** + * {@code STDOUT} + */ + OUT("OUT"); + + private final String value; + + ApplicationLogType(String value) { + this.value = value; + } + + @JsonCreator + public static ApplicationLogType from(String s) { + switch (s.toLowerCase()) { + case "err": + return ERR; + case "out": + return OUT; + default: + throw new IllegalArgumentException(String.format("Unknown log type: %s", s)); + } + } + + @JsonValue + public String getValue() { + return this.value; + } + + @Override + public String toString() { + return getValue(); + } +} 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 27e6c4ff4b..5196fef6c8 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 @@ -115,13 +115,27 @@ public interface Applications { Flux listTasks(ListApplicationTasksRequest request); /** - * List the applications logs + * List the applications logs. Uses Doppler under the hood. + * Only works with {@code Loggregator < 107.0}, shipped in {@code CFD < 24.3} + * and {@code TAS < 4.0}. * * @param request the application logs request * @return the applications logs + * @deprecated Use {@link #logs(ApplicationLogsRequest)} instead. */ + @Deprecated Flux logs(LogsRequest request); + /** + * List the applications logs. + * Only works with {@code Loggregator < 107.0}, shipped in {@code CFD < 24.3} + * and {@code TAS < 4.0}. + * + * @param request the application logs request + * @return the applications logs + */ + Flux logs(ApplicationLogsRequest request); + /** * Push a specific application * 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 03ddf9527c..d1de916f56 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 @@ -542,6 +542,26 @@ public Flux logs(LogsRequest request) { .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()); + } + @Override @SuppressWarnings("deprecation") public Mono push(PushApplicationRequest request) { diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ApplicationLog.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ApplicationLog.java new file mode 100644 index 0000000000..f8c805db0f --- /dev/null +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ApplicationLog.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.operations.applications; + +import org.immutables.value.Value; + +/** + * Represents an application log. + */ +@Value.Immutable +abstract class _ApplicationLog { + abstract String getSourceId(); + + abstract String getInstanceId(); + + abstract String getSourceType(); + + abstract String getMessage(); + + abstract ApplicationLogType getLogType(); + + abstract Long getTimestamp(); +} diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ApplicationLogsRequest.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ApplicationLogsRequest.java new file mode 100644 index 0000000000..3cef13b20f --- /dev/null +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ApplicationLogsRequest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cloudfoundry.operations.applications; + +import org.cloudfoundry.Nullable; +import org.immutables.value.Value; + +/** + * Represents a request for logs. + */ +@Value.Immutable +abstract class _ApplicationLogsRequest { + + /** + * The name of the application + */ + abstract String getName(); + + /** + * Whether only recent logs should be retrieved + */ + @Nullable + abstract Boolean getRecent(); +} 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 e04c488b0c..45addf4fb2 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java @@ -28,12 +28,21 @@ import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; -import org.cloudfoundry.doppler.LogMessage; -import org.cloudfoundry.doppler.MessageType; +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; import org.cloudfoundry.operations.applications.ApplicationHealthCheck; +import org.cloudfoundry.operations.applications.ApplicationLog; +import org.cloudfoundry.operations.applications.ApplicationLogType; +import org.cloudfoundry.operations.applications.ApplicationLogsRequest; import org.cloudfoundry.operations.applications.ApplicationManifest; import org.cloudfoundry.operations.applications.ApplicationSshEnabledRequest; import org.cloudfoundry.operations.applications.ApplicationSummary; @@ -47,7 +56,6 @@ import org.cloudfoundry.operations.applications.GetApplicationManifestRequest; import org.cloudfoundry.operations.applications.GetApplicationRequest; import org.cloudfoundry.operations.applications.ListApplicationTasksRequest; -import org.cloudfoundry.operations.applications.LogsRequest; import org.cloudfoundry.operations.applications.ManifestV3; import org.cloudfoundry.operations.applications.ManifestV3Application; import org.cloudfoundry.operations.applications.PushApplicationManifestRequest; @@ -96,6 +104,8 @@ public final class ApplicationsTest extends AbstractIntegrationTest { @Autowired private String serviceName; + @Autowired private LogCacheClient logCacheClient; + @Test public void copySource() throws IOException { String sourceName = this.nameFactory.getApplicationName(); @@ -486,7 +496,12 @@ public void listTasks() throws IOException { .verify(Duration.ofMinutes(5)); } + /** + * Doppler was dropped in PCF 4.x in favor of logcache. This test does not work + * on TAS 4.x. + */ @Test + @IfCloudFoundryVersion(lessThan = CloudFoundryVersion.PCF_4_v2) public void logs() throws IOException { String applicationName = this.nameFactory.getApplicationName(); @@ -499,14 +514,53 @@ public void logs() throws IOException { this.cloudFoundryOperations .applications() .logs( - LogsRequest.builder() + ApplicationLogsRequest.builder() .name(applicationName) .recent(true) .build())) - .map(LogMessage::getMessageType) + .map(ApplicationLog::getLogType) + .next() + .as(StepVerifier::create) + .expectNext(ApplicationLogType.OUT) + .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(MessageType.OUT) + .expectNext(LogType.OUT) .expectComplete() .verify(Duration.ofMinutes(5)); }