diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index ea548d05d6..8db78f9923 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -23,8 +23,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.TimeZone; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; @@ -48,6 +50,7 @@ import org.springframework.util.StringUtils; * @author Venil Noronha * @author Aurélien Leboulanger * @author Brian Clozel + * @author Olivier Lamy */ @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) public class ServerProperties { @@ -885,6 +888,11 @@ public class ServerProperties { */ public static class Jetty { + /** + * Access log configuration. + */ + private final Accesslog accesslog = new Accesslog(); + /** * Maximum size in bytes of the HTTP post or put content. */ @@ -900,6 +908,10 @@ public class ServerProperties { */ private Integer selectors; + public Accesslog getAccesslog() { + return this.accesslog; + } + public int getMaxHttpPostSize() { return this.maxHttpPostSize; } @@ -924,6 +936,168 @@ public class ServerProperties { this.selectors = selectors; } + /** + * Jetty access log properties. + */ + public static class Accesslog { + + /** + * Enable access log. + */ + private boolean enabled = false; + + /** + * Log filename. If not specified, logs will be redirected to "System.err". + */ + private String filename; + + /** + * Date format to place in log file name. + */ + private String fileDateFormat; + + /** + * Number of days before rotated log files are deleted. + */ + private int retentionPeriod = 31; // no days + + /** + * Append to log. + */ + private boolean append; + + /** + * Enable extended NCSA format. + */ + private boolean extendedFormat; + + /** + * Timestamp format of the request log. + */ + private String dateFormat = "dd/MMM/yyyy:HH:mm:ss Z"; + + /** + * Locale of the request log. + */ + private Locale locale; + + /** + * Timezone of the request log. + */ + private TimeZone timeZone = TimeZone.getTimeZone("GMT"); + + /** + * Enable logging of the request cookies. + */ + private boolean logCookies; + + /** + * Enable logging of the request hostname. + */ + private boolean logServer; + + /** + * Enable logging of request processing time. + */ + private boolean logLatency; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getFilename() { + return this.filename; + } + + public void setFilename(String filename) { + this.filename = filename; + } + + public String getFileDateFormat() { + return this.fileDateFormat; + } + + public void setFileDateFormat(String fileDateFormat) { + this.fileDateFormat = fileDateFormat; + } + + public int getRetentionPeriod() { + return this.retentionPeriod; + } + + public void setRetentionPeriod(int retentionPeriod) { + this.retentionPeriod = retentionPeriod; + } + + public boolean isAppend() { + return this.append; + } + + public void setAppend(boolean append) { + this.append = append; + } + + public boolean isExtendedFormat() { + return this.extendedFormat; + } + + public void setExtendedFormat(boolean extendedFormat) { + this.extendedFormat = extendedFormat; + } + + public String getDateFormat() { + return this.dateFormat; + } + + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + } + + public Locale getLocale() { + return this.locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + + public TimeZone getTimeZone() { + return this.timeZone; + } + + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + public boolean isLogCookies() { + return this.logCookies; + } + + public void setLogCookies(boolean logCookies) { + this.logCookies = logCookies; + } + + public boolean isLogServer() { + return this.logServer; + } + + public void setLogServer(boolean logServer) { + this.logServer = logServer; + } + + public boolean isLogLatency() { + return this.logLatency; + } + + public void setLogLatency(boolean logLatency) { + this.logLatency = logLatency; + } + } + } /** diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultServletWebServerFactoryCustomizer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultServletWebServerFactoryCustomizer.java index 702f3771b5..12c660b3a8 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultServletWebServerFactoryCustomizer.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultServletWebServerFactoryCustomizer.java @@ -36,6 +36,7 @@ import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.NCSARequestLog; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.HandlerCollection; @@ -66,6 +67,7 @@ import org.springframework.util.StringUtils; * * @author Brian Clozel * @author Stephane Nicoll + * @author Olivier Lamy * @since 2.0.0 */ public class DefaultServletWebServerFactoryCustomizer @@ -541,6 +543,9 @@ public class DefaultServletWebServerFactoryCustomizer customizeConnectionTimeout(factory, serverProperties.getConnectionTimeout()); } + if (jettyProperties.getAccesslog().isEnabled()) { + customizeAccesslog(factory, jettyProperties.getAccesslog()); + } } private static void customizeConnectionTimeout( @@ -637,6 +642,35 @@ public class DefaultServletWebServerFactoryCustomizer }); } + + private static void customizeAccesslog(JettyServletWebServerFactory factory, + final ServerProperties.Jetty.Accesslog accesslog) { + factory.addServerCustomizers(server -> { + NCSARequestLog requestLog = new NCSARequestLog(); + if (accesslog.getFilename() != null) { + requestLog.setFilename(accesslog.getFilename()); + } + if (accesslog.getFileDateFormat() != null) { + requestLog.setFilenameDateFormat(accesslog.getFileDateFormat()); + } + requestLog.setRetainDays(accesslog.getRetentionPeriod()); + requestLog.setAppend(accesslog.isAppend()); + requestLog.setExtended(accesslog.isExtendedFormat()); + if (accesslog.getDateFormat() != null) { + requestLog.setLogDateFormat(accesslog.getDateFormat()); + } + if (accesslog.getLocale() != null) { + requestLog.setLogLocale(accesslog.getLocale()); + } + if (accesslog.getTimeZone() != null) { + requestLog.setLogTimeZone(accesslog.getTimeZone().getID()); + } + requestLog.setLogCookies(accesslog.isLogCookies()); + requestLog.setLogServer(accesslog.isLogServer()); + requestLog.setLogLatency(accesslog.isLogLatency()); + server.setRequestLog(requestLog); + }); + } } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index 3fd99ba96f..849d91127c 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -184,6 +184,23 @@ public class ServerPropertiesTests { assertThat(this.properties.getJetty().getSelectors()).isEqualTo(10); } + @Test + public void testCustomizeJettyAccessLog() throws Exception { + Map map = new HashMap(); + map.put("server.jetty.accesslog.enabled", "true"); + map.put("server.jetty.accesslog.filename", "foo.txt"); + map.put("server.jetty.accesslog.file-date-format", "yyyymmdd"); + map.put("server.jetty.accesslog.retention-period", "4"); + map.put("server.jetty.accesslog.append", "true"); + bindProperties(map); + ServerProperties.Jetty jetty = this.properties.getJetty(); + assertThat(jetty.getAccesslog().isEnabled()).isEqualTo(true); + assertThat(jetty.getAccesslog().getFilename()).isEqualTo("foo.txt"); + assertThat(jetty.getAccesslog().getFileDateFormat()).isEqualTo("yyyymmdd"); + assertThat(jetty.getAccesslog().getRetentionPeriod()).isEqualTo(4); + assertThat(jetty.getAccesslog().isAppend()).isEqualTo(true); + } + private void bindProperties(Map map) { new RelaxedDataBinder(this.properties, "server") .bind(new MutablePropertyValues(map)); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DefaultServletWebServerFactoryCustomizerTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DefaultServletWebServerFactoryCustomizerTests.java index a8e119f043..c9c4eb9456 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DefaultServletWebServerFactoryCustomizerTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DefaultServletWebServerFactoryCustomizerTests.java @@ -20,6 +20,7 @@ import java.io.File; import java.net.URL; import java.util.EnumSet; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import javax.servlet.ServletContext; @@ -33,6 +34,8 @@ import org.apache.catalina.valves.AccessLogValve; import org.apache.catalina.valves.RemoteIpValve; import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; import org.apache.coyote.AbstractProtocol; +import org.eclipse.jetty.server.NCSARequestLog; +import org.eclipse.jetty.server.RequestLog; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -45,6 +48,7 @@ import org.springframework.beans.MutablePropertyValues; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.bind.RelaxedDataBinder; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; +import org.springframework.boot.web.embedded.jetty.JettyWebServer; import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatWebServer; @@ -527,6 +531,72 @@ public class DefaultServletWebServerFactoryCustomizerTests { verify(factory).setSessionStoreDir(new File("myfolder")); } + @Test + public void jettyAccessLogCanBeEnabled() { + JettyServletWebServerFactory factory = new JettyServletWebServerFactory(0); + Map map = new HashMap<>(); + map.put("server.jetty.accesslog.enabled", "true"); + bindProperties(map); + this.customizer.customize(factory); + JettyWebServer webServer = (JettyWebServer) factory.getWebServer(); + try { + NCSARequestLog requestLog = getNCSARequestLog(webServer); + assertThat(requestLog.getFilename()).isNull(); + assertThat(requestLog.isAppend()).isFalse(); + assertThat(requestLog.isExtended()).isFalse(); + assertThat(requestLog.getLogCookies()).isFalse(); + assertThat(requestLog.getLogServer()).isFalse(); + assertThat(requestLog.getLogLatency()).isFalse(); + } + finally { + webServer.stop(); + } + } + + @Test + public void jettyAccessLogCanBeCustomized() { + JettyServletWebServerFactory factory = new JettyServletWebServerFactory(0); + Map map = new HashMap<>(); + map.put("server.jetty.accesslog.enabled", "true"); + map.put("server.jetty.accesslog.filename", "foo"); + map.put("server.jetty.accesslog.file-date-format", "yyyy-MM-dd"); + map.put("server.jetty.accesslog.retention-period", "42"); + map.put("server.jetty.accesslog.append", "true"); + map.put("server.jetty.accesslog.extended-format", "true"); + map.put("server.jetty.accesslog.date-format", "HH:mm:ss"); + map.put("server.jetty.accesslog.locale", "en_BE"); + map.put("server.jetty.accesslog.time-zone", "UTC"); + map.put("server.jetty.accesslog.log-cookies", "true"); + map.put("server.jetty.accesslog.log-server", "true"); + map.put("server.jetty.accesslog.log-latency", "true"); + bindProperties(map); + this.customizer.customize(factory); + JettyWebServer webServer = (JettyWebServer) factory.getWebServer(); + NCSARequestLog requestLog = getNCSARequestLog(webServer); + try { + assertThat(requestLog.getFilename()).isEqualTo("foo"); + assertThat(requestLog.getFilenameDateFormat()).isEqualTo("yyyy-MM-dd"); + assertThat(requestLog.getRetainDays()).isEqualTo(42); + assertThat(requestLog.isAppend()).isTrue(); + assertThat(requestLog.isExtended()).isTrue(); + assertThat(requestLog.getLogDateFormat()).isEqualTo("HH:mm:ss"); + assertThat(requestLog.getLogLocale()).isEqualTo(new Locale("en", "BE")); + assertThat(requestLog.getLogTimeZone()).isEqualTo("UTC"); + assertThat(requestLog.getLogCookies()).isTrue(); + assertThat(requestLog.getLogServer()).isTrue(); + assertThat(requestLog.getLogLatency()).isTrue(); + } + finally { + webServer.stop(); + } + } + + private NCSARequestLog getNCSARequestLog(JettyWebServer webServer) { + RequestLog requestLog = webServer.getServer().getRequestLog(); + assertThat(requestLog).isInstanceOf(NCSARequestLog.class); + return (NCSARequestLog) requestLog; + } + @Test public void skipNullElementsForUndertow() throws Exception { UndertowServletWebServerFactory factory = mock( diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index c74468b44d..fb79a5db1f 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -157,6 +157,18 @@ content into your application; rather pick only the properties that you need. server.error.path=/error # Path of the error controller. server.error.whitelabel.enabled=true # Enable the default error page displayed in browsers in case of a server error. server.jetty.acceptors= # Number of acceptor threads to use. + server.jetty.accesslog.append=false # Append to log. + server.jetty.accesslog.date-format=dd/MMM/yyyy:HH:mm:ss Z # Timestamp format of the request log. + server.jetty.accesslog.enabled=false # Enable access log. + server.jetty.accesslog.extended-format=false # Enable extended NCSA format. + server.jetty.accesslog.file-date-format= # Date format to place in log file name. + server.jetty.accesslog.filename= # Log filename. If not specified, logs will be redirected to "System.err". + server.jetty.accesslog.locale= # Locale of the request log. + server.jetty.accesslog.log-cookies=false # Enable logging of the request cookies. + server.jetty.accesslog.log-latency=false # Enable logging of request processing time. + server.jetty.accesslog.log-server=false # Enable logging of the request hostname. + server.jetty.accesslog.retention-period=31 # Number of days before rotated log files are deleted. + server.jetty.accesslog.time-zone=GMT # Timezone of the request log. server.jetty.max-http-post-size=0 # Maximum size in bytes of the HTTP post or put content. server.jetty.selectors= # Number of selector threads to use. server.port=8080 # Server HTTP port. diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc index 54b677c0fb..7de90bc27d 100644 --- a/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -636,7 +636,8 @@ sample project for an example. [[howto-configure-accesslogs]] === Configure Access Logging -Access logs can be configured for Tomcat and Undertow via their respective namespaces. +Access logs can be configured for Tomcat, Undertow and Jetty via their respective +namespaces. For instance, the following logs access on Tomcat with a https://tomcat.apache.org/tomcat-8.0-doc/config/valve.html#Access_Logging[custom pattern]. @@ -664,6 +665,17 @@ Access logging for undertow can be configured in a similar fashion Logs are stored in a `logs` directory relative to the working directory of the application. This can be customized via `server.undertow.accesslog.directory`. +Finally, access logging for jetty can also be configured that way: + +[source,properties,indent=0,subs="verbatim,quotes,attributes"] +---- + server.jetty.accesslog.enabled=true + server.jetty.accesslog.filename=access-log +---- + +By default, logs will be redirected to `System.err`. For more details, please refer to +https://www.eclipse.org/jetty/documentation/9.3.x/configuring-jetty-request-logs.html[the documentation]. + [[howto-use-behind-a-proxy-server]]