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

org.graylog2.storage.versionprobe.VersionProbe Maven / Gradle / Ivy

There is a newer version: 6.1.4
Show newest version
/*
 * Copyright (C) 2020 Graylog, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the Server Side Public License, version 1,
 * as published by MongoDB, Inc.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * Server Side Public License for more details.
 *
 * You should have received a copy of the Server Side Public License
 * along with this program. If not, see
 * .
 */
package org.graylog2.storage.versionprobe;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.joschi.jadconfig.util.Duration;
import com.github.rholder.retry.Attempt;
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.RetryListener;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import com.google.common.base.Strings;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.ResponseBody;
import org.graylog2.configuration.RunsWithDataNode;
import org.graylog2.security.IndexerJwtAuthTokenProvider;
import org.graylog2.shared.utilities.ExceptionUtils;
import org.graylog2.storage.SearchVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import retrofit2.Converter;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;

import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;

public class VersionProbe {
    private static final Logger LOG = LoggerFactory.getLogger(VersionProbe.class);
    private final ObjectMapper objectMapper;
    private final OkHttpClient okHttpClient;
    private final int connectionAttempts;
    private final Duration delayBetweenAttempts;
    private final boolean isJwtAuthentication;
    private final IndexerJwtAuthTokenProvider indexerJwtAuthTokenProvider;

    @Inject
    public VersionProbe(ObjectMapper objectMapper,
                        OkHttpClient okHttpClient,
                        @Named("elasticsearch_version_probe_attempts") int elasticsearchVersionProbeAttempts,
                        @Named("elasticsearch_version_probe_delay") Duration elasticsearchVersionProbeDelay,
                        @RunsWithDataNode Boolean runsWithDataNode,
                        @Named("indexer_use_jwt_authentication") boolean opensearchUseJwtAuthentication,
                        IndexerJwtAuthTokenProvider indexerJwtAuthTokenProvider) {
        this.objectMapper = objectMapper;
        this.okHttpClient = okHttpClient;
        this.connectionAttempts = elasticsearchVersionProbeAttempts;
        this.delayBetweenAttempts = elasticsearchVersionProbeDelay;
        this.isJwtAuthentication = runsWithDataNode || opensearchUseJwtAuthentication;
        this.indexerJwtAuthTokenProvider = indexerJwtAuthTokenProvider;
    }

    public Optional probe(final Collection hosts) {
        try {
            return RetryerBuilder.>newBuilder()
                    .retryIfResult(input -> !input.isPresent())
                    .retryIfExceptionOfType(IOException.class)
                    .retryIfRuntimeException()
                    .withRetryListener(new RetryListener() {
                        @Override
                        public void onRetry(Attempt attempt) {
                            if (attempt.hasResult()) {
                                final Object result = attempt.getResult();
                                if (result instanceof Optional && ((Optional) result).isPresent()) {
                                    return;
                                }
                            }
                            if (connectionAttempts == 0) {
                                LOG.info("OpenSearch/Elasticsearch is not available. Retry #{}", attempt.getAttemptNumber());
                            } else {
                                LOG.info("OpenSearch/Elasticsearch is not available. Retry #{}/{}", attempt.getAttemptNumber(), connectionAttempts);
                            }
                        }
                    })
                    .withWaitStrategy(WaitStrategies.fixedWait(delayBetweenAttempts.getQuantity(), delayBetweenAttempts.getUnit()))
                    .withStopStrategy((connectionAttempts == 0) ? StopStrategies.neverStop() : StopStrategies.stopAfterAttempt(connectionAttempts))
                    .build().call(() -> this.probeAllHosts(hosts));
        } catch (ExecutionException | RetryException e) {
            LOG.error("Unable to retrieve version from OpenSearch/Elasticsearch node: ", e);
        }
        return Optional.empty();
    }

    private Optional probeAllHosts(final Collection hosts) {
        return hosts
                .stream()
                .map(this::probeSingleHost)
                .filter(Optional::isPresent)
                .findFirst()
                .orElse(Optional.empty());
    }

    private Optional probeSingleHost(URI host) {
        final Retrofit retrofit;
        try {
            retrofit = new Retrofit.Builder()
                    .baseUrl(host.toURL())
                    .addConverterFactory(JacksonConverterFactory.create(objectMapper))
                    .client(addAuthenticationIfPresent(host, okHttpClient))
                    .build();
        } catch (MalformedURLException e) {
            LOG.error("Elasticsearch node URL is invalid: " + host.toString(), e);
            return Optional.empty();
        }

        final RootRoute root = retrofit.create(RootRoute.class);

        final Converter errorResponseConverter = retrofit.responseBodyConverter(ErrorResponse.class, new Annotation[0]);
        final Consumer errorLogger = (responseBody) -> {
            try {
                final ErrorResponse errorResponse = errorResponseConverter.convert(responseBody);
                LOG.error("Unable to retrieve version from OpenSearch/Elasticsearch node {}:{}: {}", host.getHost(), host.getPort(), errorResponse);
            } catch (IOException e) {
                LOG.error("Unable to retrieve version from OpenSearch/Elasticsearch node {}:{}: unknown error - an exception occurred while deserializing error response: {}", host.getHost(), host.getPort(), e);
            }
        };


        return rootResponse(root, errorLogger)
                .map(RootResponse::version)
                .flatMap(this::parseVersion);
    }

    private Optional getAuthToken(final URI host) {
        if (Strings.emptyToNull(host.getUserInfo()) != null) {
            final String[] credentials = host.getUserInfo().split(":");
            final String username = credentials[0];
            final String password = credentials[1];
            return Optional.of(Credentials.basic(username, password));
        }

        return Optional.empty();
    }

    private OkHttpClient addAuthenticationIfPresent(URI host, OkHttpClient okHttpClient) {
        final Optional authToken = getAuthToken(host);

        if(isJwtAuthentication || authToken.isPresent()) {
            return okHttpClient.newBuilder()
                    .addInterceptor(chain -> {
                        final Request originalRequest = chain.request();
                        final Request.Builder builder = originalRequest.newBuilder().header("Authorization", isJwtAuthentication ? indexerJwtAuthTokenProvider.get() : authToken.get());
                        final Request newRequest = builder.build();
                        return chain.proceed(newRequest);
                    })
                    .build();
        }

        return okHttpClient;
    }

    private Optional parseVersion(VersionResponse versionResponse) {
        try {
            final com.github.zafarkhaja.semver.Version version = com.github.zafarkhaja.semver.Version.valueOf(versionResponse.number());
            return Optional.of(SearchVersion.create(versionResponse.distribution(), version));
        } catch (Exception e) {
            LOG.error("Unable to parse version retrieved from Elasticsearch node: <{}>", versionResponse.number(), e);
            return Optional.empty();
        }
    }

    private Optional rootResponse(final RootRoute rootRoute, Consumer errorLogger) {
        try {
            final Response response = rootRoute.root().execute();
            if (response.isSuccessful()) {
                return Optional.ofNullable(response.body());
            } else {
                errorLogger.accept(response.errorBody());
            }
        } catch (IOException e) {
            final String error = ExceptionUtils.formatMessageCause(e);
            final String rootCause = ExceptionUtils.formatMessageCause(ExceptionUtils.getRootCause(e));
            LOG.error("Unable to retrieve version from Elasticsearch node: {} - {}", error, rootCause);
            LOG.debug("Complete exception for version probe error: ", e);
        }
        return Optional.empty();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy