Documentation

You are viewing the documentation for the 3.0.2 release. The latest stable release series is 3.0.x.

§Configuring logging

Play uses SLF4J for logging, backed by Logback as its default logging engine. See the Logback documentation for details on configuration.

§Default configuration

In dev mode Play uses the following default configuration:

<?xml version="1.0" encoding="UTF-8" ?>
<!--
   Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
-->

<!DOCTYPE configuration>

<!-- The default logback configuration that Play uses in dev mode if no other configuration is provided -->
<configuration>
  <import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
  <import class="ch.qos.logback.core.ConsoleAppender"/>

  <appender name="STDOUT" class="ConsoleAppender">
    <encoder class="PatternLayoutEncoder">
      <pattern>%highlight(%-5level) %logger{15} - %message%n%xException{10}</pattern>
    </encoder>
  </appender>

  <logger name="play" level="INFO"/>
  <logger name="com.gargoylesoftware.htmlunit.javascript" level="OFF"/>

  <root level="WARN">
    <appender-ref ref="STDOUT"/>
  </root>

</configuration>

Play uses the following default configuration in production:

<?xml version="1.0" encoding="UTF-8" ?>
<!--
   Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
-->

<!DOCTYPE configuration>

<!-- The default logback configuration that Play uses if no other configuration is provided -->
<configuration>
  <import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
  <import class="ch.qos.logback.classic.AsyncAppender"/>
  <import class="ch.qos.logback.core.ConsoleAppender"/>

  <appender name="STDOUT" class="ConsoleAppender">
    <encoder class="PatternLayoutEncoder">
      <pattern>%highlight(%-5level) %logger{15} - %message%n%xException{10}</pattern>
    </encoder>
  </appender>

  <appender name="ASYNCSTDOUT" class="AsyncAppender">
    <!-- increases the default queue size -->
    <queueSize>512</queueSize>
    <!-- don't discard messages -->
    <discardingThreshold>0</discardingThreshold>
    <!-- block when queue is full -->
    <neverBlock>false</neverBlock>
    <appender-ref ref="STDOUT"/>
  </appender>

  <logger name="play" level="INFO"/>
  <logger name="com.gargoylesoftware.htmlunit.javascript" level="OFF"/>

  <root level="WARN">
    <appender-ref ref="ASYNCSTDOUT"/>
  </root>

</configuration>

A few things to note about these configurations:

To add a file logger, add the following appender to your conf/logback.xml file:

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>${application.home:-.}/logs/application.log</file>
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>%date [%level] from %logger in %thread - %message%n%xException</pattern>
    </encoder>
</appender>

Optionally use the async appender to wrap the FileAppender:

<appender name="ASYNCFILE" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="FILE" />
</appender>

Add the necessary appender(s) to the root:

<root level="WARN">
    <appender-ref ref="ASYNCFILE" />
    <appender-ref ref="ASYNCSTDOUT" />
</root>

§Security Logging

A security marker has been added for security related operations in Play, and failed security checks now log at WARN level, with the security marker set. This ensures that developers always know why a particular request is failing, which is important now that security filters are enabled by default in Play.

The security marker also allows security failures to be triggered or filtered distinct from normal logging. For example, to disable all logging with the SECURITY marker set, add the following lines to the logback.xml file:

<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
    <Marker>SECURITY</Marker>
    <OnMatch>DENY</OnMatch>
</turboFilter>

In addition, log events using the security marker can also trigger a message to a Security Information & Event Management (SEIM) engine for further processing.

§Using a custom application loader

Note that when using a custom application loader that does not extend the default GuiceApplicationLoader (for example when using compile-time dependency injection), the LoggerConfigurator needs to be manually invoked to pick up your custom configuration. You can do this with code like the following:

class MyApplicationLoaderWithInitialization extends ApplicationLoader {
  def load(context: Context) = {
    LoggerConfigurator(context.environment.classLoader).foreach {
      _.configure(context.environment, context.initialConfiguration, Map.empty)
    }
    new MyComponents(context).application
  }
}

§Custom configuration

For any custom configuration, you will need to specify your own Logback configuration file.

§Using a configuration file from project source

You can provide a default logging configuration by providing a file conf/logback.xml.

§Using an external configuration file

You can also specify a configuration file via a System property. This is particularly useful for production environments where the configuration file may be managed outside of your application source.

Note: The logging system gives top preference to configuration files specified by system properties, secondly to files in the conf directory, and lastly to the default. This allows you to customize your application’s logging configuration and still override it for specific environments or developer setups.

§Using -Dlogger.resource

Specify a configuration file to be loaded from the classpath:

$ start -Dlogger.resource=prod-logger.xml

§Using -Dlogger.file

Specify a configuration file to be loaded from the file system:

$ start -Dlogger.file=/opt/prod/logger.xml

Note: To see which file is being used, you can set a system property to debug it: -Dlogback.debug=true.

§Examples

Here’s an example of configuration that uses a rolling file appender, as well as a separate appender for outputting an access log:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>

<configuration>
  <import class="ch.qos.logback.classic.boolex.OnMarkerEvaluator"/>
  <import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
  <import class="ch.qos.logback.core.rolling.RollingFileAppender"/>
  <import class="ch.qos.logback.core.filter.EvaluatorFilter"/>
  <import class="ch.qos.logback.core.FileAppender"/>
  <import class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"/>

  <appender name="FILE" class="RollingFileAppender">
    <file>${application.home:-.}/logs/application.log</file>
    <rollingPolicy class="TimeBasedRollingPolicy">
      <!-- Daily rollover with compression -->
      <fileNamePattern>${application.home:-.}/logs/application-log-%d{yyyy-MM-dd}.gz</fileNamePattern>
      <!-- keep 30 days worth of history -->
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder class="PatternLayoutEncoder">
      <pattern>%date{yyyy-MM-dd HH:mm:ss ZZZZ} [%level] from %logger in %thread - %message%n%xException</pattern>
    </encoder>
  </appender>

  <appender name="SECURITY_FILE" class="FileAppender">
    <filter class="EvaluatorFilter">
      <evaluator class="OnMarkerEvaluator">
        <marker>SECURITY</marker>
      </evaluator>
      <onMismatch>DENY</onMismatch>
      <onMatch>ACCEPT</onMatch>
    </filter>
    <file>${application.home:-.}/logs/security.log</file>
    <encoder class="PatternLayoutEncoder">
      <pattern>%date [%level] [%marker] from %logger in %thread - %message%n%xException</pattern>
    </encoder>
  </appender>

  <appender name="ACCESS_FILE" class="RollingFileAppender">
    <file>${application.home:-.}/logs/access.log</file>
    <rollingPolicy class="TimeBasedRollingPolicy">
      <!-- daily rollover with compression -->
      <fileNamePattern>${application.home:-.}/logs/access-log-%d{yyyy-MM-dd}.gz</fileNamePattern>
      <!-- keep 1 week worth of history -->
      <maxHistory>7</maxHistory>
    </rollingPolicy>
    <encoder class="PatternLayoutEncoder">
      <pattern>%date{yyyy-MM-dd HH:mm:ss ZZZZ} %message%n</pattern>
      <!-- this quadruples logging throughput -->
      <immediateFlush>false</immediateFlush>
    </encoder>
  </appender>

  <!-- additivity=false ensures access log data only goes to the access log -->
  <logger name="access" level="INFO" additivity="false">
    <appender-ref ref="ACCESS_FILE"/>
  </logger>

  <root level="INFO">
    <appender-ref ref="FILE"/>
    <appender-ref ref="SECURITY_FILE"/>
  </root>

</configuration>

This demonstrates a few useful features:

Note: the file tag is optional and you can omit it if you want to avoid file renaming. See Logback docs for more information.

§Including Properties

By default, only the property application.home is exported to the logging framework, meaning that files can be referenced relative to the Play application:

 <file>${application.home:-}/example.log</file>

If you want to reference properties that are defined in the application.conf file, you can add play.logger.includeConfigProperties=true to your application.conf file. When the application starts, all properties defined in configuration will be available to the logger:

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>context = ${my.property.defined.in.application.conf} %message%n</pattern>
    </encoder>
</appender>

§Pekko logging configuration

Pekko system logging can be done by changing the org.apache.pekko logger to INFO.

<!-- Set logging for all Pekko library classes to INFO -->
<logger name="org.apache.pekko" level="INFO" />
<!-- Set a specific actor to DEBUG -->
<logger name="actors.MyActor" level="DEBUG" />

You may also wish to configure an appender for the Pekko loggers that includes useful properties such as thread and actor address. For more information about configuring Pekko’s logging, including details on Logback and Slf4j integration, see the Pekko documentation.

§Using a Custom Logging Framework

Play uses Logback by default, but it is possible to configure Play to use another logging framework as long as there is an SLF4J adapter for it. To do this, the PlayLogback sbt plugin must be disabled using disablePlugins:

lazy val root = (project in file("."))
  .enablePlugins(PlayScala)
  .disablePlugins(PlayLogback)

From there, a custom logging framework can be used. Here, Log4J 2 is used as an example.

libraryDependencies ++= Seq(
  "org.apache.logging.log4j" % "log4j-slf4j-impl" % "2.19.0",
  "org.apache.logging.log4j" % "log4j-api" % "2.19.0",
  "org.apache.logging.log4j" % "log4j-core" % "2.19.0"
)

Once the libraries and the SLF4J adapter are loaded, the log4j.configurationFile system property can be set on the command line as usual.

If custom configuration depending on Play’s mode is required, you can do additional customization with the LoggerConfigurator. To do this, add a logger-configurator.properties to the classpath, with

play.logger.configurator=Log4J2LoggerConfigurator

And then extend LoggerConfigurator with any customizations:

Java
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.slf4j.ILoggerFactory;
import org.slf4j.LoggerFactory;
import play.Environment;
import play.LoggerConfigurator;
import play.Mode;
import play.api.PlayException;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.*;
import org.apache.logging.log4j.core.config.Configurator;

public class JavaLog4JLoggerConfigurator implements LoggerConfigurator {

  private ILoggerFactory factory;

  @Override
  public void init(File rootPath, Mode mode) {
    Map<String, String> properties = new HashMap<>();
    properties.put("application.home", rootPath.getAbsolutePath());

    String resourceName = "log4j2.xml";
    URL resourceUrl = this.getClass().getClassLoader().getResource(resourceName);
    configure(properties, Optional.ofNullable(resourceUrl));
  }

  @Override
  public void configure(Environment env) {
    Map<String, String> properties =
        LoggerConfigurator.generateProperties(env, ConfigFactory.empty(), Collections.emptyMap());
    URL resourceUrl = env.resource("log4j2.xml");
    configure(properties, Optional.ofNullable(resourceUrl));
  }

  @Override
  public void configure(
      Environment env, Config configuration, Map<String, String> optionalProperties) {
    // LoggerConfigurator.generateProperties enables play.logger.includeConfigProperties=true
    Map<String, String> properties =
        LoggerConfigurator.generateProperties(env, configuration, optionalProperties);
    URL resourceUrl = env.resource("log4j2.xml");
    configure(properties, Optional.ofNullable(resourceUrl));
  }

  @Override
  public void configure(Map<String, String> properties, Optional<URL> config) {
    try {
      LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
      loggerContext.setConfigLocation(config.get().toURI());

      factory = LoggerFactory.getILoggerFactory();
    } catch (URISyntaxException ex) {
      throw new PlayException(
          "log4j2.xml resource was not found",
          "Could not parse the location for log4j2.xml resource",
          ex);
    }
  }

  @Override
  public ILoggerFactory loggerFactory() {
    return factory;
  }

  @Override
  public void shutdown() {
    LoggerContext loggerContext = (LoggerContext) LogManager.getContext();
    Configurator.shutdown(loggerContext);
  }
}
Scala
import java.io.File
import java.net.URI
import java.net.URL

import play.api.{Mode, Configuration, Environment, LoggerConfigurator}

import org.slf4j.ILoggerFactory

import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.core._
import org.apache.logging.log4j.core.config.Configurator

import org.slf4j.ILoggerFactory
import org.slf4j.LoggerFactory
import play.api.Configuration
import play.api.Environment
import play.api.LoggerConfigurator
import play.api.Mode

class Log4J2LoggerConfigurator extends LoggerConfigurator {
  private var factory: ILoggerFactory = _

  override def init(rootPath: File, mode: Mode): Unit = {
    val properties   = Map("application.home" -> rootPath.getAbsolutePath)
    val resourceName = "log4j2.xml"
    val resourceUrl  = Option(this.getClass.getClassLoader.getResource(resourceName))
    configure(properties, resourceUrl)
  }

  override def shutdown(): Unit = {
    val context = LogManager.getContext().asInstanceOf[LoggerContext]
    Configurator.shutdown(context)
  }

  override def configure(env: Environment): Unit = {
    val properties  = LoggerConfigurator.generateProperties(env, Configuration.empty, Map.empty)
    val resourceUrl = env.resource("log4j2.xml")
    configure(properties, resourceUrl)
  }

  override def configure(
      env: Environment,
      configuration: Configuration,
      optionalProperties: Map[String, String]
  ): Unit = {
    // LoggerConfigurator.generateProperties enables play.logger.includeConfigProperties=true
    val properties  = LoggerConfigurator.generateProperties(env, configuration, optionalProperties)
    val resourceUrl = env.resource("log4j2.xml")
    configure(properties, resourceUrl)
  }

  override def configure(properties: Map[String, String], config: Option[URL]): Unit = {
    val context = LogManager.getContext(false).asInstanceOf[LoggerContext]
    context.setConfigLocation(config.get.toURI)

    factory = LoggerFactory.getILoggerFactory
  }

  override def loggerFactory: ILoggerFactory = factory
}

Next: Configuring WS SSL