All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.siegmar.logbackgelf.GelfEncoder Maven / Gradle / Ivy

Go to download

Logback appender for sending GELF messages with zero additional dependencies.

The newest version!
/*
 * Logback GELF - zero dependencies Logback GELF appender library.
 * Copyright (C) 2016 Oliver Siegmar
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package de.siegmar.logbackgelf;

import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;

import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.util.LevelToSyslogSeverity;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.Layout;
import ch.qos.logback.core.encoder.EncoderBase;
import de.siegmar.logbackgelf.mappers.CallerDataFieldMapper;
import de.siegmar.logbackgelf.mappers.KeyValueFieldMapper;
import de.siegmar.logbackgelf.mappers.MarkerFieldMapper;
import de.siegmar.logbackgelf.mappers.MdcDataFieldMapper;
import de.siegmar.logbackgelf.mappers.RootExceptionDataFieldMapper;
import de.siegmar.logbackgelf.mappers.SimpleFieldMapper;

/**
 * This class is responsible for transforming a Logback log event to a GELF message.
 */
@SuppressWarnings("checkstyle:classdataabstractioncoupling")
public class GelfEncoder extends EncoderBase {

    private static final Pattern VALID_ADDITIONAL_FIELD_PATTERN = Pattern.compile("^[\\w.-]*$");
    private static final String DEFAULT_SHORT_PATTERN = "%m%nopex";
    private static final String DEFAULT_FULL_PATTERN = "%m%n";

    /**
     * Origin hostname - will be auto-detected if not specified.
     */
    private String originHost;

    /**
     * If true, the raw message (with argument placeholders) will be sent, too. Default: false.
     */
    private boolean includeRawMessage;

    /**
     * If true, key value pairs will be sent, too. Default: true.
     */
    private boolean includeKeyValues = true;

    /**
     * If true, logback markers will be sent, too. Default: false.
     */
    private boolean includeMarker;

    /**
     * If true, MDC keys/values will be sent, too. Default: true.
     */
    private boolean includeMdcData = true;

    /**
     * If true, caller data (source file-, method-, class name and line) will be sent, too.
     * Default: false.
     */
    private boolean includeCallerData;

    /**
     * If true, root cause exception of the exception passed with the log message will be
     * exposed in the exception field. Default: false.
     */
    private boolean includeRootCauseData;

    /**
     * If true, the log level name (e.g. DEBUG) will be sent, too. Default: false.
     */
    private boolean includeLevelName;

    /**
     * The key that should be used for the levelName.
     */
    private String levelNameKey = "level_name";

    /**
     * The key that should be used for the loggerName.
     */
    private String loggerNameKey = "logger_name";

    /**
     * The key that should be used for the threadName.
     */
    private String threadNameKey = "thread_name";

    /**
     * If true, a system dependent newline separator will be added at the end of each message.
     * Don't use this in conjunction with TCP or UDP appenders, as this is only reasonable for
     * console logging!
     * Default: false.
     */
    private boolean appendNewline;

    /**
     * Short message format.
     * Default is a {@link PatternLayout} with {@value DEFAULT_SHORT_PATTERN}.
     */
    private Layout shortMessageLayout;

    /**
     * Full message format (Stacktrace).
     * Default is a {@link PatternLayout} with {@value DEFAULT_FULL_PATTERN}.
     */
    private Layout fullMessageLayout;

    /**
     * Log numbers as String. Default: false.
     */
    private boolean numbersAsString;

    /**
     * Additional, static fields to send to graylog. Defaults: none.
     */
    private final Map staticFields = new HashMap<>();

    private final List> builtInFieldMappers = new ArrayList<>();

    private final List> fieldMappers = new ArrayList<>();

    public String getOriginHost() {
        return originHost;
    }

    public void setOriginHost(final String originHost) {
        this.originHost = originHost;
    }

    public boolean isIncludeRawMessage() {
        return includeRawMessage;
    }

    public void setIncludeRawMessage(final boolean includeRawMessage) {
        this.includeRawMessage = includeRawMessage;
    }

    public boolean isIncludeKeyValues() {
        return includeKeyValues;
    }

    public void setIncludeKeyValues(final boolean includeKeyValues) {
        this.includeKeyValues = includeKeyValues;
    }

    public boolean isIncludeMarker() {
        return includeMarker;
    }

    public void setIncludeMarker(final boolean includeMarker) {
        this.includeMarker = includeMarker;
    }

    public boolean isIncludeMdcData() {
        return includeMdcData;
    }

    public void setIncludeMdcData(final boolean includeMdcData) {
        this.includeMdcData = includeMdcData;
    }

    public boolean isIncludeCallerData() {
        return includeCallerData;
    }

    public void setIncludeCallerData(final boolean includeCallerData) {
        this.includeCallerData = includeCallerData;
    }

    public boolean isIncludeRootCauseData() {
        return includeRootCauseData;
    }

    public void setIncludeRootCauseData(final boolean includeRootCauseData) {
        this.includeRootCauseData = includeRootCauseData;
    }

    public boolean isIncludeLevelName() {
        return includeLevelName;
    }

    public void setIncludeLevelName(final boolean includeLevelName) {
        this.includeLevelName = includeLevelName;
    }

    public String getLevelNameKey() {
        return levelNameKey;
    }

    public void setLevelNameKey(final String levelNameKey) {
        this.levelNameKey = levelNameKey;
    }

    public String getLoggerNameKey() {
        return loggerNameKey;
    }

    public void setLoggerNameKey(final String loggerNameKey) {
        this.loggerNameKey = loggerNameKey;
    }

    public String getThreadNameKey() {
        return threadNameKey;
    }

    public void setThreadNameKey(final String threadNameKey) {
        this.threadNameKey = threadNameKey;
    }

    public boolean isAppendNewline() {
        return appendNewline;
    }

    public void setAppendNewline(final boolean appendNewline) {
        this.appendNewline = appendNewline;
    }

    public boolean isNumbersAsString() {
        return numbersAsString;
    }

    public void setNumbersAsString(final boolean numbersAsString) {
        this.numbersAsString = numbersAsString;
    }

    public Layout getShortMessageLayout() {
        return shortMessageLayout;
    }

    public void setShortMessageLayout(final Layout shortMessageLayout) {
        this.shortMessageLayout = shortMessageLayout;
    }

    public Layout getFullMessageLayout() {
        return fullMessageLayout;
    }

    public void setFullMessageLayout(final Layout fullMessageLayout) {
        this.fullMessageLayout = fullMessageLayout;
    }

    public Map getStaticFields() {
        return Collections.unmodifiableMap(staticFields);
    }

    public void addStaticField(final String key, final Object value) {
        try {
            addField(staticFields, key, value);
        } catch (final IllegalArgumentException e) {
            addWarn("Could not add field " + key, e);
        }
    }

    @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
    public void addStaticField(final String staticField) {
        final String[] split = staticField.split(":", 2);
        if (split.length != 2) {
            addWarn("staticField must be in format key:value - rejecting '" + staticField + "'");
            return;
        }

        addStaticField(split[0].trim(), split[1].trim());
    }

    public List> getFieldMappers() {
        return Collections.unmodifiableList(fieldMappers);
    }

    public void addFieldMapper(final GelfFieldMapper fieldMapper) {
        fieldMappers.add(fieldMapper);
    }

    private void addField(final Map dst, final String fieldName, final Object fieldValue) {
        if (fieldName.isEmpty()) {
            throw new IllegalArgumentException("fieldName key must not be empty");
        }
        if ("id".equalsIgnoreCase(fieldName)) {
            throw new IllegalArgumentException("fieldName key name 'id' is prohibited");
        }
        if (!VALID_ADDITIONAL_FIELD_PATTERN.matcher(fieldName).matches()) {
            throw new IllegalArgumentException("fieldName key '" + fieldName + "' is illegal. "
                + "Keys must apply to regex " + VALID_ADDITIONAL_FIELD_PATTERN);
        }

        final Object oldValue = dst.putIfAbsent(fieldName, convertToNumberIfNeeded(fieldValue));
        if (oldValue != null) {
            throw new IllegalArgumentException("Field mapper tried to set already defined key '" + fieldName + "'.");
        }
    }

    @SuppressWarnings("checkstyle:ReturnCount")
    private Object convertToNumberIfNeeded(final Object value) {
        if (numbersAsString || !(value instanceof String)) {
            return value;
        }

        // Simple check if the string could be a number to avoid the performance overhead of exception handling
        final char[] ca = ((String) value).toCharArray();
        for (char c : ca) {
            if (!isBigDecimalChar(c)) {
                return value;
            }
        }

        try {
            return new BigDecimal(ca, 0, ca.length);
        } catch (final NumberFormatException e) {
            return value;
        }
    }

    @SuppressWarnings("checkstyle:BooleanExpressionComplexity")
    private static boolean isBigDecimalChar(final char c) {
        return c >= '0' && c <= '9'
               || c == '.'
               || c == '+' || c == '-'
               || c == 'E' || c == 'e';
    }

    @Override
    public void start() {
        if (originHost == null || originHost.isBlank()) {
            originHost = Optional.ofNullable(context.getProperty(CoreConstants.HOSTNAME_KEY)).orElse("unknown");
        }
        if (shortMessageLayout == null) {
            shortMessageLayout = buildPattern(DEFAULT_SHORT_PATTERN);
        }
        if (fullMessageLayout == null) {
            fullMessageLayout = buildPattern(DEFAULT_FULL_PATTERN);
        }
        addBuiltInFieldMappers();

        super.start();
    }

    private PatternLayout buildPattern(final String pattern) {
        final PatternLayout patternLayout = new PatternLayout();
        patternLayout.setContext(getContext());
        patternLayout.setPattern(pattern);
        patternLayout.start();
        return patternLayout;
    }

    private void addBuiltInFieldMappers() {
        builtInFieldMappers.add(new SimpleFieldMapper<>(loggerNameKey, ILoggingEvent::getLoggerName));
        builtInFieldMappers.add(new SimpleFieldMapper<>(threadNameKey, ILoggingEvent::getThreadName));

        if (includeLevelName) {
            builtInFieldMappers.add(new SimpleFieldMapper<>(levelNameKey, event -> event.getLevel().toString()));
        }

        if (includeRawMessage) {
            builtInFieldMappers.add(new SimpleFieldMapper<>("raw_message", ILoggingEvent::getMessage));
        }

        if (includeCallerData) {
            builtInFieldMappers.add(new CallerDataFieldMapper());
        }

        if (includeRootCauseData) {
            builtInFieldMappers.add(new RootExceptionDataFieldMapper());
        }

        if (includeKeyValues) {
            builtInFieldMappers.add(new KeyValueFieldMapper());
        }

        if (includeMarker) {
            builtInFieldMappers.add(new MarkerFieldMapper("marker"));
        }

        if (includeMdcData) {
            builtInFieldMappers.add(new MdcDataFieldMapper());
        }
    }

    @SuppressWarnings({"PMD.ReturnEmptyArrayRatherThanNull", "PMD.ReturnEmptyCollectionRatherThanNull"})
    @Override
    public byte[] headerBytes() {
        return null;
    }

    @Override
    public byte[] encode(final ILoggingEvent event) {
        final GelfMessage gelfMessage = buildGelfMessage(
            event.getTimeStamp(),
            LevelToSyslogSeverity.convert(event),
            normalizeShortMessage(buildShortMessage(event)),
            buildFullMessage(event),
            collectAdditionalFields(event)
        );

        final var sb = gelfMessage.toJSON();

        if (appendNewline) {
            sb.append(System.lineSeparator());
        }

        return sb.toString().getBytes(StandardCharsets.UTF_8);
    }

    protected GelfMessage buildGelfMessage(final long timestamp, final int logLevel, final String shortMessage,
                                           final String fullMessage, final Map additionalFields) {
        return new GelfMessage(originHost, shortMessage, fullMessage, timestamp, logLevel, additionalFields);
    }

    protected String normalizeShortMessage(final String shortMessage) {
        // Short message is mandatory per GELF spec
        // Graylog doesn't like a single newline as short message: https://github.com/Graylog2/graylog2-server/issues/4842
        if (shortMessage.isBlank()) {
            addWarn("Log message was blank - replaced to prevent Graylog error");
            return "Blank message replaced by logback-gelf";
        }

        return shortMessage;
    }

    protected String buildShortMessage(final ILoggingEvent event) {
        return shortMessageLayout.doLayout(event);
    }

    protected String buildFullMessage(final ILoggingEvent event) {
        return fullMessageLayout.doLayout(event);
    }

    protected Map collectAdditionalFields(final ILoggingEvent event) {
        final Map additionalFields = new HashMap<>(staticFields);
        addFieldMapperData(event, additionalFields, builtInFieldMappers);
        addFieldMapperData(event, additionalFields, fieldMappers);
        return additionalFields;
    }

    @SuppressWarnings("checkstyle:IllegalCatch")
    private void addFieldMapperData(final ILoggingEvent event, final Map additionalFields,
                                    final List> mappers) {
        for (final GelfFieldMapper fieldMapper : mappers) {
            try {
                fieldMapper.mapField(event, (key, value) -> {
                    try {
                        addField(additionalFields, key, value);
                    } catch (final IllegalArgumentException e) {
                        addWarn("Could not add field " + key, e);
                    }
                });
            } catch (final Exception e) {
                addError("Exception in field mapper", e);
            }
        }
    }

    @SuppressWarnings({"PMD.ReturnEmptyArrayRatherThanNull", "PMD.ReturnEmptyCollectionRatherThanNull"})
    @Override
    public byte[] footerBytes() {
        return null;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy