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:
parent
91e7d499b9
commit
9a940702bc
|
@ -21,6 +21,7 @@ import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.DockerApi;
|
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.TotalProgressEvent;
|
||||||
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
|
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
|
||||||
import org.springframework.boot.buildpack.platform.docker.TotalProgressPushListener;
|
import org.springframework.boot.buildpack.platform.docker.TotalProgressPushListener;
|
||||||
|
@ -75,7 +76,7 @@ public class Builder {
|
||||||
* @param log a logger used to record output
|
* @param log a logger used to record output
|
||||||
*/
|
*/
|
||||||
public Builder(BuildLog log) {
|
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
|
* @since 2.4.0
|
||||||
*/
|
*/
|
||||||
public Builder(BuildLog log, DockerConfiguration dockerConfiguration) {
|
public Builder(BuildLog log, DockerConfiguration dockerConfiguration) {
|
||||||
this(log, new DockerApi((dockerConfiguration != null) ? dockerConfiguration.getHost() : null),
|
this(log, new DockerApi((dockerConfiguration != null) ? dockerConfiguration.getHost() : null,
|
||||||
dockerConfiguration);
|
BuildLogDockerLogDelegate.get(log)), dockerConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
Builder(BuildLog log, DockerApi docker, DockerConfiguration 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}.
|
* {@link BuildpackResolverContext} implementation for the {@link Builder}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -87,7 +87,7 @@ public class DockerApi {
|
||||||
* Create a new {@link DockerApi} instance.
|
* Create a new {@link DockerApi} instance.
|
||||||
*/
|
*/
|
||||||
public DockerApi() {
|
public DockerApi() {
|
||||||
this(HttpTransport.create(null));
|
this(HttpTransport.create(null), DockerLog.toSystemOut());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -96,21 +96,34 @@ public class DockerApi {
|
||||||
* @since 2.4.0
|
* @since 2.4.0
|
||||||
*/
|
*/
|
||||||
public DockerApi(DockerHostConfiguration dockerHost) {
|
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}
|
* Create a new {@link DockerApi} instance backed by a specific {@link HttpTransport}
|
||||||
* implementation.
|
* implementation.
|
||||||
* @param http the http 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.http = http;
|
||||||
this.jsonStream = new JsonStream(SharedObjectMapper.get());
|
this.jsonStream = new JsonStream(SharedObjectMapper.get());
|
||||||
this.image = new ImageApi();
|
this.image = new ImageApi();
|
||||||
this.container = new ContainerApi();
|
this.container = new ContainerApi();
|
||||||
this.volume = new VolumeApi();
|
this.volume = new VolumeApi();
|
||||||
this.system = new SystemApi();
|
this.system = new SystemApi(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpTransport http() {
|
private HttpTransport http() {
|
||||||
|
@ -485,7 +498,10 @@ public class DockerApi {
|
||||||
*/
|
*/
|
||||||
class SystemApi {
|
class SystemApi {
|
||||||
|
|
||||||
SystemApi() {
|
private final DockerLog log;
|
||||||
|
|
||||||
|
SystemApi(DockerLog log) {
|
||||||
|
this.log = log;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -502,6 +518,7 @@ public class DockerApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
|
this.log.log("Warning: Failed to determine Docker API version: " + ex.getMessage());
|
||||||
// fall through to return default value
|
// fall through to return default value
|
||||||
}
|
}
|
||||||
return UNKNOWN_API_VERSION;
|
return UNKNOWN_API_VERSION;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -25,10 +25,12 @@ import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.stubbing.Answer;
|
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;
|
||||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi;
|
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.ImageApi;
|
||||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
|
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.TotalProgressPullListener;
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
||||||
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
|
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
|
||||||
|
@ -75,6 +77,26 @@ class BuilderTests {
|
||||||
assertThat(builder).isNotNull();
|
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
|
@Test
|
||||||
void buildWhenRequestIsNullThrowsException() {
|
void buildWhenRequestIsNullThrowsException() {
|
||||||
Builder builder = new Builder();
|
Builder builder = new Builder();
|
||||||
|
|
|
@ -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.IOConsumer;
|
||||||
import org.springframework.boot.buildpack.platform.io.Owner;
|
import org.springframework.boot.buildpack.platform.io.Owner;
|
||||||
import org.springframework.boot.buildpack.platform.io.TarArchive;
|
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.LinkedMultiValueMap;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
|
|
||||||
|
@ -82,7 +84,7 @@ import static org.mockito.Mockito.times;
|
||||||
* @author Rafael Ceccone
|
* @author Rafael Ceccone
|
||||||
* @author Moritz Halbritter
|
* @author Moritz Halbritter
|
||||||
*/
|
*/
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith({ MockitoExtension.class, OutputCaptureExtension.class })
|
||||||
class DockerApiTests {
|
class DockerApiTests {
|
||||||
|
|
||||||
private static final String API_URL = "/v" + DockerApi.API_VERSION;
|
private static final String API_URL = "/v" + DockerApi.API_VERSION;
|
||||||
|
@ -108,7 +110,7 @@ class DockerApiTests {
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setup() {
|
void setup() {
|
||||||
this.dockerApi = new DockerApi(this.http);
|
this.dockerApi = new DockerApi(this.http, DockerLog.toSystemOut());
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpTransport http() {
|
private HttpTransport http() {
|
||||||
|
@ -732,9 +734,10 @@ class DockerApiTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getApiVersionWithExceptionReturnsUnknownVersion() throws Exception {
|
void getApiVersionWithExceptionReturnsUnknownVersion(CapturedOutput output) throws Exception {
|
||||||
given(http().head(eq(new URI(PING_URL)))).willThrow(new IOException("simulated error"));
|
given(http().head(eq(new URI(PING_URL)))).willThrow(new IOException("simulated error"));
|
||||||
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.UNKNOWN_API_VERSION);
|
assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.UNKNOWN_API_VERSION);
|
||||||
|
assertThat(output).contains("Warning: Failed to determine Docker API version: simulated error");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue