<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-starter</artifactId>
<version>2.16.0</version>
</dependency>
server:
env: ${APP_ENV}
servlet:
encoding:
#中文乱码
force-response: true
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import feign.Request;
import feign.Response;
import lombok.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.zalando.logbook.*;
import org.zalando.logbook.json.JsonHttpLogFormatter;
import javax.annotation.Nullable;
import java.io.*;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* 请求记录
*/
@Configuration(proxyBeanMethods = false)
public class LogbookAutoExtConfiguration {
private final LogbookAutoExtProperties logbookAutoExtProperties;
public LogbookAutoExtConfiguration(LogbookAutoExtProperties logbookAutoExtProperties) {
this.logbookAutoExtProperties = logbookAutoExtProperties;
}
@Bean
@RefreshScope
@ConditionalOnMissingBean(HttpLogFormatter.class)
public HttpLogFormatter jsonFormatter(final ObjectMapper mapper) {
return new CustomsFormatter(mapper,
logbookAutoExtProperties.getRequests(),
logbookAutoExtProperties.getResponses(),
logbookAutoExtProperties.getSkipPaths());
}
@Bean
@RefreshScope
@ConditionalOnMissingBean(HttpLogWriter.class)
public HttpLogWriter writer() {
return new CustomsHttpLogWriter();
}
@Bean
@ConditionalOnMissingBean(feign.Logger.class)
public feign.Logger feignLogbookLogger(Logbook logbook) {
return new FeignLogbookLogger(logbook);
}
@Setter
@Getter
@ToString
@RefreshScope
@Component
@ConfigurationProperties("logbook.ext")
public static class LogbookAutoExtProperties {
/**
* 打印哪些请求信息,不配打印所有
*
* @see StructuredHttpLogFormatter#prepare(Precorrelation, HttpRequest)
*/
private List<String> requests = Lists.newArrayList("type", "correlation", "method", "uri", "body");
/**
* 打印哪些返回信息,不配打印所有
*
* @see StructuredHttpLogFormatter#prepare(Correlation, HttpResponse)
*/
private List<String> responses = Lists.newArrayList("type", "correlation", "duration", "status", "body");
private List<String> skipPaths = Lists.newArrayList("/swagger-ui.html");
}
public static class CustomsFormatter implements HttpLogFormatter {
private final JsonHttpLogFormatter delegate;
private final List<String> requests;
private final List<String> responses;
private final List<String> skipPaths;
private final ThreadLocal<String> correlations = new ThreadLocal<>();
public CustomsFormatter(ObjectMapper mapper,
List<String> requests,
List<String> responses,
List<String> skipPaths) {
this.delegate = new JsonHttpLogFormatter(mapper);
this.requests = requests == null ? new ArrayList<>() : requests;
this.responses = responses == null ? new ArrayList<>() : responses;
this.skipPaths = skipPaths == null ? new ArrayList<>() : skipPaths;
}
@Override
public String format(@NonNull Precorrelation precorrelation,
@NonNull HttpRequest request) throws IOException {
String path = request.getPath();
if (skipPaths.contains(path)) {
correlations.set(precorrelation.getId());
return delegate.format(new HashMap<>());
}
Map<String, Object> content = delegate.prepare(precorrelation, request);
return delegate.format(filter(content, requests));
}
@Override
public String format(@NonNull Correlation correlation,
@NonNull HttpResponse response) throws IOException {
if (Objects.equals(correlations.get(), correlation.getId())) {
correlations.remove();
return delegate.format(new HashMap<>());
}
Map<String, Object> content = delegate.prepare(correlation, response);
return delegate.format(filter(content, responses));
}
private Map<String, Object> filter(Map<String, Object> content, List<String> filters) {
if (filters.isEmpty()) {
return content;
}
Map<String, Object> result = new LinkedHashMap<>();
filters.forEach(k -> {
Object value = content.get(k);
if (value != null) {
result.put(k, value);
}
});
return result.isEmpty() ? content : result;
}
}
public static class CustomsHttpLogWriter implements HttpLogWriter {
private final Logger log = LoggerFactory.getLogger(Logbook.class);
@Override
public boolean isActive() {
return log.isInfoEnabled();
}
@Override
public void write(@NonNull final Precorrelation precorrelation, @NonNull final String request) {
log.info(request);
}
@Override
public void write(@NonNull final Correlation correlation, @NonNull final String response) {
log.info(response);
}
}
/**
* copy
* org.zalando.logbook.openfeign.FeignLogbookLogger
*/
@AllArgsConstructor
public static class FeignLogbookLogger extends feign.Logger {
private final Logbook logbook;
private final ThreadLocal<Logbook.ResponseProcessingStage> stage = new ThreadLocal<>();
@Override
@Generated
protected void log(String configKey, String format, Object... args) {
}
@Override
protected void logRetry(String configKey, Level logLevel) {
}
@Override
protected IOException logIOException(String configKey, Level logLevel, IOException ioe, long elapsedTime) {
return ioe;
}
@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
final HttpRequest httpRequest = LocalRequest.create(request);
try {
Logbook.ResponseProcessingStage processingStage = logbook.process(httpRequest).write();
stage.set(processingStage);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) {
try {
byte[] body = response.body() != null ? toByteArray(response.body().asInputStream()) : null;
final HttpResponse httpResponse = RemoteResponse.create(response, body);
stage.get().process(httpResponse).write();
return Response.builder()
.status(response.status())
.request(response.request())
.reason(response.reason())
.headers(response.headers())
.body(body)
.build();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
static HttpHeaders toLogbookHeaders(Map<String, Collection<String>> feignHeaders) {
Map<String, List<String>> convertedHeaders = new HashMap<>();
for (Map.Entry<String, Collection<String>> header : feignHeaders.entrySet()) {
convertedHeaders.put(header.getKey(), new ArrayList<>(header.getValue()));
}
return HttpHeaders.of(convertedHeaders);
}
static byte[] toByteArray(final InputStream in) throws IOException {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
copy(in, out);
return out.toByteArray();
}
static void copy(final InputStream from, final OutputStream to) throws IOException {
final byte[] buf = new byte[4096];
while (true) {
final int r = from.read(buf);
if (r == -1) {
break;
}
to.write(buf, 0, r);
}
}
}
@RequiredArgsConstructor
static class LocalRequest implements HttpRequest {
private final URI uri;
private final Request.HttpMethod httpMethod;
private final HttpHeaders headers;
private final byte[] body;
private final Charset charset;
private boolean withBody = false;
public static LocalRequest create(Request request) {
return new LocalRequest(
URI.create(request.url()),
request.httpMethod(),
FeignLogbookLogger.toLogbookHeaders(request.headers()),
request.body(),
request.charset()
);
}
@Override
public String getRemote() {
return "localhost";
}
@Override
public String getMethod() {
return httpMethod.toString();
}
@Override
public String getScheme() {
return uri.getScheme() == null ? "" : uri.getScheme();
}
@Override
public String getHost() {
return uri.getHost() == null ? "" : uri.getHost();
}
@Override
public Optional<Integer> getPort() {
return Optional.of(uri).map(URI::getPort).filter(p -> p != -1);
}
@Override
public String getPath() {
return uri.getPath() == null ? "" : uri.getPath();
}
@Override
public String getQuery() {
return uri.getQuery() == null ? "" : uri.getQuery();
}
@Override
public HttpRequest withBody() {
withBody = true;
return this;
}
@Override
public HttpRequest withoutBody() {
withBody = false;
return this;
}
@Override
public String getProtocolVersion() {
return "HTTP/1.1";
}
@Override
public Origin getOrigin() {
return Origin.LOCAL;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
@Nullable
@Override
public String getContentType() {
return Optional.ofNullable(headers.get("Content-Type"))
.flatMap(ct -> ct.stream().findFirst())
.orElse(null);
}
@Override
public Charset getCharset() {
return charset == null ? StandardCharsets.UTF_8 : charset;
}
@Override
public byte[] getBody() {
return withBody && body != null ? body : new byte[0];
}
}
@RequiredArgsConstructor
static class RemoteResponse implements HttpResponse {
private final int status;
private final HttpHeaders headers;
private final byte[] body;
private final Charset charset;
private boolean withBody = false;
public static RemoteResponse create(Response response, byte[] body) {
return new RemoteResponse(
response.status(),
FeignLogbookLogger.toLogbookHeaders(response.headers()),
body,
StandardCharsets.UTF_8
);
}
@Override
public int getStatus() {
return status;
}
@Override
public String getProtocolVersion() {
return "HTTP/1.1";
}
@Override
public Origin getOrigin() {
return Origin.REMOTE;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
@Nullable
@Override
public String getContentType() {
return Optional.ofNullable(headers.get("Content-Type"))
.flatMap(ct -> ct.stream().findFirst())
.orElse(null);
}
@Override
public Charset getCharset() {
return charset;
}
@Override
public HttpResponse withBody() {
withBody = true;
return this;
}
@Override
public RemoteResponse withoutBody() {
withBody = false;
return this;
}
@Override
public byte[] getBody() {
return withBody && body != null ? body : new byte[0];
}
}
}