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

no.nav.common.metrics.SensuHandler Maven / Gradle / Ivy

The newest version!
package no.nav.common.metrics;

import lombok.extern.slf4j.Slf4j;
import no.nav.common.utils.MathUtils;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray;
import org.json.JSONObject;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import static java.lang.String.format;
import static no.nav.common.metrics.SensuConfig.SENSU_MIN_BATCH_TIME;
import static no.nav.common.metrics.SensuConfig.SENSU_MIN_QUEUE_SIZE;

@Slf4j
public class SensuHandler {

    private final SensuConfig sensuConfig;

    private final LinkedBlockingQueue reportQueue;

    private final ScheduledExecutorService scheduledExecutorService;

    private volatile long queueSisteGangFullTimestamp;

    private volatile boolean isShutDown;

    public SensuHandler(SensuConfig sensuConfig) {
        validateSensuConfig(sensuConfig);

        this.sensuConfig = sensuConfig;
        this.reportQueue = new LinkedBlockingQueue<>(sensuConfig.getQueueSize());
        this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        this.scheduledExecutorService.execute(new SensuReporter());

        if (sensuConfig.isCleanupOnShutdown()) {
            setupShutdownHook();
        }

        log.info("Metrics aktivert med parametre: {}", sensuConfig);
    }

    private void setupShutdownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            if (isShutDown) {
                log.info("Sensu is already shut down. Skipping shutdown hook.");
                return;
            }

            shutdown();
            log.info("Sensu shutdown hook triggered. Will try to flush leftover metrics.");

            if (!reportQueue.isEmpty()) {
                List reports = new ArrayList<>();
                reportQueue.drainTo(reports, Integer.MAX_VALUE);
                String influxOutput = StringUtils.join(reports, "\n");
                JSONObject jsonObject = createJSON(influxOutput);

                try (Socket socket = new Socket()) {
                    writeToSensu(jsonObject, socket);
                    log.info("Successfully flushed metrics after shutdown. Metrics sent: " + reports.size());
                } catch (Exception e) {
                    log.error("Failed to flush metrics after shutdown. Metrics that were not sent: " + reports.size(), e);
                }
            }
        }));
    }

    public void shutdown() {
        isShutDown = true;
        try {
            scheduledExecutorService.awaitTermination(5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private class SensuReporter implements Runnable {

        @Override
        public void run() {

            List reports = new ArrayList<>();

            while (!isShutDown) {
                try {
                    if (!reportQueue.isEmpty()) {
                        reports.clear();

                        reportQueue.drainTo(reports, sensuConfig.getBatchSize() - 1);
                        float percentFull = (reportQueue.size() / (float) sensuConfig.getQueueSize()) * 100;
                        log.info(format("Metrics still in queue: %d \tQueue full: %.2f%% \tMetrics sent: %d", reportQueue.size(), percentFull, reports.size()));

                        String influxOutput = StringUtils.join(reports, "\n");
                        JSONObject jsonObject = createJSON(influxOutput);

                        try (Socket socket = new Socket()) {
                            writeToSensu(jsonObject, socket);
                        } catch (IOException e) {
                            reportQueue.addAll(reports); // Legger tilbake i køen
                            log.error("Noe gikk feil med tilkoblingen til Sensu socket: {} - {}", e.getClass().getSimpleName(), e.getMessage());
                            if (!isShutDown) {
                                Thread.sleep(sensuConfig.getRetryInterval()); // Unngår å spamme connections (og loggen med feilmeldinger) om noe ikke virker
                            }
                        }
                    }

                    if (!isShutDown) {
                        long batchTime = calculateBatchTime(reportQueue.size(), sensuConfig.getQueueSize(), SENSU_MIN_BATCH_TIME, sensuConfig.getMaxBatchTime());
                        Thread.sleep(batchTime);
                    }
                } catch (InterruptedException e) {
                    log.error("Å vente på neste objekt ble avbrutt, bør ikke kunne skje", e);
                }

            }

        }
    }

    void writeToSensu(JSONObject jsonObject, Socket socket) throws IOException {
        BufferedWriter writer = connectToSensu(socket);
        writer.write(jsonObject.toString());
        writer.newLine();
        writer.flush();
    }

    private BufferedWriter connectToSensu(Socket socket) throws IOException {
        InetSocketAddress inetSocketAddress = new InetSocketAddress(sensuConfig.getSensuHost(), sensuConfig.getSensuPort());
        socket.connect(inetSocketAddress, sensuConfig.getConnectTimeout());
        return new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
    }

    /**
     * Calculates batch time based on how full the queue is. Fuller queue = less time between batches
     */
    static long calculateBatchTime(int currentQueueSize, int maxQueueSize, long minBatchTime, long maxBatchTime) {
        float percentEmpty = 1 - (currentQueueSize / (float) maxQueueSize);
        return MathUtils.linearInterpolation(minBatchTime, maxBatchTime, percentEmpty);
    }

    private void validateSensuConfig(SensuConfig sensuConfig) {
        if (sensuConfig.getMaxBatchTime() < SENSU_MIN_BATCH_TIME) {
            throw new IllegalArgumentException("Max batch time must be equal to or above " + SENSU_MIN_BATCH_TIME);
        } else if (sensuConfig.getQueueSize() < SENSU_MIN_QUEUE_SIZE) {
            throw new IllegalArgumentException("Queue size must be equal to or larger than " + SENSU_MIN_QUEUE_SIZE);
        }
    }

    public void report(String output) {
        if (isShutDown) {
            log.error("Cannot send report to sensu after shutting down");
            return;
        }

        boolean result = reportQueue.offer(output); // Thread-safe måte å legge til i køen på, returnerer false i stedet for å blokkere om køen er full

        if (!result && (System.currentTimeMillis() - queueSisteGangFullTimestamp > 1000 * 60)) { // Unngår å spamme loggen om køen er full over lengre tid (f. eks. Sensu er nede)
            log.error("Sensu-køen har vært full, ikke alle metrikker har blitt sendt til Sensu");
            queueSisteGangFullTimestamp = System.currentTimeMillis();
        }
    }

    private JSONObject createJSON(String output) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("name", sensuConfig.getApplication());
        jsonObject.put("type", "metric");
        jsonObject.put("output", output);
        jsonObject.put("status", 0);
        jsonObject.put("handlers", new JSONArray("[events_nano]"));

        return jsonObject;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy