Skip to content

Commit 2b854c7

Browse files
committed
Merge pull request #44412 from nosan
* pr/44412: Polish "Improve logging in DockerApi" Improve logging in DockerApi Closes gh-44412
2 parents 91e7d49 + 3be5e6c commit 2b854c7

File tree

6 files changed

+193
-11
lines changed

6 files changed

+193
-11
lines changed

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

+38-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, BuildLogAdapter.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+
BuildLogAdapter.get(log)), dockerConfiguration);
9091
}
9192

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

263264
}
264265

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

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 out::println;
52+
}
53+
54+
}

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.BuildLogAdapter;
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 createDockerApiWithLogDockerLogDelegate() {
82+
Builder builder = new Builder(BuildLog.toSystemOut());
83+
assertThat(builder).isNotNull();
84+
assertThat(builder).extracting("docker")
85+
.extracting("system")
86+
.extracting("log")
87+
.isInstanceOf(BuildLogAdapter.class);
88+
}
89+
90+
@Test
91+
void createDockerApiWithLogDockerSystemOutDelegate() {
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)