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

org.graylog.events.search.MoreSearch 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.graylog.events.search;

import com.google.auto.value.AutoValue;
import org.graylog.events.processor.EventProcessorException;
import org.graylog.plugins.views.search.IndexRangeContainsOneOfStreams;
import org.graylog.plugins.views.search.Parameter;
import org.graylog.plugins.views.search.ParameterProvider;
import org.graylog.plugins.views.search.elasticsearch.QueryStringDecorators;
import org.graylog.plugins.views.search.errors.EmptyParameterError;
import org.graylog.plugins.views.search.errors.SearchException;
import org.graylog2.database.NotFoundException;
import org.graylog2.indexer.ranges.IndexRange;
import org.graylog2.indexer.ranges.IndexRangeService;
import org.graylog2.indexer.results.ResultMessage;
import org.graylog2.indexer.searches.Sorting;
import org.graylog2.plugin.indexer.searches.timeranges.TimeRange;
import org.graylog2.plugin.streams.Stream;
import org.graylog2.streams.StreamService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import static com.google.common.base.Preconditions.checkArgument;

/**
 * This class contains search helper for the events system.
 */
public class MoreSearch {
    private static final Logger LOG = LoggerFactory.getLogger(MoreSearch.class);

    private final StreamService streamService;
    private final IndexRangeService indexRangeService;
    private final QueryStringDecorators queryDecorators;
    private final MoreSearchAdapter moreSearchAdapter;

    @Inject
    public MoreSearch(StreamService streamService,
                      IndexRangeService indexRangeService,
                      QueryStringDecorators queryDecorators,
                      MoreSearchAdapter moreSearchAdapter) {
        this.streamService = streamService;
        this.indexRangeService = indexRangeService;
        this.queryDecorators = queryDecorators;
        this.moreSearchAdapter = moreSearchAdapter;
    }

    /**
     * Executes an events search for the given parameters.
     *
     * @param parameters             event search parameters
     * @param filterString           filter string
     * @param eventStreams           event streams to search in
     * @param forbiddenSourceStreams forbidden source streams
     * @return the result
     */
    // TODO: We cannot use Searches#search() at the moment because that method cannot handle multiple streams. (because of Searches#extractStreamId())
    //       We also cannot use the new search code at the moment because it doesn't do pagination.
    Result eventSearch(EventsSearchParameters parameters, String filterString, Set eventStreams, Set forbiddenSourceStreams) {
        checkArgument(parameters != null, "parameters cannot be null");
        checkArgument(!eventStreams.isEmpty(), "eventStreams cannot be empty");
        checkArgument(forbiddenSourceStreams != null, "forbiddenSourceStreams cannot be null");

        final Sorting.Direction sortDirection = parameters.sortDirection() == EventsSearchParameters.SortDirection.ASC ? Sorting.Direction.ASC : Sorting.Direction.DESC;
        final Sorting sorting = new Sorting(parameters.sortBy(), sortDirection);
        final String queryString = parameters.query().trim();
        final Set affectedIndices = getAffectedIndices(eventStreams, parameters.timerange());

        return moreSearchAdapter.eventSearch(queryString, parameters.timerange(), affectedIndices, sorting, parameters.page(), parameters.perPage(), eventStreams, filterString, forbiddenSourceStreams);
    }

    private Set getAffectedIndices(Set streamIds, TimeRange timeRange) {
        final SortedSet indexRanges = indexRangeService.find(timeRange.getFrom(), timeRange.getTo());

        // We support an empty streams list and return all affected indices in that case.
        if (streamIds.isEmpty()) {
            return indexRanges.stream()
                    .map(IndexRange::indexName)
                    .collect(Collectors.toSet());
        } else {
            final Set streams = loadStreams(streamIds);
            final IndexRangeContainsOneOfStreams indexRangeContainsOneOfStreams = new IndexRangeContainsOneOfStreams();
            return indexRanges.stream()
                    .filter(ir -> indexRangeContainsOneOfStreams.test(ir, streams))
                    .map(IndexRange::indexName)
                    .collect(Collectors.toSet());
        }
    }

    /**
     * This scrolls results for the given query, streams and time range from Elasticsearch. The result is passed to
     * the given callback in batches. (using the given batch size)
     * 

* The search will continue until it is done, an error occurs or the search is stopped by setting the * {@code continueScrolling} boolean to {@code false} from the {@link ScrollCallback}. *

* TODO: Elasticsearch has a default limit of 500 concurrent scrolls. Every caller of this method should check * if there is capacity to create a new scroll request. This can be done by using the ES nodes stats API. * See: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html#scroll-search-context * * @param queryString the search query string * @param streams the set of streams to search in * @param timeRange the time range for the search * @param batchSize the number of documents to retrieve at once * @param resultCallback the callback that gets executed for each batch */ public void scrollQuery(String queryString, Set streams, Set queryParameters, TimeRange timeRange, int batchSize, ScrollCallback resultCallback) throws EventProcessorException { final Set affectedIndices = getAffectedIndices(streams, timeRange); try { queryString = decorateQuery(queryString, queryParameters); } catch (SearchException e) { if (e.error() instanceof EmptyParameterError) { LOG.debug("Empty parameter from lookup table. Assuming non-matching query. Error: {}", e.getMessage()); return; } throw e; } moreSearchAdapter.scrollEvents(queryString, timeRange, affectedIndices, streams, batchSize, resultCallback::call); } public Set loadStreams(Set streamIds) { // TODO: Use method from `StreamService` which loads a collection of ids (when implemented) to prevent n+1. // Track https://github.com/Graylog2/graylog2-server/issues/4897 for progress. Set streams = new HashSet<>(); for (String streamId : streamIds) { try { Stream load = streamService.load(streamId); streams.add(load); } catch (NotFoundException e) { LOG.debug("Failed to load stream <{}>", streamId); } } return streams; } /** * Substitute query string parameters using {@link QueryStringDecorators}. */ private String decorateQuery(String queryString, Set queryParameters) { return queryDecorators.decorate(queryString, ParameterProvider.of(queryParameters)); } /** * Helper to perform basic Lucene escaping of query string values * @param searchString search string which may contain unescaped reserved characters * @return String where those characters that Lucene expects to be escaped are escaped by a * preceding \ */ public static String luceneEscape(String searchString) { StringBuilder result = new StringBuilder(); if (searchString != null) { for (char c : searchString.toCharArray()) { // These characters are part of the query syntax and must be escaped if (c == '\\' || c == '+' || c == '-' || c == '!' || c == '(' || c == ')' || c == ':' || c == '^' || c == '[' || c == ']' || c == '\"' || c == '{' || c == '}' || c == '~' || c == '*' || c == '?' || c == '|' || c == '&' || c == '/') { result.append('\\'); } result.append(c); } } return result.toString(); } /** * Callback that receives message batches from {@link #scrollQuery(String, Set, Set, TimeRange, int, ScrollCallback)}. */ public interface ScrollCallback { /** * This will be called with message batches from a scroll query. To stop the scroll query, the * {@code continueScrolling} boolean can be set to {@code false}. * * @param messages the message batch * @param continueScrolling the boolean that can be set to {@code false} to stop the scroll query */ void call(List messages, AtomicBoolean continueScrolling) throws EventProcessorException; } @AutoValue public static abstract class Result { public abstract List results(); public abstract long resultsCount(); public abstract long duration(); public abstract Set usedIndexNames(); public abstract String executedQuery(); public static Builder builder() { return new AutoValue_MoreSearch_Result.Builder(); } @AutoValue.Builder public abstract static class Builder { public abstract Builder results(List results); public abstract Builder resultsCount(long resultsCount); public abstract Builder duration(long duration); public abstract Builder usedIndexNames(Set usedIndexNames); public abstract Builder executedQuery(String executedQuery); public abstract Result build(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy