Skip to content

Commit c22d97c

Browse files
committed
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-43460 Signed-off-by: Dmytro Nosan <[email protected]>
1 parent 48e3de0 commit c22d97c

File tree

7 files changed

+236
-11
lines changed

7 files changed

+236
-11
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java

+39-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.function.Consumer;
2222

2323
import org.springframework.boot.buildpack.platform.docker.DockerApi;
24+
import org.springframework.boot.buildpack.platform.docker.DockerLog;
2425
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
2526
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
2627
import org.springframework.boot.buildpack.platform.docker.TotalProgressPushListener;
@@ -75,7 +76,7 @@ public Builder(DockerConfiguration dockerConfiguration) {
7576
* @param log a logger used to record output
7677
*/
7778
public Builder(BuildLog log) {
78-
this(log, new DockerApi(), null);
79+
this(log, new DockerApi(null, DelegatingBuildLogDockerLog.get(log)), null);
7980
}
8081

8182
/**
@@ -85,8 +86,8 @@ public Builder(BuildLog log) {
8586
* @since 2.4.0
8687
*/
8788
public Builder(BuildLog log, DockerConfiguration dockerConfiguration) {
88-
this(log, new DockerApi((dockerConfiguration != null) ? dockerConfiguration.getHost() : null),
89-
dockerConfiguration);
89+
this(log, new DockerApi((dockerConfiguration != null) ? dockerConfiguration.getHost() : null,
90+
DelegatingBuildLogDockerLog.get(log)), dockerConfiguration);
9091
}
9192

9293
Builder(BuildLog log, DockerApi docker, DockerConfiguration dockerConfiguration) {
@@ -262,6 +263,41 @@ private Image pullImage(ImageReference reference, ImageType imageType) throws IO
262263

263264
}
264265

266+
/**
267+
* A {@link DockerLog} implementation that delegates logging to a provided
268+
* {@link AbstractBuildLog}.
269+
*/
270+
static final class DelegatingBuildLogDockerLog implements DockerLog {
271+
272+
private final AbstractBuildLog log;
273+
274+
private DelegatingBuildLogDockerLog(AbstractBuildLog log) {
275+
this.log = log;
276+
}
277+
278+
@Override
279+
public void log(String message) {
280+
this.log.log(message);
281+
}
282+
283+
/**
284+
* Creates{@link DockerLog} instance based on the provided {@link BuildLog}.
285+
* <p>
286+
* If the provided {@link BuildLog} instance is an {@link AbstractBuildLog}, the
287+
* method returns a {@link DelegatingBuildLogDockerLog}, otherwise it returns a
288+
* default {@link DockerLog#toSystemOut()}.
289+
* @param log the {@link BuildLog} instance to delegate
290+
* @return a {@link DockerLog} instance for logging
291+
*/
292+
static DockerLog get(BuildLog log) {
293+
if (log instanceof AbstractBuildLog) {
294+
return new DelegatingBuildLogDockerLog(((AbstractBuildLog) log));
295+
}
296+
return DockerLog.toSystemOut();
297+
}
298+
299+
}
300+
265301
/**
266302
* {@link BuildpackResolverContext} implementation for the {@link Builder}.
267303
*/

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java

+22-5
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public class DockerApi {
8787
* Create a new {@link DockerApi} instance.
8888
*/
8989
public DockerApi() {
90-
this(HttpTransport.create(null));
90+
this(HttpTransport.create(null), DockerLog.toSystemOut());
9191
}
9292

9393
/**
@@ -96,21 +96,34 @@ public DockerApi() {
9696
* @since 2.4.0
9797
*/
9898
public DockerApi(DockerHostConfiguration dockerHost) {
99-
this(HttpTransport.create(dockerHost));
99+
this(HttpTransport.create(dockerHost), DockerLog.toSystemOut());
100+
}
101+
102+
/**
103+
* Create a new {@link DockerApi} instance.
104+
* @param dockerHost the Docker daemon host information
105+
* @param log a logger used to record output
106+
* @since 3.5.0
107+
*/
108+
public DockerApi(DockerHostConfiguration dockerHost, DockerLog log) {
109+
this(HttpTransport.create(dockerHost), log);
100110
}
101111

102112
/**
103113
* Create a new {@link DockerApi} instance backed by a specific {@link HttpTransport}
104114
* implementation.
105115
* @param http the http implementation
116+
* @param log a logger used to record output
106117
*/
107-
DockerApi(HttpTransport http) {
118+
DockerApi(HttpTransport http, DockerLog log) {
119+
Assert.notNull(http, "'http' must not be null");
120+
Assert.notNull(log, "'log' must not be null");
108121
this.http = http;
109122
this.jsonStream = new JsonStream(SharedObjectMapper.get());
110123
this.image = new ImageApi();
111124
this.container = new ContainerApi();
112125
this.volume = new VolumeApi();
113-
this.system = new SystemApi();
126+
this.system = new SystemApi(log);
114127
}
115128

116129
private HttpTransport http() {
@@ -485,7 +498,10 @@ public void delete(VolumeName name, boolean force) throws IOException {
485498
*/
486499
class SystemApi {
487500

488-
SystemApi() {
501+
private final DockerLog log;
502+
503+
SystemApi(DockerLog log) {
504+
this.log = log;
489505
}
490506

491507
/**
@@ -502,6 +518,7 @@ ApiVersion getApiVersion() {
502518
}
503519
}
504520
catch (Exception ex) {
521+
this.log.log("Warning: Failed to determine Docker API version: " + ex.getMessage());
505522
// fall through to return default value
506523
}
507524
return UNKNOWN_API_VERSION;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.buildpack.platform.docker;
18+
19+
import java.io.PrintStream;
20+
21+
/**
22+
* Callback interface used to provide {@link DockerApi} output logging.
23+
*
24+
* @author Dmytro Nosan
25+
* @since 3.5.0
26+
* @see #toSystemOut()
27+
*/
28+
public interface DockerLog {
29+
30+
/**
31+
* Logs a given message.
32+
* @param message the message to log
33+
*/
34+
void log(String message);
35+
36+
/**
37+
* Factory method that returns a {@link DockerLog} the outputs to {@link System#out}.
38+
* @return {@link DockerLog} instance that logs to system out
39+
*/
40+
static DockerLog toSystemOut() {
41+
return to(System.out);
42+
}
43+
44+
/**
45+
* Factory method that returns a {@link DockerLog} the outputs to a given
46+
* {@link PrintStream}.
47+
* @param out the print stream used to output the log
48+
* @return {@link DockerLog} instance that logs to the given print stream
49+
*/
50+
static DockerLog to(PrintStream out) {
51+
return new PrintStreamDockerLog(out);
52+
}
53+
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.buildpack.platform.docker;
18+
19+
import java.io.PrintStream;
20+
21+
import org.springframework.util.Assert;
22+
23+
/**
24+
* {@link DockerLog} implementation that prints output to a {@link PrintStream}.
25+
*
26+
* @author Dmytro Nosan
27+
*/
28+
class PrintStreamDockerLog implements DockerLog {
29+
30+
private final PrintStream stream;
31+
32+
PrintStreamDockerLog(PrintStream stream) {
33+
Assert.notNull(stream, "'stream' must not be null");
34+
this.stream = stream;
35+
}
36+
37+
@Override
38+
public void log(String message) {
39+
this.stream.println(message);
40+
}
41+
42+
}

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java

+22
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
import org.mockito.ArgumentCaptor;
2626
import org.mockito.stubbing.Answer;
2727

28+
import org.springframework.boot.buildpack.platform.build.Builder.DelegatingBuildLogDockerLog;
2829
import org.springframework.boot.buildpack.platform.docker.DockerApi;
2930
import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi;
3031
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
3132
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
33+
import org.springframework.boot.buildpack.platform.docker.DockerLog;
3234
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
3335
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
3436
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
@@ -75,6 +77,26 @@ void createWithDockerConfiguration() {
7577
assertThat(builder).isNotNull();
7678
}
7779

80+
@Test
81+
void createWithDelegatingDockerLogger() {
82+
Builder builder = new Builder(BuildLog.toSystemOut());
83+
assertThat(builder).isNotNull();
84+
assertThat(builder).extracting("docker")
85+
.extracting("system")
86+
.extracting("log")
87+
.isInstanceOf(DelegatingBuildLogDockerLog.class);
88+
}
89+
90+
@Test
91+
void createWithDockerLogger() {
92+
Builder builder = new Builder(mock(BuildLog.class));
93+
assertThat(builder).isNotNull();
94+
assertThat(builder).extracting("docker")
95+
.extracting("system")
96+
.extracting("log")
97+
.isInstanceOf(DockerLog.toSystemOut().getClass());
98+
}
99+
78100
@Test
79101
void buildWhenRequestIsNullThrowsException() {
80102
Builder builder = new Builder();

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java

+6-3
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
import org.springframework.boot.buildpack.platform.io.IOConsumer;
6060
import org.springframework.boot.buildpack.platform.io.Owner;
6161
import org.springframework.boot.buildpack.platform.io.TarArchive;
62+
import org.springframework.boot.testsupport.system.CapturedOutput;
63+
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
6264
import org.springframework.util.LinkedMultiValueMap;
6365
import org.springframework.util.MultiValueMap;
6466

@@ -82,7 +84,7 @@
8284
* @author Rafael Ceccone
8385
* @author Moritz Halbritter
8486
*/
85-
@ExtendWith(MockitoExtension.class)
87+
@ExtendWith({ MockitoExtension.class, OutputCaptureExtension.class })
8688
class DockerApiTests {
8789

8890
private static final String API_URL = "/v" + DockerApi.API_VERSION;
@@ -108,7 +110,7 @@ class DockerApiTests {
108110

109111
@BeforeEach
110112
void setup() {
111-
this.dockerApi = new DockerApi(this.http);
113+
this.dockerApi = new DockerApi(this.http, DockerLog.toSystemOut());
112114
}
113115

114116
private HttpTransport http() {
@@ -732,9 +734,10 @@ void getApiVersionWithNoVersionHeaderReturnsUnknownVersion() throws Exception {
732734
}
733735

734736
@Test
735-
void getApiVersionWithExceptionReturnsUnknownVersion() throws Exception {
737+
void getApiVersionWithExceptionReturnsUnknownVersion(CapturedOutput output) throws Exception {
736738
given(http().head(eq(new URI(PING_URL)))).willThrow(new IOException("simulated error"));
737739
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.UNKNOWN_API_VERSION);
740+
assertThat(output).contains("Warning: Failed to determine Docker API version: simulated error");
738741
}
739742

740743
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.buildpack.platform.docker;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.extension.ExtendWith;
21+
22+
import org.springframework.boot.testsupport.system.CapturedOutput;
23+
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
/**
28+
* Tests for {@link DockerLog}.
29+
*
30+
* @author Dmytro nosan
31+
*/
32+
@ExtendWith(OutputCaptureExtension.class)
33+
class DockerLogTests {
34+
35+
@Test
36+
void toSystemOutPrintsToSystemOut(CapturedOutput output) {
37+
DockerLog logger = DockerLog.toSystemOut();
38+
logger.log("Hello world");
39+
assertThat(output.getErr()).isEmpty();
40+
assertThat(output.getOut()).contains("Hello world");
41+
}
42+
43+
@Test
44+
void toPrintsToOutput(CapturedOutput output) {
45+
DockerLog logger = DockerLog.to(System.err);
46+
logger.log("Hello world");
47+
assertThat(output.getOut()).isEmpty();
48+
assertThat(output.getErr()).contains("Hello world");
49+
}
50+
51+
}

0 commit comments

Comments
 (0)