Skip to content

Improve logging in DockerApi #44412

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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;
Expand Down Expand Up @@ -75,7 +76,7 @@ public Builder(DockerConfiguration dockerConfiguration) {
* @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);
}

/**
Expand All @@ -85,8 +86,8 @@ public Builder(BuildLog log) {
* @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) {
Expand Down Expand Up @@ -262,6 +263,41 @@ private Image pullImage(ImageReference reference, ImageType imageType) throws IO

}

/**
* 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}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

/**
Expand All @@ -96,21 +96,34 @@ public 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() {
Expand Down Expand Up @@ -485,7 +498,10 @@ public void delete(VolumeName name, boolean force) throws IOException {
*/
class SystemApi {

SystemApi() {
private final DockerLog log;

SystemApi(DockerLog log) {
this.log = log;
}

/**
Expand All @@ -502,6 +518,7 @@ ApiVersion getApiVersion() {
}
}
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
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;
Expand Down Expand Up @@ -75,6 +77,26 @@ void createWithDockerConfiguration() {
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
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;

Expand All @@ -82,7 +84,7 @@
* @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;
Expand All @@ -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() {
Expand Down Expand Up @@ -732,9 +734,10 @@ void getApiVersionWithNoVersionHeaderReturnsUnknownVersion() throws Exception {
}

@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");
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}

}
Loading