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

org.appenders.log4j2.elasticsearch.ahc.BatchingClientObjectFactory Maven / Gradle / Ivy

Go to download

Log4j2 Appender plugin pushing logs in batches to Elasticsearch (2.x/5.x/6.x/7.x/8.x) clusters

The newest version!
package org.appenders.log4j2.elasticsearch.ahc;

/*-
 * #%L
 * log4j2-elasticsearch
 * %%
 * Copyright (C) 2022 Rafal Foltynski
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import org.appenders.log4j2.elasticsearch.ClientObjectFactory;
import org.appenders.log4j2.elasticsearch.ClientProvider;
import org.appenders.log4j2.elasticsearch.FailoverPolicy;
import org.appenders.log4j2.elasticsearch.LifeCycle;
import org.appenders.log4j2.elasticsearch.Operation;
import org.appenders.log4j2.elasticsearch.backoff.BackoffPolicy;
import org.appenders.log4j2.elasticsearch.backoff.NoopBackoffPolicy;
import org.appenders.log4j2.elasticsearch.failover.FailedItemOps;
import org.appenders.log4j2.elasticsearch.metrics.DefaultMetricsFactory;
import org.appenders.log4j2.elasticsearch.metrics.Measured;
import org.appenders.log4j2.elasticsearch.metrics.Metric;
import org.appenders.log4j2.elasticsearch.metrics.MetricConfig;
import org.appenders.log4j2.elasticsearch.metrics.MetricConfigFactory;
import org.appenders.log4j2.elasticsearch.metrics.Metrics;
import org.appenders.log4j2.elasticsearch.metrics.MetricsFactory;
import org.appenders.log4j2.elasticsearch.metrics.MetricsRegistry;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;

import static org.appenders.core.logging.InternalLogging.getLogger;

public abstract class BatchingClientObjectFactory, ITEM_TYPE extends Item>
        implements ClientObjectFactory, Measured {

    private volatile State state = State.STOPPED;

    protected final HttpClientProvider clientProvider;
    protected final FailedItemOps failedItemOps;
    protected final BackoffPolicy backoffPolicy;

    protected final BatchingClientMetrics metrics;

    private final ConcurrentLinkedQueue operations = new ConcurrentLinkedQueue<>();

    public BatchingClientObjectFactory(final BatchingClientObjectFactory.Builder builder) {
        this.clientProvider = builder.clientProvider;
        this.failedItemOps = builder.failedItemOps;
        this.backoffPolicy = builder.backoffPolicy;
        this.metrics = new BatchingClientMetrics(builder.name, builder.metricsFactory);
    }

    @Override
    public Function createFailureHandler(final FailoverPolicy failover) {
        return batchRequest -> {

            final long start = System.currentTimeMillis();
            metrics.batchFailed();

            final Collection items = batchRequest.getItems();

            final int batchSize = batchRequest.size();
            metrics.itemsFailed(batchSize);
            getLogger().warn("Batch of {} items failed. Redirecting to {}", batchSize, failover.getClass().getName());

            items.forEach(batchItem -> {
                // TODO: FailoverPolicyChain
                try {
                    failover.deliver(failedItemOps.createItem(batchItem));
                } catch (Exception e) {
                    // let's handle here as exception thrown at this stage will cause the client to shutdown
                    getLogger().error(e.getMessage(), e);
                }
            });

            metrics.failoverTookMs(System.currentTimeMillis() - start);

            return true;
        };
    }

    protected abstract ResponseHandler createResultHandler(BATCH_TYPE request, Function failureHandler);

    @Override
    public Collection getServerList() {
        return new ArrayList<>(clientProvider.getHttpClientFactoryBuilder().serverList);
    }

    @Override
    public HttpClient createClient() {
        return clientProvider.createClient();
    }

    @Override
    public Function createBatchListener(final FailoverPolicy failoverPolicy) {
        return new Function() {

            private final Function failureHandler = createFailureHandler(failoverPolicy);

            @Override
            public Boolean apply(final BATCH_TYPE request) {

                // FIXME: Wrap in a queue of some sort.. BatchPhaseQueue?
                //        The goal is to have: beforeBatchQueue().executeAll() or queue.beforeBatch().execute() or similar
                //        This should pave the way for before/on/afterBatch style handling
                while (!operations.isEmpty()) {
                    try {
                        operations.remove().execute();
                    } catch (Exception e) {
                        // TODO: redirect to failover (?) retry with exp. backoff (?) multiple options here
                        getLogger().error("before-batch failed: " + e.getMessage(), e);
                    }
                }

                if (backoffPolicy.shouldApply(request)) {

                    getLogger().warn("Backoff applied. Batch of {} items rejected", request.size());
                    metrics.backoffApplied(1);

                    failureHandler.apply(request);
                    request.completed();

                    return false;

                } else {
                    backoffPolicy.register(request);
                }

                final ResponseHandler responseHandler = createResultHandler(request, failureHandler);
                // FIXME: Batch interface shouldn't extend Request!
                createClient().executeAsync(request, responseHandler);

                metrics.itemsSent(request.size());

                return true;
            }

        };
    }

    @Override
    public void addOperation(final Operation operation) {
        operations.add(operation);
    }

    public static abstract class Builder, ITEM_TYPE extends Item> {

        private static final AtomicInteger counter = new AtomicInteger();

        private String name = BatchingClientMetrics.class.getSimpleName() + "-" + counter.getAndIncrement();

        protected HttpClientProvider clientProvider = new HttpClientProvider(new HttpClientFactory.Builder());
        protected BackoffPolicy backoffPolicy = new NoopBackoffPolicy<>();
        protected FailedItemOps failedItemOps;
        protected final MetricsFactory metricsFactory = new DefaultMetricsFactory(BatchingClientMetrics.metricConfigs(false));

        public abstract BatchingClientObjectFactory build();

        protected Builder validate() {

            if (clientProvider == null) {
                throw new IllegalArgumentException(nullValidationExceptionMessage(ClientProvider.class.getSimpleName()));
            }

            if (backoffPolicy == null) {
                throw new IllegalArgumentException(nullValidationExceptionMessage(BackoffPolicy.class.getSimpleName()));
            }

            if (failedItemOps == null) {
                failedItemOps = createFailedItemOps();
            }

            return this;

        }

        protected abstract FailedItemOps createFailedItemOps();

        private String nullValidationExceptionMessage(final String className) {
            return String.format("No %s provided for %s", className, AHCHttp.class.getSimpleName());
        }

        public Builder withName(final String name) {
            this.name = name;
            return this;
        }

        public Builder withClientProvider(final HttpClientProvider clientProvider) {
            this.clientProvider = clientProvider;
            return this;
        }

        public Builder withBackoffPolicy(final BackoffPolicy backoffPolicy) {
            this.backoffPolicy = backoffPolicy;
            return this;
        }

        public Builder withFailedItemOps(final FailedItemOps failedItemOps) {
            this.failedItemOps = failedItemOps;
            return this;
        }

        public Builder withMetricConfig(final MetricConfig metricConfig) {
            this.metricsFactory.configure(metricConfig);
            return this;
        }

        public Builder withMetricConfigs(final List metricConfigs) {
            this.metricsFactory.configure(metricConfigs);
            return this;
        }

    }

    @Override
    public final void start() {

        if (isStarted()) {
            return;
        }

        addOperation(() -> LifeCycle.of(clientProvider).start());

        startExtensions();

        state = State.STARTED;

    }

    @Override
    public final void stop() {

        if (isStopped()) {
            return;
        }

        getLogger().debug("Stopping {}", getClass().getSimpleName());

        stopExtensions();

        LifeCycle.of(clientProvider).stop();

        state = State.STOPPED;

        getLogger().debug("{} stopped", getClass().getSimpleName());

    }

    @Override
    public final boolean isStarted() {
        return state == State.STARTED;
    }

    @Override
    public final boolean isStopped() {
        return state == State.STOPPED;
    }

    @Override
    public void register(final MetricsRegistry registry) {
        metrics.register(registry);
        addOperation(() -> Measured.of(clientProvider).register(registry));
        addOperation(() -> Measured.of(clientProvider.getHttpClientFactoryBuilder().serviceDiscovery).register(registry));
    }

    @Override
    public void deregister() {
        metrics.deregister();
        Measured.of(clientProvider).deregister();
        Measured.of(clientProvider.getHttpClientFactoryBuilder().serviceDiscovery).deregister();
    }

    public static class BatchingClientMetrics implements Metrics {

        private final List registrations = new ArrayList<>();;
        private final Metric serverTookMs;
        private final Metric itemsSent;
        private final Metric itemsDelivered;
        private final Metric itemsFailed;
        private final Metric backoffApplied;
        private final Metric batchesFailed;
        private final Metric failoverTookMs;

        public BatchingClientMetrics(final String name, final MetricsFactory factory) {
            this.serverTookMs = factory.createMetric(name, "serverTookMs");
            this.itemsSent = factory.createMetric(name, "itemsSent");
            this.itemsDelivered = factory.createMetric(name, "itemsDelivered");
            this.itemsFailed = factory.createMetric(name, "itemsFailed");
            this.backoffApplied = factory.createMetric(name, "backoffApplied");
            this.batchesFailed = factory.createMetric(name, "batchesFailed");
            this.failoverTookMs = factory.createMetric(name, "failoverTookMs");
        }

        public static List metricConfigs(final boolean enabled) {
            return Collections.unmodifiableList(Arrays.asList(
                    MetricConfigFactory.createMaxConfig(enabled, "serverTookMs", true),
                    MetricConfigFactory.createCountConfig(enabled, "itemsSent"),
                    MetricConfigFactory.createCountConfig(enabled, "itemsDelivered"),
                    MetricConfigFactory.createCountConfig(enabled, "itemsFailed"),
                    MetricConfigFactory.createCountConfig(enabled, "backoffApplied"),
                    MetricConfigFactory.createCountConfig(enabled, "batchesFailed"),
                    MetricConfigFactory.createMaxConfig(enabled, "failoverTookMs", true))
            );
        }

        @Override
        public void register(final MetricsRegistry registry) {
            registrations.add(registry.register(serverTookMs));
            registrations.add(registry.register(itemsSent));
            registrations.add(registry.register(itemsDelivered));
            registrations.add(registry.register(itemsFailed));
            registrations.add(registry.register(backoffApplied));
            registrations.add(registry.register(batchesFailed));
            registrations.add(registry.register(failoverTookMs));
        }

        @Override
        public void deregister() {
            registrations.forEach(MetricsRegistry.Registration::deregister);
            registrations.clear();
        }

        public void serverTookMs(final int tookMs) {
            this.serverTookMs.store(tookMs);
        }

        public void itemsSent(final int itemsSent) {
            this.itemsSent.store(itemsSent);
        }

        public void itemsDelivered(final int count) {
            this.itemsDelivered.store(count);
        }

        public void itemsFailed(final int count) {
            this.itemsFailed.store(count);
        }

        public void backoffApplied(final int count) {
            this.backoffApplied.store(count);
        }

        public void batchFailed() {
            this.batchesFailed.store(1);
        }

        public void failoverTookMs(final long tookMs) {
            this.failoverTookMs.store(tookMs);
        }

    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy