Improve logging in DockerApi

This commit introduces a new constructor in `DockerApi`
that accepts `DockerLogger` as a parameter.

The `DockerLogger` is a pretty simple callback interface used to
provide DockerApi output logging.

See gh-44412

Signed-off-by: Dmytro Nosan <dimanosan@gmail.com>
This commit is contained in:
Dmytro Nosan 2025-02-23 20:32:55 +02:00 committed by Moritz Halbritter
parent 91e7d499b9
commit 9a940702bc
8 changed files with 291 additions and 11 deletions

View File

@ -21,6 +21,7 @@ import java.util.List;
import java.util.function.Consumer;
import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.DockerLog;
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
import org.springframework.boot.buildpack.platform.docker.TotalProgressPushListener;
@ -75,7 +76,7 @@ public class Builder {
* @param log a logger used to record output
*/
public Builder(BuildLog log) {
this(log, new DockerApi(), null);
this(log, new DockerApi(null, BuildLogDockerLogDelegate.get(log)), null);
}
/**
@ -85,8 +86,8 @@ public class Builder {
* @since 2.4.0
*/
public Builder(BuildLog log, DockerConfiguration dockerConfiguration) {
this(log, new DockerApi((dockerConfiguration != null) ? dockerConfiguration.getHost() : null),
dockerConfiguration);
this(log, new DockerApi((dockerConfiguration != null) ? dockerConfiguration.getHost() : null,
BuildLogDockerLogDelegate.get(log)), dockerConfiguration);
}
Builder(BuildLog log, DockerApi docker, DockerConfiguration dockerConfiguration) {
@ -262,6 +263,41 @@ public class Builder {
}
/**
* A {@link DockerLog} implementation that delegates logging to a provided
* {@link AbstractBuildLog}.
*/
static final class BuildLogDockerLogDelegate implements DockerLog {
private final AbstractBuildLog log;
private BuildLogDockerLogDelegate(AbstractBuildLog log) {
this.log = log;
}
@Override
public void log(String message) {
this.log.log(message);
}
/**
* Creates{@link DockerLog} instance based on the provided {@link BuildLog}.
* <p>
* If the provided {@link BuildLog} instance is an {@link AbstractBuildLog}, the
* method returns a {@link BuildLogDockerLogDelegate}, otherwise it returns a
* default {@link DockerLog#toSystemOut()}.
* @param log the {@link BuildLog} instance to delegate
* @return a {@link DockerLog} instance for logging
*/
static DockerLog get(BuildLog log) {
if (log instanceof AbstractBuildLog) {
return new BuildLogDockerLogDelegate(((AbstractBuildLog) log));
}
return DockerLog.toSystemOut();
}
}
/**
* {@link BuildpackResolverContext} implementation for the {@link Builder}.
*/

View File

@ -87,7 +87,7 @@ public class DockerApi {
* Create a new {@link DockerApi} instance.
*/
public DockerApi() {
this(HttpTransport.create(null));
this(HttpTransport.create(null), DockerLog.toSystemOut());
}
/**
@ -96,21 +96,34 @@ public class DockerApi {
* @since 2.4.0
*/
public DockerApi(DockerHostConfiguration dockerHost) {
this(HttpTransport.create(dockerHost));
this(HttpTransport.create(dockerHost), DockerLog.toSystemOut());
}
/**
* Create a new {@link DockerApi} instance.
* @param dockerHost the Docker daemon host information
* @param log a logger used to record output
* @since 3.5.0
*/
public DockerApi(DockerHostConfiguration dockerHost, DockerLog log) {
this(HttpTransport.create(dockerHost), log);
}
/**
* Create a new {@link DockerApi} instance backed by a specific {@link HttpTransport}
* implementation.
* @param http the http implementation
* @param log a logger used to record output
*/
DockerApi(HttpTransport http) {
DockerApi(HttpTransport http, DockerLog log) {
Assert.notNull(http, "'http' must not be null");
Assert.notNull(log, "'log' must not be null");
this.http = http;
this.jsonStream = new JsonStream(SharedObjectMapper.get());
this.image = new ImageApi();
this.container = new ContainerApi();
this.volume = new VolumeApi();
this.system = new SystemApi();
this.system = new SystemApi(log);
}
private HttpTransport http() {
@ -485,7 +498,10 @@ public class DockerApi {
*/
class SystemApi {
SystemApi() {
private final DockerLog log;
SystemApi(DockerLog log) {
this.log = log;
}
/**
@ -502,6 +518,7 @@ public class DockerApi {
}
}
catch (Exception ex) {
this.log.log("Warning: Failed to determine Docker API version: " + ex.getMessage());
// fall through to return default value
}
return UNKNOWN_API_VERSION;

View File

@ -0,0 +1,54 @@
/*
* Copyright 2012-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
*
* https://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.springframework.boot.buildpack.platform.docker;
import java.io.PrintStream;
/**
* Callback interface used to provide {@link DockerApi} output logging.
*
* @author Dmytro Nosan
* @since 3.5.0
* @see #toSystemOut()
*/
public interface DockerLog {
/**
* Logs a given message.
* @param message the message to log
*/
void log(String message);
/**
* Factory method that returns a {@link DockerLog} the outputs to {@link System#out}.
* @return {@link DockerLog} instance that logs to system out
*/
static DockerLog toSystemOut() {
return to(System.out);
}
/**
* Factory method that returns a {@link DockerLog} the outputs to a given
* {@link PrintStream}.
* @param out the print stream used to output the log
* @return {@link DockerLog} instance that logs to the given print stream
*/
static DockerLog to(PrintStream out) {
return new PrintStreamDockerLog(out);
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2012-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
*
* https://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.springframework.boot.buildpack.platform.docker;
import java.io.PrintStream;
import org.springframework.util.Assert;
/**
* {@link DockerLog} implementation that prints output to a {@link PrintStream}.
*
* @author Dmytro Nosan
*/
class PrintStreamDockerLog implements DockerLog {
private final PrintStream stream;
PrintStreamDockerLog(PrintStream stream) {
Assert.notNull(stream, "'stream' must not be null");
this.stream = stream;
}
@Override
public void log(String message) {
this.stream.println(message);
}
}

View File

@ -25,10 +25,12 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.stubbing.Answer;
import org.springframework.boot.buildpack.platform.build.Builder.BuildLogDockerLogDelegate;
import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
import org.springframework.boot.buildpack.platform.docker.DockerLog;
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
@ -75,6 +77,26 @@ class BuilderTests {
assertThat(builder).isNotNull();
}
@Test
void createDockerApiWithLogDockerLogDelegate() {
Builder builder = new Builder(BuildLog.toSystemOut());
assertThat(builder).isNotNull();
assertThat(builder).extracting("docker")
.extracting("system")
.extracting("log")
.isInstanceOf(BuildLogDockerLogDelegate.class);
}
@Test
void createDockerApiWithLogDockerSystemOutDelegate() {
Builder builder = new Builder(mock(BuildLog.class));
assertThat(builder).isNotNull();
assertThat(builder).extracting("docker")
.extracting("system")
.extracting("log")
.isInstanceOf(DockerLog.toSystemOut().getClass());
}
@Test
void buildWhenRequestIsNullThrowsException() {
Builder builder = new Builder();

View File

@ -59,6 +59,8 @@ import org.springframework.boot.buildpack.platform.io.Content;
import org.springframework.boot.buildpack.platform.io.IOConsumer;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive;
import org.springframework.boot.testsupport.system.CapturedOutput;
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@ -82,7 +84,7 @@ import static org.mockito.Mockito.times;
* @author Rafael Ceccone
* @author Moritz Halbritter
*/
@ExtendWith(MockitoExtension.class)
@ExtendWith({ MockitoExtension.class, OutputCaptureExtension.class })
class DockerApiTests {
private static final String API_URL = "/v" + DockerApi.API_VERSION;
@ -108,7 +110,7 @@ class DockerApiTests {
@BeforeEach
void setup() {
this.dockerApi = new DockerApi(this.http);
this.dockerApi = new DockerApi(this.http, DockerLog.toSystemOut());
}
private HttpTransport http() {
@ -732,9 +734,10 @@ class DockerApiTests {
}
@Test
void getApiVersionWithExceptionReturnsUnknownVersion() throws Exception {
void getApiVersionWithExceptionReturnsUnknownVersion(CapturedOutput output) throws Exception {
given(http().head(eq(new URI(PING_URL)))).willThrow(new IOException("simulated error"));
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.UNKNOWN_API_VERSION);
assertThat(output).contains("Warning: Failed to determine Docker API version: simulated error");
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2012-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
*
* https://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.springframework.boot.buildpack.platform.docker;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.testsupport.system.CapturedOutput;
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DockerLog}.
*
* @author Dmytro nosan
*/
@ExtendWith(OutputCaptureExtension.class)
class DockerLogTests {
@Test
void toSystemOutPrintsToSystemOut(CapturedOutput output) {
DockerLog logger = DockerLog.toSystemOut();
logger.log("Hello world");
assertThat(output.getErr()).isEmpty();
assertThat(output.getOut()).contains("Hello world");
}
@Test
void toPrintsToOutput(CapturedOutput output) {
DockerLog logger = DockerLog.to(System.err);
logger.log("Hello world");
assertThat(output.getOut()).isEmpty();
assertThat(output.getErr()).contains("Hello world");
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2012-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
*
* https://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.springframework.boot.buildpack.platform.docker;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PrintStreamDockerLog}.
*
* @author Dmytro Nosan
*/
class PrintStreamDockerLogTests {
@Test
void printsExpectedOutput() {
TestPrintStream stream = new TestPrintStream();
PrintStreamDockerLog logger = new PrintStreamDockerLog(stream);
logger.log("Some message");
logger.log("Some message1");
assertThat(stream.toString()).isEqualTo(String.format("Some message%nSome message1%n"));
}
static class TestPrintStream extends PrintStream {
TestPrintStream() {
super(new ByteArrayOutputStream());
}
@Override
public String toString() {
return this.out.toString();
}
}
}