org.tomitribe.tribestream.registryng.service.monitoring.MonitoringService.java Source code

Java tutorial

Introduction

Here is the source code for org.tomitribe.tribestream.registryng.service.monitoring.MonitoringService.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
package org.tomitribe.tribestream.registryng.service.monitoring;

import lombok.extern.java.Log;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.deltaspike.core.api.config.ConfigProperty;
import org.tomitribe.tribestream.registryng.documentation.Description;
import org.tomitribe.tribestream.registryng.entities.OpenApiDocument;
import org.tomitribe.util.Duration;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import javax.ejb.ConcurrencyManagement;
import javax.ejb.ConcurrencyManagementType;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.Timeout;
import javax.ejb.Timer;
import javax.ejb.TimerConfig;
import javax.ejb.TimerService;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.Response;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import static java.lang.Boolean.parseBoolean;
import static java.util.Optional.ofNullable;
import static java.util.logging.Level.SEVERE;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static javax.ws.rs.core.MediaType.WILDCARD_TYPE;
import static org.apache.deltaspike.core.api.config.ConfigResolver.getPropertyValue;

@Log
@Startup
@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
@TransactionManagement(TransactionManagementType.BEAN)
public class MonitoringService {
    @Resource
    private TimerService timerService;

    @Inject
    @Description("How often health checks are executed")
    @ConfigProperty(name = "tribe.registry.monitoring.period", defaultValue = "5 minutes")
    private String period;

    @Inject
    @Description("How often health checks are executed")
    @ConfigProperty(name = "tribe.registry.monitoring.startup-silent-period", defaultValue = "5 minutes")
    private String startupSilentPeriod;

    @Inject
    private HTTPMonitoringValidator httpValidator;

    @Inject
    private DatabaseMonitoringValidator mySqlValidator;

    @Inject
    private Event<Alert> alert;

    private long startedAt;

    private Timer timer;
    private long periodMs;

    private volatile boolean running = false;
    private volatile long lastRun = -1;
    private volatile List<ValidationWrapper> last;

    @PostConstruct
    private void boot() {
        startedAt = System.currentTimeMillis();

        if ("skip".equals(period)) {
            log.info("Monitoring is disabled");
            return;
        }

        periodMs = new Duration(period, TimeUnit.SECONDS).getTime(TimeUnit.MILLISECONDS);
        if (periodMs <= 0) {
            log.info("Monitoring is disabled");
            return;
        }

        monitor(null); // init state

        timer = timerService.createIntervalTimer(periodMs, periodMs,
                new TimerConfig("tribestream-api-registry-monitoring", false));
        log.info("Monitoring active each " + periodMs + "ms");
    }

    @PreDestroy
    private void destroy() {
        ofNullable(timer).ifPresent(Timer::cancel);
    }

    @Timeout
    public void monitor(final Timer ignored) {
        final long now = System.currentTimeMillis();
        if (running) {
            if (now - lastRun > periodMs * 2) {
                log.severe(
                        "Seems validation are really slow (taking more than twice the run period: " + period + ")");
            }
            return;
        }
        running = true;

        try {
            final List<ValidationWrapper> validations = Stream.of(httpValidator, mySqlValidator)
                    .map(e -> new ValidationWrapper(e.name(), e.valid())).collect(toList());
            last = validations;
            final long end = System.currentTimeMillis();
            if (end - now > periodMs) {
                log.warning("Validations take longer than the period: period=" + period + ", validation duration="
                        + TimeUnit.MILLISECONDS.toSeconds(end - now) + "s");
            }

            if (validations.stream().filter(v -> v.getValidation().getState() == Validation.State.KO).findFirst()
                    .isPresent()) {
                onError(validations);
            } else {
                log.fine("All validations suceeded");
            }
        } catch (final Throwable exception) {
            // dont call onError() otherwise it can loop if alerters are buggy
            log.log(SEVERE, "Error running validations", exception);
        } finally {
            lastRun = now;
            running = false;
        }
    }

    private void onError(final Collection<ValidationWrapper> messages) {
        if (System.currentTimeMillis() - startedAt < new Duration(startupSilentPeriod, TimeUnit.MILLISECONDS)
                .getTime()) {
            return;
        }
        alert.fire(new Alert(messages));
    }

    public Collection<ValidationWrapper> currentHealth() {
        return last;
    }

    public static abstract class MonitoringEntry {
        private boolean active;

        @PostConstruct
        private void init() {
            final String key = "tribe.registry.monitoring."
                    + getClass().getSimpleName().toLowerCase(Locale.ENGLISH).replaceAll("MonitoringValidator", "")
                    + ".active";
            active = parseBoolean(getPropertyValue(key, "true"));
        }

        public boolean isActive() {
            return active;
        }

        public abstract Validation valid();

        public abstract String name();
    }

    @ApplicationScoped
    public static class DatabaseMonitoringValidator extends MonitoringEntry {
        @Inject
        @Description("Timeout to execute select * from OpenApiDocument (limit 1)")
        @ConfigProperty(name = "tribe.registry.monitoring.database.timeout", defaultValue = "10000")
        private Integer timeout;

        @PersistenceContext
        private EntityManager em;

        @Override
        public Validation valid() {
            try { // just ensure we can run a query
                em.createNamedQuery(OpenApiDocument.Queries.FIND_ALL, OpenApiDocument.class)
                        .setHint("javax.persistence.query.timeout", timeout).setMaxResults(1).getResultList();
                return new Validation(Validation.State.OK, "'select * from OpenApiDocument' suceeds");
            } catch (final Throwable error) {
                return new Validation(Validation.State.KO, error.getMessage());
            }
        }

        @Override
        public String name() {
            return "Database";
        }
    }

    @ApplicationScoped
    public static class HTTPMonitoringValidator extends MonitoringEntry {
        @Inject
        @Description("URL to check for a 2xx HTTP status (split by a comma)")
        @ConfigProperty(name = "tribe.registry.monitoring.http.urls", defaultValue = "http://localhost:9200")
        private String bases;

        @Inject
        @Description("Should HTTP validator ignore invalid certificates (or self signed ones)")
        @ConfigProperty(name = "tribe.registry.monitoring.http.ssl-skip", defaultValue = "false")
        private Boolean forceSslAccept;

        @Inject
        @Description("HTTP receive timeout")
        @ConfigProperty(name = "tribe.registry.monitoring.http.timeout.receive", defaultValue = "10000")
        private String receiveTimeout;

        @Inject
        @Description("HTTP connect timeout")
        @ConfigProperty(name = "tribe.registry.monitoring.http.timeout.connect", defaultValue = "10000")
        private String connectTimeout;

        @Override
        public Validation valid() {
            final ClientBuilder clientBuilder = ClientBuilder.newBuilder();
            if (forceSslAccept) {
                final SSLContext sslContext;
                try {
                    sslContext = SSLContext.getInstance("SSL");
                    sslContext.init(null, new TrustManager[] { new X509TrustManager() {
                        @Override
                        public void checkClientTrusted(final X509Certificate[] x509Certificates, final String s)
                                throws CertificateException {
                            // no-op
                        }

                        @Override
                        public void checkServerTrusted(final X509Certificate[] x509Certificates, final String s)
                                throws CertificateException {
                            // no-op
                        }

                        @Override
                        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                            return null;
                        }

                    } }, new java.security.SecureRandom());
                } catch (final NoSuchAlgorithmException | KeyManagementException e) {
                    throw new IllegalStateException(e);
                }

                clientBuilder.hostnameVerifier((s, session) -> true).sslContext(sslContext);
            }
            final Client client = clientBuilder.build().property("http.connection.timeout", connectTimeout)
                    .property("http.receive.timeout", receiveTimeout)
                    .property("jersey.config.client.connectTimeout", connectTimeout)
                    .property("jersey.config.client.readTimeout", receiveTimeout);
            try {
                final List<Pair<String, Integer>> checks = Stream.of(bases.split(","))
                        .map(base -> ImmutablePair.of(base, doGet(client, base))).collect(toList());
                if (checks.stream()
                        .filter(s -> s.getRight() > 299 || s.getRight() < Response.Status.OK.getStatusCode())
                        .findAny().isPresent()) {
                    return new Validation(Validation.State.KO,
                            "One or multiple servers are slow or not responding: " + checks.stream()
                                    .map(p -> p.getLeft() + ": "
                                            + (p.getRight() < 0 ? "no connection" : ("HTTP " + p.getRight())))
                                    .collect(joining(", ")));
                }
                return new Validation(Validation.State.OK, "Servers are responding");
            } finally {
                client.close();
            }
        }

        @Override
        public String name() {
            return "HTTP";
        }

        private int doGet(final Client client, final String base) {
            try {
                return client.target(base).request(WILDCARD_TYPE).get().getStatus();
            } catch (final WebApplicationException | ProcessingException war) {
                return -1;
            }
        }
    }
}