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

codes.vps.logging.fluentd.jdk.FluentdHandler Maven / Gradle / Ivy

package codes.vps.logging.fluentd.jdk;

import codes.vps.logging.fluentd.jdk.util.ConsumerT;
import codes.vps.logging.fluentd.jdk.util.ForwardString;
import codes.vps.logging.fluentd.jdk.util.StringWinder;
import codes.vps.logging.fluentd.jdk.util.U;
import org.jetbrains.annotations.NotNull;
import org.komamitsu.fluency.EventTime;
import org.komamitsu.fluency.Fluency;
import org.komamitsu.fluency.fluentd.FluencyBuilderForFluentd;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Handler;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;

/**
 * JDK logging handler implementation for forwarding logging data
 * to Fluentd.
 */
@SuppressWarnings("unused")
public class FluentdHandler extends Handler {

    // this follows apache tomcat default format, but without a timestamp, and stack trace logged separately
    /**
     * Default logger format.
     */
    public final static String DEFAULT_FORMAT = "$tag\"\";message\"${level10n} [${tid}] ${class}.${method} ${l10n}\";stack\"${trace}\"";

    private Function> mapper;
    private List extractors;

    private Fluency logger;

    /**
     * Creates new handler from JDK logging configuration. This construction should only
     * be invoked by the JDK logging framework; otherwise you will need to populate the properties
     * needed by this handler into the JDK logger manager.
     */
    public FluentdHandler() {
        configure();
    }

    /**
     * Creates new handler from specified builder.
     * @param with builder with the information on how to create the handler.
     */
    public FluentdHandler(Builder with) {
        configure(with);
    }

    private void configure(Builder b) {

        if (b.extractors == null && b.mapper == null) {
            throw new NullPointerException("No extraction properties provided, specify extractors or mapper in the builder");
        }

        this.extractors = b.extractors;
        this.mapper = b.mapper;

        initLogger(b);

    }

    private void configure() {
        Builder b = new Builder();
        // our stuff
        cfg("tag_prefix", p->b.tagPrefix = p);
        cfg("host", p->b.host = p);
        cfg("port", p->b.port = p);
        cfg("format", p->b.extractors = parseFormat(p));

        FluencyBuilderForFluentd fb = b.fluencyBuilder;

        // fluency-fluentd
        iCfg("sender_max_retry_count", fb::setSenderMaxRetryCount);
        iCfg("sender_base_retry_interval_millis", fb::setSenderBaseRetryIntervalMillis);
        iCfg("sender_max_retry_interval_millis", fb::setSenderMaxRetryIntervalMillis);
        bCfg("ack_response_mode", fb::setAckResponseMode);
        bCfg("ssl_enabled", fb::setSslEnabled);
        iCfg("connection_timeout_milli", fb::setConnectionTimeoutMilli);
        iCfg("read_timeout_milli", fb::setReadTimeoutMilli);

        // fluency
        lCfg("max_buffer_size", fb::setMaxBufferSize);
        iCfg("buffer_chunk_initial_size", fb::setBufferChunkInitialSize);
        iCfg("buffer_chunk_retention_size", fb::setBufferChunkRetentionSize);
        iCfg("buffer_chunk_retention_time_millis", fb::setBufferChunkRetentionTimeMillis);
        iCfg("flush_attempt_interval_millis", fb::setFlushAttemptIntervalMillis);
        cfg("file_backup_dir", fb::setFileBackupDir);
        iCfg("wait_until_buffer_flushed", fb::setWaitUntilBufferFlushed);
        iCfg("wait_until_flusher_terminated", fb::setWaitUntilFlusherTerminated);
        bCfg("jvm_head_buffer_mode", fb::setJvmHeapBufferMode);

        configure(b);
    }

    private void cfg(String prop, @NotNull ConsumerT fun) {
        U.whenNotNull(getProperty(prop), fun);
    }

    private void iCfg(String prop, @NotNull ConsumerT fun) {
        U.whenNotNull(getProperty(prop), p->fun.accept(Integer.parseInt(p)));
    }

    @SuppressWarnings("SameParameterValue")
    private void lCfg(String prop, @NotNull ConsumerT fun) {
        U.whenNotNull(getProperty(prop), p->fun.accept(Long.parseLong(p)));
    }

    private void bCfg(String prop, @NotNull ConsumerT fun) {
        U.whenNotNull(getProperty(prop), p->fun.accept("true".equals(p)));
    }

    private void initLogger(Builder b) {

        FluencyBuilderForFluentd builder = new FluencyBuilderForFluentd();

        String [] hosts = b.getHost().split(",");
        String [] ports = b.getPort().split(",");

        if (hosts.length != ports.length) {
            throw new IllegalArgumentException("List of hosts must match list of ports");
        }

        if (hosts.length == 1) {

            logger = builder.build(hosts[0], Integer.parseInt(ports[0]));

        } else {

            List list = new ArrayList<>();
            for (int i=0; i result;
        if (mapper != null) {
            result = mapper.apply(record);
        } else {
            result = new HashMap<>();
            for (FieldExtractor f : extractors) {
                result.put(f.getFieldName(), f.extract(record));
            }
        }

        String tag = (String) result.remove("$tag");

        Long timestamp = U.ifNotNull(result.remove("$timestamp"), r->((Number)r).longValue(), null);

        if (tag == null) {
            tag = record.getLoggerName();
        }
        if (timestamp == null) {
            timestamp = record.getMillis();
        }

        try {
            logger.emit(tag, EventTime.fromEpochMilli(timestamp), result);
        } catch (IOException e) {
            throw U.doThrow(e);
        }

    }

    /**
     * Flushes logged messages.
     */
    public void flush() {
        U.reThrow(()->logger.flush());
    }

    /**
     * Closes the handler. Underlying fluentd connection is also closed.
     * Handler must not be used after this method is called.
     */
    public void close() {
        U.reThrow(()->logger.close());
    }

    /**
     * Parses format string to produce a list of field extractors
     * that will be used to populate the map.
     * See https://github.com/veselov/fluentd-jdk-handler/blob/master/README.md
     * for the information on this format.
     * @param s format string to translate to list of field extractors.
     * @return list of field extractors created based on the specified format.
     */
    public static List parseFormat(String s) {

        List items = new ArrayList<>();
        StringWinder sw = new ForwardString(s);
        boolean escape = false;
        StringBuilder buf = new StringBuilder();

        Consumer addOne = buf2-> {
            String item = U.sTrim(buf2.toString());
            if (item != null) {
                try {
                    items.add(new FieldExtractorImpl(item));
                } catch (Exception e) {
                    throw new IllegalArgumentException("Failed to parse format "+s, e);
                }
            }
        };

        // the top-level just cuts the string into individual FieldExtractors

        while (sw.hasNext()) {

            char c = sw.next();
            if (escape) {
                buf.append(c);
                escape = false;
                continue;
            }

            if (c == '\\') {
                escape = true;
                continue;
            }

            if (c == ';') {
                addOne.accept(buf);
                buf = new StringBuilder();
                continue;
            }

            buf.append(c);

        }

        if (buf.length() > 0) {
            addOne.accept(buf);
        }

        if (escape) {
            throw new IllegalArgumentException("String "+s+" terminated on escape character");
        }

        return items;

    }

    /**
     * Builder class used to provide configuration for the handler.
     * When a new build is created, it is populated with default values.
     * See https://github.com/veselov/fluentd-jdk-handler/blob/master/README.md for
     * list of default values not provided here.
     */
    @SuppressWarnings({"FieldMayBeFinal", "UnusedReturnValue"})
    public static class Builder {

        // builder is filled with default values. Fluency default values are based on
        // https://github.com/komamitsu/fluency (and from source code when needed)

        FluencyBuilderForFluentd fluencyBuilder = new FluencyBuilderForFluentd();

        private String host = "127.0.0.1";
        private String port = "24224";
        private String tagPrefix = "";
        private Function> mapper;
        private List extractors = parseFormat(DEFAULT_FORMAT);


        /**
         * Returns currently set tag prefix.
         * @return currently set tag prefix.
         */
        public String getTagPrefix() {
            return tagPrefix;
        }

        /**
         * Sets tag prefix. All messages will be prefixed with it, indiscriminately.
         * @param tagPrefix tag prefix to use.
         * @return this builder instance
         */
        public Builder setTagPrefix(String tagPrefix) {
            this.tagPrefix = tagPrefix;
            return this;
        }

        /**
         * Returns currently set host(s) to log messages to.
         * @return currently set host(s).
         */
        public String getHost() {
            return host;
        }

        /**
         * Sets host to send log messages to. To connect to multiple fluentd instances simultaneously,
         * specify comma-separated list. The list length must match value passed to
         * {@link #setPort(String)}
         * @param host host to use
         * @return this builder instance
         */
        public Builder setHost(String host) {
            this.host = host;
            return this;
        }

        /**
         * Returns current set port(s) to log messages to.
         * @return currently set port(s).
         */
        public String getPort() {
            return port;
        }

        /**
         * Sets port to send log messages to.  To connect to multiple fluentd instances
         * simultaneously, specify comma-separated list. The list length must match value passed to
         * {@link #setHost(String)}
         * @param port port(s) to use
         * @return this builder instance
         */
        public Builder setPort(String port) {
            this.port = port;
            return this;
        }

        /**
         * Returns currently set function to map log records to
         * outgoing message. See {@link #setMapper(Function)} for
         * more details.
         * @return currently set mapping function.
         */
        public Function> getMapper() {
            return mapper;
        }

        /**
         * Sets mapper function to use for mapping log records into
         * outgoing message. For contents of the map, please
         * see https://github.com/veselov/fluentd-jdk-handler/blob/master/README.md#formatting.
         * If mapper function is defined, extractors (default or set by {@link #setExtractors(List)}
         * are not used. Mapper function is invoked for every incoming log record,
         * and must produce a map. Except for {@code tag} and {@code timestamp} properties of
         * the map, its contents are forwarded to fluentd.
         * @param mapper mapper to use
         * @return this builder instance
         */
        public Builder setMapper(Function> mapper) {
            this.mapper = mapper;
            return this;
        }

        /**
         * Lists currently set extractors. See {@link #setExtractors(List)} for more details.
         * @return list currently set extractors used to create outgoing messages.
         */
        public List getExtractors() {
            return extractors;
        }

        /**
         * Sets list of extractors to use for sending out a message. Note that
         * extractors are not used if a non-null mapper is set with {@link #setMapper(Function)}.
         * Extractors are used to populate the object map that is then forwarded to
         * fluentd. See this
         * on how the map is interpreted. Each extractor must indicate which map property it populates,
         * and contain functionality that produces its value.
         * Default extractors are created using {@link #parseFormat(String)}. Caller can create
         * a function-based extractor using {@link FieldExtractorImpl#FieldExtractorImpl(String, Function)}.
         *
         * @param extractors extractors to use.
         * @return this builder instance
         */
        public Builder setExtractors(List extractors) {
            this.extractors = extractors;
            return this;
        }

        /**
         * Return underlying fluency fluentd builder. Configure this builder
         * to modify fluency specific parameters.
         * @return fluency builder
         */
        @NotNull
        public FluencyBuilderForFluentd getFluencyBuilder() {
            return fluencyBuilder;
        }

        public Builder() {
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy