GitHub

Extending

You can edit this page on GitHub

tinylog is extendable. It is possible to develop custom 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 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 shtut 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

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('.');
        return index < className.length() - 1 && index >= 0 ? className.substring(index + 1) : 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