Extending

You can edit this page on GitHub

tinylog is extendable. It is possible to develop custom configuration loaders, file converters, logging APIs, logging providers, throwable filters, writers, and policies. How this works, what has to be considered, and what are the possibilities, are explained by means of examples.

Custom Configuration Loader

By default, tinylog load its configuration from a properties file. However, it is possible to support other file formats such as JSON or YAML by writing a custom configuration loader that implements the ConfigurationLoader interface.

The configuration loader in the following example is a simplified version of tinylog’s PropertiesConfigurationLoader. It just loads the configuration from tinylog.properties form the class path.

package org.tinylog.example;

import org.tinylog.configuration.ConfigurationLoader;
import org.tinylog.runtime.RuntimeProvider;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class PropertiesConfigurationLoader implements ConfigurationLoader {

    @Override
    public Properties load() throws IOException {
        Properties properties = new Properties();

        ClassLoader classLoader = RuntimeProvider.getClassLoader();
        try (InputStream stream = classLoader.getResourceAsStream("tinylog.properties")) {
            if (stream != null) {
                properties.load(stream);
            }
        }

        return properties;
    }

}

More advanced examples for loading the configuration from a JSON or YAML file can be found in the GitHub repository tinylog_extra_stuff.

Custom configuration loaders have to be registered as a service so that tinylog can find them at runtime. If it does not yet exist, create the directory META-INF/services and insert a text file with the name org.tinylog.configuration.ConfigurationLoader. In this text file, your custom configuration loaders can be input with its fully-qualified class name.

For the new custom configuration loader, META-INF/services/org.tinylog.configuration.ConfigurationLoader would look like this:

org.tinylog.example.PropertiesConfigurationLoader

Custom File Converter

A custom file converter can transform the output of rolling file writers and simply has to implement the interface FileConverter with its four methods.

The file converter in the following example encrypts all written binary data by shifting each byte value by its position in the log file. The encryption algorithm is not secure but is a good and simple example to illustrate how to implement a custom file converter.

package org.tinylog.example;

import java.io.File;
import org.tinylog.converters.FileConverter;

public class CipherFileConverter implements FileConverter {

    private long offset = 0;

    @Override
    public String getBackupSuffix() {
        return null; // No additional file extension for backup files
    }

    @Override
    public void open(String fileName) {
        File file = new File(fileName);
        offset = file.exists() ? file.length() : 0;
    }

    @Override
    public byte[] write(byte[] data) {
        byte[] encrypted = new byte[data.length];
        for (int i = 0; i < data.length; ++i) {
            encrypted[i] = (byte) ((Byte.toUnsignedLong(data[i]) + offset + i) % 256);
        }
        offset += encrypted.length;
        return encrypted;
     }

    @Override
    public void close() {
        // Nothing to do when the writer is closing the current file
    }

    @Override
    public void shutdown() {
        // Nothing to do when tinylog is shutting down
    }

}

The getter getBackupSuffix() can return a file extension for backup files, if the converter deletes the original log file after closing it and creates a new file with an additional file extension. This is not the case in the example above. Therefore, it just returns null. However, the GZIP file converter, which comes directly with tinylog, makes use of this feature.

Before the rolling file writer opens or creates a log file, the method open() is called. All data can be transformed in the method write() before it is written to the current log file. After the rolling file writer has closed the current log file, the method close() is called. Finally, the method shutdown() is called while tinylog is shutting down.

Custom file converters have to be registered as a service so that tinylog can find them at runtime. If it does not yet exist, create the directory META-INF/services and insert a text file with the name org.tinylog.converters.FileConverter. In this text file, all custom file converters can be input line by line with their fully-qualified class names.

For the new custom file converter, META-INF/services/org.tinylog.converters.FileConverter would look like this:

org.tinylog.example.CipherFileConverter

Now the file converter is ready to be used. To use it in a configuration, the name of the file converter is derived from the class name. Spaces are inserted between the words, and “File Converter” disappears at the end. Hence, CipherFileConverter becomes cipher.

Configuration for activating the new custom file converter in tinylog.properties:

writer         = rolling file
writer.file    = output-{count}.log
writer.convert = cipher

Custom Logging API

Log entries can be provided by any logging API to tinylog’s logging back-end. There are already multiple third-party logging APIs for tinylog, and a good and easy real world example of a custom logging API is jcl-tinylog in tinylog’s GitHub repository.

A custom logger can pass log entries to tinylog’s logging back-end via LoggingProvider.log(). The log() method accepts the following parameters:

  1. Depth of caller: If you call LoggingProvider.log() directly from your custom logger, this is 2. For each additional method between caller and logging provider in the stack trace, you have to add +1. By knowing the exact depth of the caller in the stack trace, tinylog can extract the location information directly. This is one reason why tinylog is much faster than other logging frameworks. If you don’t know the depth of the caller, you can also pass the fully-qualified logger class name instead of the depth.

  2. Tag: Log entries can optionally be tagged with a string.

  3. Severity level: This can be any supported severity level.

  4. Exception: This can be null, an exception, or any other kind of throwable.

  5. Message Formatter: Message formatters are used to format messages with arguments. If there are no arguments, no message formatter is required and null can be passed. The most common message formatters are AdvancedMessageFormatter and PrintfStyleFormatter.

  6. Message: This can be a string or any other object with a meaningful toString() method. The message can be null, if an exception or another kind of throwable is passed as the fourth argument. If there is a message formatter and arguments, the message may contain placeholders.

  7. Arguments: Arguments are optional and require both a message formatter and a message with placeholders.

Example custom logger:

package org.tinylog.example;

import org.tinylog.Level;
import org.tinylog.format.PrintfStyleFormatter;
import org.tinylog.provider.LoggingProvider;
import org.tinylog.provider.ProviderRegistry;

import java.util.Locale;

public class MyLogger {

    private static final LoggingProvider provider = ProviderRegistry.getLoggingProvider();

    // Example: MyLogger.info("Hello World!")
    public static void info(String message) {
        provider.log(
            2,          // depth in stack trace
            null,       // optional tag
            Level.INFO, // severity level
            null,       // exception or any other kind of throwable
            null,       // message formatter for arguments
            message     // message to log
        );
    }

    // Example: MyLogger.printf("Hello %s!", "World")
    public static void printf(String message, Object... arguments) {
        PrintfStyleFormatter formatter = new PrintfStyleFormatter(Locale.ENGLISH);
        provider.log(2, null, Level.INFO, null, formatter, message, arguments);
    }

    // Example: MyLogger.error(ex)
    public static void error(Throwable exception) {
        provider.log(2, null, Level.ERROR, exception, null, null);
    }

}

It is recommended to store the status of enabled severity levels as final booleans and test them before passing any log entries. This allows the JVM to easily identify which logging methods will never output anything at runtime and remove the calls. For this reason, the performance of tinylog 2 for disabled severity levels is identical to an invocation of an empty method that does nothing.

package org.tinylog.example;

import org.tinylog.Level;
import org.tinylog.provider.LoggingProvider;
import org.tinylog.provider.ProviderRegistry;

public class MyLogger {

    private static final LoggingProvider provider = ProviderRegistry.getLoggingProvider();

    private static final int minimumLevel = provider.getMinimumLevel(null).ordinal();
    private static final boolean infoEnabled = minimumLevel <= Level.INFO.ordinal();
    private static final boolean errorEnabled = minimumLevel <= Level.ERROR.ordinal();

    public static void info(String message) {
        if (infoEnabled) {
            provider.log(2, null, Level.INFO, null, null, message);
        }
    }

    public static void error(Throwable exception) {
        if (errorEnabled) {
            provider.log(2, null, Level.ERROR, exception, null, null);
        }
    }

}

Custom Logging Provider

All log entries, issued by any logging API of tinylog, are processed by a logging provider. Logging provider implementations can be, for example, a logging framework back-end or an adapter. A custom logging provider only needs to implement the interface LoggingProvider with its seven methods and provide a public default constructor without any arguments.

The example implementation below will output all log entries with severity level info and higher to System.out.

package org.tinylog.example;

import java.util.Locale;

import org.tinylog.Level;
import org.tinylog.provider.ContextProvider;
import org.tinylog.provider.LoggingProvider;
import org.tinylog.provider.MessageFormatter;
import org.tinylog.provider.NopContextProvider;

public class SystemOutLoggingProvider implements LoggingProvider {

    @Override
    public ContextProvider getContextProvider() {
        return new NopContextProvider();
    }

    @Override
    public Level getMinimumLevel() {
        return Level.INFO;
    }

    @Override
    public Level getMinimumLevel(String tag) {
        return Level.INFO;
    }

    @Override
    public boolean isEnabled(int depth, String tag, Level level) {
        return level.ordinal() >= Level.INFO.ordinal();
    }

    @Override
    public void log(int depth, String tag, Level level, Throwable exception, 
                    Object obj, Object... arguments) {
        log(exception, obj == null ? null : obj.toString(), arguments);
    }

    @Override
    public void log(String loggerClassName, String tag, Level level, Throwable exception,
                    Object obj, Object... arguments) {
        log(exception, obj == null ? null : obj.toString(), arguments);
    }

    @Override
    public void shutdown() {
        // Nothing to do
    }

    private void log(Throwable exception, String message, Object[] arguments) {
        StringBuilder builder = new StringBuilder();
        if (message != null) {
            builder.append(new MessageFormatter(Locale.ENGLISH).format(message, arguments));
        }
        if (exception != null) {
            if (builder.length() > 0) builder.append(": ");
            builder.append(exception);
        }
        System.out.println(builder);
    }

}

The first method getContextProvider() returns a provider for the thread-based mapped diagnostic context values. In the above example, a NopContextProvider is registered, which is part of tinylog’s API artifact and simply ignores all registered context values.

The method getMinimumLevel() returns the lowest supported severity level (regardless if tagged or not) and getMinimumLevel(String tag) returns the lowest supported severity level for each tag. Both methods are called only once. The returned values are cached and used for internal optimization. If a logging provider cannot guarantee a defined minimum lifetime severity level, Level.TRACE can be returned instead.

Almost all logging APIs have getters to check whether a defined severity level is currently enabled or disabled. These checks are routed to the isEnabled() method. Unlike the two getMinimumLevel() methods, return values are not cached.

There are two different log() methods. The only difference is that the first one accepts the depth of the caller in the stack trace (e.g., “1” if there is only one method between caller and this method in the stack trace), and the second method accepts the fully-qualified class name of the logger class instead. Which of the two method is called depends on the logging API. Therefor, both log() method must be supported.

Finally, there is a shutdown() method, which is called when the logging framework is shut down, as the method name suggests.

Logging Providers have to be registered as a service so that tinylog can find them at runtime. If it does not yet exist, create the directory META-INF/services and insert a text file with the name org.tinylog.provider.LoggingProvider. In this text file, all custom logging providers can be input line by line with their fully-qualified class names.

For the new custom logging provider, META-INF/services/org.tinylog.provider.LoggingProvider would look like this:

org.tinylog.example.SystemOutLoggingProvider

Now the logging provider is ready to be used. To use it in a configuration, the name of the logging provider is derived from the class name. Spaces are inserted between the words, and “Logging Provider” disappears at the end. Hence, SystemOutLoggingProvider becomes system out.

Configuration for activating the new custom logging provider in tinylog.properties:

provider = system out

Since tinylog 2.2, it is even possible to define multiple logging providers as a comma separated list:

provider = system out, tinylog

If no logging provider is configured, tinylog will forward log entries to all available logging providers.

Custom Throwable Filter

A custom throwable filter, for transforming exceptions and stack traces, simply has to implement the interface ThrowableFilter with its single method. A potential argument from the configuration is passed as a string. Therefore, every throwable filter has to provide a public constructor that accepts a string, even if it is not configurable.

The example below shows a simple non-configurable throwable filter that strips the package part from the exception class name and all class names in the stack trace.

package com.example;

import java.util.stream.Collectors;

import org.tinylog.throwable.ThrowableData;
import org.tinylog.throwable.ThrowableFilter;
import org.tinylog.throwable.ThrowableStore;

public class PackageStripThrowableFilter implements ThrowableFilter {

    public PackageStripThrowableFilter(String argument) {
    }

    @Override
    public ThrowableData filter(ThrowableData origin) {
        return new ThrowableStore(
                strip(origin.getClassName()),
                origin.getMessage(),
                origin.getStackTrace().stream().map(this::strip).collect(Collectors.toList()),
                origin.getCause() == null ? null : filter(origin.getCause())
        );
    }

    private StackTraceElement strip(StackTraceElement element) {
        return new StackTraceElement(
                strip(element.getClassName()),
                element.getMethodName(),
                element.getFileName(),
                element.getLineNumber()
        );
    }

    private String strip(String className) {
        int index = className.lastIndexOf('.');
        if (index < className.length() - 1 && index >= 0) {
            return className.substring(index + 1);
        } else {
            className;
        }
    }

}

Throwable filters have to be registered as a service so that tinylog can find them at runtime. If it does not yet exist, create the directory META-INF/services and insert a text file with the name org.tinylog.throwable.ThrowableFilter. In this text file, all custom throwable filters can be input line by line with their fully-qualified class names.

For the new custom throwable filter, META-INF/services/org.tinylog.throwable.ThrowableFilter would look like this:

com.example.PackageStripThrowableFilter

Now the throwable filter is ready to be used. To use it in a configuration, the name of the throwable filter is derived from the class name. Spaces are inserted between the words and “Throwable Filter” disappears at the end. Hence, PackageStripThrowableFilter becomes just package strip.

Configuration of the new custom throwable filter in tinylog.properties:

exception = package strip

Custom Writer

A custom writer simply has to implement the interface Writer with its four methods. Potential properties from the configuration are passed as Map<String, String>. Therefore, every writer has to provide a public constructor that accepts a string map. Kotlin developers must be careful to use java.util.Map and not kotlin.collections.Map by accident. Otherwise, tinylog cannot find the writer at runtime.

The example below shows a simple writer that just outputs all log entries via System.out.

package org.tinylog.example;

import java.util.Collection;
import java.util.EnumSet;
import java.util.Map;

import org.tinylog.core.LogEntry;
import org.tinylog.core.LogEntryValue;
import org.tinylog.writers.Writer;

public class SystemOutWriter implements Writer {

    private final String delimiter;

    public SystemOutWriter(Map<String, String> properties) {
        delimiter = properties.getOrDefault("delimiter", "-");
    }

    @Override
    public Collection<LogEntryValue> getRequiredLogEntryValues() {
        return EnumSet.of(LogEntryValue.LEVEL, LogEntryValue.MESSAGE);
    }

    @Override
    public void write(LogEntry logEntry) {
        System.out.println(logEntry.getLevel() + " " + delimiter + " " + logEntry.getMessage());
    }

    @Override
    public void flush() {
        System.out.flush();
    }

    @Override
    public void close() {
        // System.out doesn't have to be closed
    }

}

Writers have to be registered as a service so that tinylog can find them at runtime. If it does not yet exist, create the directory META-INF/services and insert a text file with the name org.tinylog.writers.Writer. In this text file, all custom writers can be input line by line with their fully-qualified class names.

For the new custom writer, META-INF/services/org.tinylog.writers.Writer would look like this:

org.tinylog.example.SystemOutWriter

Now the writer is ready to be used. To use it in a configuration, the name of the writer is derived from the class name. Spaces are inserted between the words, and “Writer” disappears at the end. Hence, SystemOutWriter becomes system out.

Configuration of the new custom writer in tinylog.properties:

writer           = system out
writer.delimiter = ::

Instead of only defining a delimiter for the output of log entries, the output should usually be completely freely configurable as a format pattern. The class AbstractFormatPatternWriter provides this functionality out of the box, and also computes all required log entry values automatically. This simplifies the implementation of the writer with support for format patterns.

package org.tinylog.example;

import java.util.Map;

import org.tinylog.core.LogEntry;
import org.tinylog.writers.AbstractFormatPatternWriter;

public class SystemOutWriter extends AbstractFormatPatternWriter {

    public SystemOutWriter(final Map<String, String> properties) {
        super(properties);
    }

    @Override
    public void write(LogEntry logEntry) {
        System.out.println(render(logEntry));
    }

    @Override
    public void flush() {
        System.out.flush();
    }

    @Override
    public void close() {
        // System.out doesn't have to be closed
    }

}
writer           = system out
writer.format    = {level} :: {message}

The class AbstractFormatPatternWriter also provides utility methods to write log entries to files. A good example is the FileWriter for understanding how to write log entries to files in a simple and safe way, by using the tinylog framework.

Custom Policy

A custom policy for the rolling file writer simply has to implement the interface Policy with its three methods. A potential argument from the configuration is passed as a string. Therefore, every policy has to provide a public constructor that accepts a string.

The example below shows a simple policy that randomly decides whether the current log file should be continued, or a new one started. The method continueExistingFile() is called to determine whether an already existing log file should be continued, and continueCurrentFile() to determine whether a new log entry can be still written to the current log file. After starting a new log file by any policy, the third method reset() is called.

package org.tinylog.example;

import java.util.Random;

import org.tinylog.policies.Policy;

public class RandomPolicy implements Policy {
    
    private final Random random;

    public RandomPolicy(String argument) {
        if (argument == null || argument.isEmpty()) {
            random = new Random();
        } else {
            random =  new Random(Long.parseLong(argument));
        }
    }

    @Override
    public boolean continueExistingFile(String path) {
        return random.nextBoolean();
    }

    @Override
    public boolean continueCurrentFile(byte[] entry) {
        return random.nextBoolean();
    }

    @Override
    public void reset() {
        // Nothing to do
    }

}

Just like writers, policies have to be registered as a service so that tinylog can find them at runtime. If it does not yet exist, create the directory META-INF/services and insert a text file with the name org.tinylog.policies.Policy. In this text file, all custom policies can be input line by line with their fully-qualified class names.

For the new custom policy, META-INF/services/org.tinylog.policies.Policy would look like this:

org.tinylog.example.RandomPolicy

Now the policy is ready to be used. To use it in a configuration, the name of the policy is derived from the class name. Spaces are inserted between the words and “Policy” disappears at the end. Hence, RandomPolicy becomes just random.

Configuration of the new custom policy in tinylog.properties:

writer           = rolling file
writer.file      = log.txt
writer.policies  = random: 42