Skip to content

A (yet another) Logging framework for Java / Groovy.

License

Notifications You must be signed in to change notification settings

osoco/java-logging

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

java-logging

A (yet another) Logging framework for Java / Groovy.

Getting Started

This framework provides your application multiple logging capabilities, while hiding you the details of how it's working internally.

You express your logging preferences using the @LoggingPreferences annotation. Within that execution context, your preferences will be honored. To log a message, you just need to call LoggingFactory.getInstance().createLogging(). It will return a Logging instance which gives you the methods you are expecting: info(String), debug(String) and the like.

Besides that, the Logging instance also provides you a way to pass additional context information so that the logging mechanism can optionally use it. By calling Logging#getLoggingContext(), you get a LoggingContext, which is a Map-like API storing the information locally to the thread.

Why another logging library

This library allows your code to express its logging preferences via annotations, and then use a minimal API. By preferences, we mean "if possible, use ElasticSearch and Log4J. If any of them fails, then use SLF4J and System.out".

In practice, we've found that using DDD allowed us to reuse the code in different scenarios. Frequently, those scenarios differ in the runtime infrastructure. Some of them use ElasticSearch, some of them delegate logging to CloudWatch (via AWS-Lambda logger). So we came up with a solution that has some benefits:

  • You declare your logging preferences.
  • You're not bound to any logging framework. Ours is just a thin layer that just resolves your preferences to the logging solutions available at runtime.
  • The logging API is the bare minimum: logging and logging context (for MDC/NDC capabilities).
  • You still use your current logging framework.
  • You still configure your logging as you need.

Runtime flags

By default, the framework will automatically discover logging configurations and producer implementations. Additionally, it finds out which logging preferences are defined, based on the methods in the stack. In some scenarios, such as AWS Lambda, that behavior is undesirable. You can switch them off using the following environment variables / system properties:

  • AUTOMATICALLY_DISCOVER_LOGGING_CONFIGURATIONS / automatically.discover.logging.configurations: Set to false to disable runtime discovery of logging configurations.
  • AUTOMATICALLY_DISCOVER_LOGGING_ANNOTATIONS / automatically.discover.logging.annotations: Set to false to disable runtime discovery of logging annotations.
  • AUTOMATICALLY_DISCOVER_LOGGING_CONFIGURATION_PRODUCERS / automatically.discover.logging.configuration.producers: Set to false to disable runtime discovery of logging configuration producers.
  • DEFAULT_PREFERRED_LOGGING / default.preferred.logging: Set to aws-lambda in your AWS Lambda functions.

Prerequisites

First, add Java-Logging as dependency in your pom.xml or build.gradle.

Maven dependency

The dependency is already in maven-central. Just add the dependency to your pom.xml.

<dependency>
  <groupId>es.osoco.logging</groupId>
  <artifactId>java-logging</artifactId>
  <version>0.2</version>
</dependency>

Gradle coordinates

Similarly, the Gradle coordinates are:

dependencies {
    compile(es.osoco.logging:java-logging:0.2)
}

Usage

When your code needs to log anything, first import the required classes:

import es.osoco.logging.Logging;
import es.osoco.logging.LoggingFactory;

Then, retrieve the Logging instance using the LoggingFactory:

Logging logging = LogFactory.createLogging();

Once you have the Logging instance, use it for, well, logging:

logging.info("Use case started");

Logging preferences

While the underlying logging configuration is auto-discovered at runtime, you should specify your preferences. You do so by using the @LoggingPreferences annotation either in a method or a class. Your preferences will define which logging mechanism will be used, within the execution context they are defined. The general use case is to define your preferences in the application's entry point(s). That way, they'll "stick" to all the code running within that execution flow.

For example, you could express your preferences in a static void main() method:

import es.osoco.logging.LoggingFactory;
import es.osoco.logging.annotations.LoggingPreferences;

public class EntryPoint {
    @LoggingPreferences(preferred="ElasticSearch", fallback="System.err")
    public static void main(String[] args) {
        LoggingFactory.getInstance().createLogging().info("Application started");
    }
}

A more complex scenario

Let's say your code is modelled after DDD principles, and at the application level you have different services or use cases interacting with your domain classes. For illustration purposes, let's say your application is listening both RabbitMQ messages, and HTTP requests for a REST API routed by an AWS API Gateway, spawning an AWS Lambda with your code.

public class RabbitMQAdapter implements ...
...
    public void onMessage(...) {
        // run the use case
    }
...
}
public class HttpServerAdapter implements ...
...
    public void onGet(...) {
        // run the use case
    }
...
}

In this scenario, let's say you'd like to log differently depending on the adapter: when the application receives messages from the RabbitMQ queue, you want to send the logs to a ElasticSearch server, whereas the Lambda should use the AWS-Lambda built-in logging mechanism itself.

You can accomplish that using @LoggingPreferences annotations:

@LoggingPreferences(preferred="ElasticSearch")
public class RabbitMQAdapter implements ...
@LoggingPreferences(preferred="aws-lambda")
public class HttpServerAdapter implements ...

Design concepts

This library uses two concepts: Logging Configuration, and Logging Adapters. Logging Configurations are abstractions to represent required configurations needed by Logging Adapters, but they don't know who uses them. Logging Adapters are the materializations of the Logging interface the client uses at runtime.

Logging Configuration

Logging Configurations are responsible to provide whatever information is needed for a Logging Adapter to work correctly. It includes file paths, remote server IPs and ports, and so on. They include the necessary checks as well. For example, file permissions, if the remote servers are up and running, etc.

Logging Configurations can be explicitly defined, and automatically discovered.

If your application can provide logging configuration automatically by itself, wrap that logic in a method, and annotate it with a @LoggingConfigurationProducer annotation. For example:

    @LoggingConfigurationProducer(key="logstash")
    public LoggingConfiguration initLogging() {
        return LogstashLoggingConfiguration("logstash", ...);
    }

The first time the library is loaded, it will auto-discover all @LoggingConfigurationProducer annotations, run the annotated methods, and collect the results into LoggingConfigurationRegistry.

The LoggingConfigurationRegistry discovers all child classes of LoggingConfigurationListener, and notifies them whenever a new LoggingConfiguration is collected.

You can also provide your own logging configuration whenever it's available. For example, the AWS-Lambda Logger instance is only available in the context of a Lambda execution. In such cases, you'll need to provide a LoggingConfiguration yourself. For example, in an AWS-Lambda RequestHandler, you'd want to call

new AwsLambdaLoggingConfigurationProducer().configureLogging(context.getLogger());

There's no API for LoggingConfigurationProducer, since there's no way for the library to automatically discover them (the ones that can be auto-discovered should use the @LoggingConfigurationProducer annotation). Fear not. Your code just needs to call:

LoggingConfigurationRegistry.getInstance().put("your-key", yourLoggingConfiguration);

Logging Adapters

The main responsibility of a LoggingConfigurationListener is to check if the new LoggingConfiguration is meaningful for him. If so, it will create a LoggingAdapterBuilder instance, and publish it into the LoggingAdapterBuilderRegistry.

Such registry simply maps keys with logging adapter builders. Builders know how to build LoggingAdapters using the LoggingConfiguration information.

LoggingFactory

The LoggingFactory resolves which logging keys are bound to the runtime context, based on the current stack trace. Once that keys (preferred and fallback) are known, it creates a composite instance after asking the relevant builders to create the LoggingAdapters. That composite logging then delegates the actual logging calls to the adapters: the log message is broadcasted to all preferred mechanisms. In case any of them fails, the same message is broadcasted to all fallback mechanisms.

Build from source

To build the artifact(s) yourself, just install Maven (2 or 3) and run:

mvn install

It will generate the artifacts under the target/ folder.

Running the tests

Java-Logging uses Spock as testing framework. To run the specifications, run

mvn test

or

gradle test

Contributing

Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.

Authors

Shibata team at OSOCO. See the <developers> section in the Maven pom.xml.

License

This project is licensed under the GPLv3 License - see the LICENSE.md file for details

Acknowledgments

The idea behind this library arose spontaneously after a refactoring of a DDD project. It used its own AWS-Lambda-based Logging, and it modelled it after the ports-and-adapters approach borrowed from hexagonal architectures.

We wanted to reuse that module in other contexts, so we stole the Logging code and started implementing this library.

The design was heavily influenced by Pharo (a Smalltalk dialect we at OSOCO are big fans of), and was made possible thanks to the awesome fast-classpath-scanner library from lukehutch.