elf4j

Maven Central

elf4j - Easy Logging Facade for Java

API and SPI of a no-fluff Java logging facade

User stories

  1. As a Java application developer, I want to use a log service API, so that I can choose or switch to use any compliant log service provider without changing my application code.
  2. As a log service/engine/framework provider, I want to implement a Service Provider Interface (SPI), so that the log service client application can opt (either in or out) to use my log service implementation, without code change.

Prerequisite

Note that individual logging service providers may require higher JDK versions.

What it is…

It is a logging Service Interface and Access API

If you are familiar with other logging APIs such as SLF4J/LOGBACK/LOG4J, you will find most counterpart logging methods in ELF4J, with some noticeable differences:

It is a logging Service Provider Interface (SPI)

public interface LoggerFactory {
    Logger getLogger();
}

See details in the section of “Use it as the service provider interface (SPI) to provide concrete log service”.

Conventions, Defaults, and Implementation Notes (a.k.a. “the spec”)

Thread safety

A Logger instance should be thread-safe.

Severity Level

If a Logger instance is obtained via the Logger.instance() static factory method, then the default severity level of such instance is decided by the service provider implementation. If a Logger instance is obtained via one of the Logger.at<Level> instance factory methods, then its severity level should be as requested.

Placeholder token

The empty curly braces token {} should be the placeholder for message arguments. This is by convention, and does not syntactically appear in the API or SPI. Both the API user and the Service Provider must honor such convention.

Lazy arguments

Lazy arguments are those whose runtime type is java.util.function.Supplier, often provided via lambda expressions. Unlike other/eager types of arguments, lazy ones have to be treated specially in that the Supplier function must be applied first before the result is used as the substitution to the argument placeholder {}. This special handling of lazy arguments is by convention, and not syntactically enforced by the API or SPI. It allows for the API user to mix up lazy and eager arguments within the same logging method call.

Get it

Maven Central

Include it as a compile-scope dependency in a build tool compatible with Maven Dependency Management. e.g.

<dependency>
    <groupId>io.github.elf4j</groupId>
    <artifactId>elf4j</artifactId>
    <version>${version}</version>
</dependency>

Use it…

Use it as the logging Service (facade) API (while hiding the concrete log service provider API from your application code)

class SampleUsage {
  static Logger logger = Logger.instance();

  @Nested
  class plainText {
    @Test
    void declarationsAndLevels() {
      logger.log(
          "Logger instance is thread-safe so it can be declared and used as a local, instance, or static variable");
      logger.log("Default severity level is decided by the logging provider implementation");
      Logger trace = logger.atTrace();
      trace.log("Explicit severity level is specified by user i.e. TRACE");
      Logger.instance().atTrace().log("Same explicit level TRACE");
      logger.atDebug().log("Severity level is DEBUG");
      logger.atInfo().log("Severity level is INFO");
      trace.atWarn().log("Severity level is WARN, not TRACE");
      logger.atError().log("Severity level is ERROR");
      Logger.instance()
          .atDebug()
          .atError()
          .atTrace()
          .atWarn()
          .atInfo()
          .log("Not a practical example but the severity level is INFO");
    }
  }

  @Nested
  class textWithArguments {
    Logger info = logger.atInfo();

    @Test
    void lazyAndEagerArgumentsCanBeMixed() {
      info.log("Message can have any number of arguments of {} type", Object.class.getTypeName());
      info.log(
          "Lazy arguments, of {} type, whose values may be {} can be mixed with eager arguments of non-Supplier types",
          Supplier.class.getTypeName(),
          (Supplier) () -> "expensive to compute");
      info.atWarn()
          .log("The Supplier downcast is mandatory per lambda syntax because arguments are declared as generic Object rather than functional interface");
    }
  }

  @Nested
  class supplierMessageAndArguments {
    Logger logger = Logger.instance();

    @Test
    void noDowncastNeededWhenAllMessageOrArgumentsAreSuppliers() {
      logger.log(
          () ->
              "No downcast needed when message or arguments are all of Supplier type, rather than mixed with Object types");
      logger.log("Message can have any number of {} type arguments", Supplier.class::getTypeName);
      logger.log(
          "Lazy arguments of {} type can be used to supply values that may be {}",
          Supplier.class::getTypeName,
          () -> "expensive to compute");
      Exception ex = new Exception("test ex for Suppliers");
      logger.log(ex, () -> "Exception log message can be a Supplier");
      logger.log(ex, "So can the {}'s {}", () -> "message", () -> "arguments");
    }
  }

  @Nested
  class throwable {
    @Test
    void asTheFirstArgument() {
      Exception exception = new Exception("Exception message");
      logger.atError().log(exception);
      logger.atError().log(exception, "Optional log message");
      logger.atInfo()
          .log(exception,
              "Exception is always the first argument to a logging method. The {} log message and following arguments work the same way {}.",
              "optional",
              (Supplier) () -> "as usual");
    }
  }
}

Note that elf4j is a logging service facade and specification, rather than the implementation. As such,

Use it as the service provider interface (SPI) to provide concrete log service

To enable an independent logging framework/engine via the elf4j spec, the service provider should follow instructions of Java Service Provider Framework. Namely, the implementation should include

Available logging service providers of elf4j