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

org.graylog2.log4j2.GelfAppender Maven / Gradle / Ivy

The newest version!
package org.graylog2.log4j2;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.appender.AppenderLoggingException;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.net.Severity;
import org.apache.logging.log4j.core.util.KeyValuePair;
import org.apache.logging.log4j.status.StatusLogger;
import org.graylog2.gelfclient.GelfConfiguration;
import org.graylog2.gelfclient.GelfMessage;
import org.graylog2.gelfclient.GelfMessageBuilder;
import org.graylog2.gelfclient.GelfMessageLevel;
import org.graylog2.gelfclient.GelfTransports;
import org.graylog2.gelfclient.transport.GelfTransport;

import java.io.File;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import static java.util.Objects.requireNonNull;

@Plugin(name = "GELF", category = "Core", elementType = "appender", printObject = true)
public class GelfAppender extends AbstractAppender {
    private static final long serialVersionUID = 4796033328540158817L;

    private static Pattern IPV4_PATTERN = Pattern.compile("[:0-9a-f]*(\\d{1,3}\\.){3}(\\d{1,3})");
    private static Pattern IPV6_PATTERN = Pattern.compile("([0-9a-f]{1,4}:){7}([0-9a-f]){1,4}");

    private static final Logger LOG = StatusLogger.getLogger();

    private final GelfConfiguration gelfConfiguration;
    private final String hostName;
    private final boolean includeSource;
    private final boolean includeThreadContext;
    private final boolean includeStackTrace;
    private final boolean includeExceptionCause;
    private final Map additionalFields;

    private GelfTransport client;

    protected GelfAppender(final String name,
                           final Layout layout,
                           final Filter filter,
                           final boolean ignoreExceptions,
                           final GelfConfiguration gelfConfiguration,
                           final String hostName,
                           final boolean includeSource,
                           final boolean includeThreadContext,
                           final boolean includeStackTrace,
                           final KeyValuePair[] additionalFields,
                           final boolean includeExceptionCause) {
        super(name, filter, layout, ignoreExceptions);
        this.gelfConfiguration = gelfConfiguration;
        this.hostName = hostName;
        this.includeSource = includeSource;
        this.includeThreadContext = includeThreadContext;
        this.includeStackTrace = includeStackTrace;
        this.includeExceptionCause = includeExceptionCause;

        if (null != additionalFields) {
            this.additionalFields = new HashMap<>();
            for (KeyValuePair pair : additionalFields) {
                this.additionalFields.put(pair.getKey(), pair.getValue());
            }
        } else {
            this.additionalFields = Collections.emptyMap();
        }
    }

    @Override
    public void append(LogEvent event) {
        final Layout layout = getLayout();
        final String formattedMessage;
        if (layout == null) {
            formattedMessage = event.getMessage().getFormattedMessage();
        } else {
            formattedMessage = new String(layout.toByteArray(event), StandardCharsets.UTF_8);
        }

        final GelfMessageBuilder builder = new GelfMessageBuilder(formattedMessage, hostName)
                .timestamp(event.getTimeMillis() / 1000d)
                .level(GelfMessageLevel.fromNumericLevel(Severity.getSeverity(event.getLevel()).getCode()))
                .additionalField("loggerName", event.getLoggerName())
                .additionalField("threadName", event.getThreadName());

        final Marker marker = event.getMarker();
        if (marker != null) {
            builder.additionalField("marker", marker.getName());
        }

        if (includeThreadContext) {
            for (Map.Entry entry : event.getContextMap().entrySet()) {
                builder.additionalField(entry.getKey(), entry.getValue());
            }

            // Guard against https://issues.apache.org/jira/browse/LOG4J2-1530
            final ThreadContext.ContextStack contextStack = event.getContextStack();
            if (contextStack != null) {
                final List contextStackItems = contextStack.asList();
                if (contextStackItems != null && !contextStackItems.isEmpty()) {
                    builder.additionalField("contextStack", contextStackItems.toString());
                }
            }
        }

        if (includeSource) {
            final StackTraceElement source = event.getSource();
            if (source != null) {
                builder.additionalField("sourceFileName", source.getFileName());
                builder.additionalField("sourceMethodName", source.getMethodName());
                builder.additionalField("sourceClassName", source.getClassName());
                builder.additionalField("sourceLineNumber", source.getLineNumber());
            }
        }

        @SuppressWarnings("all")
        final Throwable thrown = event.getThrown();
        if (includeStackTrace && thrown != null) {
            String stackTrace;
            if (includeExceptionCause) {
                final StringWriter stringWriter = new StringWriter();
                final PrintWriter printWriter = new PrintWriter(stringWriter);
                thrown.printStackTrace(printWriter);
                stackTrace = stringWriter.toString();
            } else {
                stackTrace = getSimpleStacktraceAsString(thrown);
            }

            builder.additionalField("exceptionClass", thrown.getClass().getCanonicalName());
            builder.additionalField("exceptionMessage", thrown.getMessage());
            builder.additionalField("exceptionStackTrace", stackTrace);

            builder.fullMessage(formattedMessage);
        }

        if (!additionalFields.isEmpty()) {
            builder.additionalFields(additionalFields);
        }

        final GelfMessage gelfMessage = builder.build();
        try {
            final boolean sent = client.trySend(gelfMessage);
            if (!sent) {
                LOG.debug("Couldn't send message: {}", gelfMessage);
            }
        } catch (Exception e) {
            throw new AppenderLoggingException("failed to write log event to GELF server: " + e.getMessage(), e);
        }
    }

    protected String getSimpleStacktraceAsString(final Throwable thrown) {
        final StringBuilder stackTraceBuilder = new StringBuilder();
        for (StackTraceElement stackTraceElement : thrown.getStackTrace()) {
            new Formatter(stackTraceBuilder).format("%s.%s(%s:%d)%n",
                    stackTraceElement.getClassName(),
                    stackTraceElement.getMethodName(),
                    stackTraceElement.getFileName(),
                    stackTraceElement.getLineNumber());
        }
        return stackTraceBuilder.toString();
    }

    protected void setClient(GelfTransport client) {
        this.client = requireNonNull(client);
    }

    @Override
    public void start() {
        super.start();
        client = GelfTransports.create(gelfConfiguration);
    }

    @Override
    public void stop() {
        super.stop();
        if (client != null) {
            client.stop();
        }
    }

    @Override
    public String toString() {
        return GelfAppender.class.getSimpleName() + "{"
                + "name=" + getName()
                + ",server=" + gelfConfiguration.getRemoteAddress().getHostName()
                + ",port=" + gelfConfiguration.getRemoteAddress().getPort()
                + ",protocol=" + gelfConfiguration.getTransport().toString()
                + ",hostName=" + hostName
                + ",queueSize=" + gelfConfiguration.getQueueSize()
                + ",connectTimeout=" + gelfConfiguration.getConnectTimeout()
                + ",reconnectDelay=" + gelfConfiguration.getReconnectDelay()
                + ",sendBufferSize=" + gelfConfiguration.getSendBufferSize()
                + ",tcpNoDelay=" + gelfConfiguration.isTcpNoDelay()
                + ",tcpKeepAlive=" + gelfConfiguration.isTcpKeepAlive()
                + ",tlsEnabled=" + gelfConfiguration.isTlsEnabled()
                + ",tlsCertVerificationEnabled=" + gelfConfiguration.isTlsCertVerificationEnabled()
                + ",tlsTrustCertChainFilename=" + (gelfConfiguration.getTlsTrustCertChainFile() != null ?
                gelfConfiguration.getTlsTrustCertChainFile().getPath() : "null")
                + "}";
    }

    /**
     * Factory method for creating a {@link GelfTransport} provider within the plugin manager.
     *
     * @param name                             The name of the Appender.
     * @param filter                           A Filter to determine if the event should be handled by this Appender.
     * @param layout                           The Layout to use to format the LogEvent defaults to {@code "%m%n"}.
     * @param ignoreExceptions                 The default is {@code true}, causing exceptions encountered while appending events
     *                                         to be internally logged and then ignored. When set to {@code false} exceptions will
     *                                         be propagated to the caller, instead. Must be set to {@code false} when wrapping this
     *                                         Appender in a {@link org.apache.logging.log4j.core.appender.FailoverAppender}.
     * @param server                           The server name of the GELF server, defaults to {@code localhost}.
     * @param port                             The port the GELF server is listening on, defaults to {@code 12201}.
     * @param hostName                         The host name of the machine generating the logs, defaults to local host name
     *                                         or {@code localhost} if it couldn't be detected.
     * @param protocol                         The transport protocol to use, defaults to {@code UDP}.
     * @param tlsEnabled                       Whether TLS should be enabled, defaults to {@code false}.
     * @param tlsEnableCertificateVerification Whether TLS certificate chain should be checked, defaults to {@code true}.
     * @param tlsTrustCertChainFilename        A X.509 certificate chain file in PEM format for certificate verification, defaults to {@code null}
     * @param queueSize                        The size of the internally used queue, defaults to {@code 512}.
     * @param connectTimeout                   The connection timeout for TCP connections in milliseconds, defaults to {@code 1000}.
     * @param reconnectDelay                   The time to wait between reconnects in milliseconds, defaults to {@code 500}.
     * @param sendBufferSize                   The size of the socket send buffer in bytes, defaults to {@code -1} (deactivate).
     * @param tcpNoDelay                       Whether Nagle's algorithm should be used for TCP connections, defaults to {@code false}.
     * @param tcpKeepAlive                     Whether to try keeping alive TCP connections, defaults to {@code false}.
     * @param includeSource                    Whether the source of the log message should be included, defaults to {@code true}.
     * @param includeThreadContext             Whether the contents of the {@link org.apache.logging.log4j.ThreadContext} should be included, defaults to {@code true}.
     * @param includeStackTrace                Whether a full stack trace should be included, defaults to {@code true}.
     * @param includeExceptionCause            Whether the included stack trace should contain causing exceptions, defaults to {@code false}.
     * @param additionalFields                 Additional static key=value pairs that will be added to every log message.
     * @return a new GELF provider
     */
    @PluginFactory
    @SuppressWarnings("unused")
    public static GelfAppender createGelfAppender(@PluginElement("Filter") Filter filter,
                                                  @PluginElement("Layout") Layout layout,
                                                  @PluginElement(value = "AdditionalFields") final KeyValuePair[] additionalFields,
                                                  @PluginAttribute(value = "name") String name,
                                                  @PluginAttribute(value = "ignoreExceptions", defaultBoolean = true) Boolean ignoreExceptions,
                                                  @PluginAttribute(value = "server", defaultString = "localhost") String server,
                                                  @PluginAttribute(value = "port", defaultInt = 12201) Integer port,
                                                  @PluginAttribute(value = "protocol", defaultString = "UDP") String protocol,
                                                  @PluginAttribute(value = "hostName") String hostName,
                                                  @PluginAttribute(value = "queueSize", defaultInt = 512) Integer queueSize,
                                                  @PluginAttribute(value = "connectTimeout", defaultInt = 1000) Integer connectTimeout,
                                                  @PluginAttribute(value = "reconnectDelay", defaultInt = 500) Integer reconnectDelay,
                                                  @PluginAttribute(value = "sendBufferSize", defaultInt = -1) Integer sendBufferSize,
                                                  @PluginAttribute(value = "tcpNoDelay", defaultBoolean = false) Boolean tcpNoDelay,
                                                  @PluginAttribute(value = "tcpKeepAlive", defaultBoolean = false) Boolean tcpKeepAlive,
                                                  @PluginAttribute(value = "includeSource", defaultBoolean = true) Boolean includeSource,
                                                  @PluginAttribute(value = "includeThreadContext", defaultBoolean = true) Boolean includeThreadContext,
                                                  @PluginAttribute(value = "includeStackTrace", defaultBoolean = true) Boolean includeStackTrace,
                                                  @PluginAttribute(value = "includeExceptionCause", defaultBoolean = false) Boolean includeExceptionCause,
                                                  @PluginAttribute(value = "tlsEnabled", defaultBoolean = false) Boolean tlsEnabled,
                                                  @PluginAttribute(value = "tlsEnableCertificateVerification", defaultBoolean = true) Boolean tlsEnableCertificateVerification,
                                                  @PluginAttribute(value = "tlsTrustCertChainFilename") String tlsTrustCertChainFilename) {
        if (name == null) {
            LOGGER.error("No name provided for ConsoleAppender");
            return null;
        }

        if (!"UDP".equalsIgnoreCase(protocol) && !"TCP".equalsIgnoreCase(protocol)) {
            LOG.warn("Invalid protocol {}, falling back to UDP", protocol);
            protocol = "UDP";
        }
        if (hostName == null || hostName.trim().isEmpty()) {
            try {
                final String canonicalHostName = InetAddress.getLocalHost().getCanonicalHostName();
                if (isFQDN(canonicalHostName)) {
                    hostName = canonicalHostName;
                } else {
                    hostName = InetAddress.getLocalHost().getHostName();
                }
            } catch (UnknownHostException e) {
                LOG.warn("Couldn't detect local host name, falling back to \"localhost\"");
                hostName = "localhost";
            }
        }

        final InetSocketAddress serverAddress = new InetSocketAddress(server, port);
        final GelfTransports gelfProtocol = GelfTransports.valueOf(protocol.toUpperCase());
        final GelfConfiguration gelfConfiguration = new GelfConfiguration(serverAddress)
                .transport(gelfProtocol)
                .queueSize(queueSize)
                .connectTimeout(connectTimeout)
                .reconnectDelay(reconnectDelay)
                .sendBufferSize(sendBufferSize)
                .tcpNoDelay(tcpNoDelay)
                .tcpKeepAlive(tcpKeepAlive);

        if (tlsEnabled) {
            if (gelfProtocol.equals(GelfTransports.TCP)) {
                gelfConfiguration.enableTls();
                if (!tlsEnableCertificateVerification) {
                    LOG.warn("TLS certificate validation is disabled. This is unsecure!");
                    gelfConfiguration.disableTlsCertVerification();
                }
                if (tlsEnableCertificateVerification && tlsTrustCertChainFilename != null) {
                    gelfConfiguration.tlsTrustCertChainFile(new File(tlsTrustCertChainFilename));
                }
            } else {
                LOG.warn("Enabling of TLS is invalid for UDP Transport");
            }
        }

        return new GelfAppender(name, layout, filter, ignoreExceptions, gelfConfiguration, hostName, includeSource,
                includeThreadContext, includeStackTrace, additionalFields, includeExceptionCause);
    }

    static boolean isFQDN(String canonicalHostName) {
        return canonicalHostName.contains(".") &&
                !IPV4_PATTERN.matcher(canonicalHostName).matches() &&
                !IPV6_PATTERN.matcher(canonicalHostName).matches();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy