defmodule GraylogSearch do @moduledoc """ Constructs Graylog search URL. """ @doc """ Given a base URL, returns a URI for GraylogSearch pipelines. """ def new(url) when is_binary(url) do uri = URI.parse(url) %URI{uri | path: "/search"} end def url(%URI{} = uri), do: URI.to_string(uri) @doc """ Search for a message by the given query term or phrase. This function can be chained to combine queries with AND. By default, all fields are included in the search when a field to search in is not specified. """ @spec by(URI.t(), String.t()) :: URI.t() def by(%URI{} = uri, query) when is_binary(query) do sanitised_query = sanitise_input(query) and_query(uri, sanitised_query) end @doc "Alias for `by/2`" @spec and_by(URI.t(), String.t()) :: URI.t() def and_by(uri, query), do: by(uri, query) defp and_query(uri, nil), do: uri defp and_query(%URI{query: nil} = uri, query) do query_string = URI.encode_query(%{"q" => query}) %URI{uri | query: query_string} end defp and_query(%URI{} = uri, query) do query_string = uri.query |> URI.decode_query() |> Map.get_and_update("q", &add_and_query(&1, query)) |> (fn {_old_query, new_query} -> new_query end).() |> URI.encode_query() %URI{uri | query: query_string} end @doc """ Search messages by another term or phrase. Uses the OR operator to combine queries. """ @spec or_by(URI.t(), String.t()) :: URI.t() def or_by(%URI{} = uri, query) when is_binary(query) do sanitised_query = sanitise_input(query) or_query(uri, sanitised_query) end defp or_query(uri, nil), do: uri defp or_query(%URI{query: nil} = uri, _query), do: uri defp or_query(%URI{} = uri, query) do query_string = uri.query |> URI.decode_query() |> Map.get_and_update("q", &add_or_query(&1, query)) |> (fn {_old_query, new_query} -> new_query end).() |> URI.encode_query() %URI{uri | query: query_string} end @doc """ Search for messages that do not include a term or phrase. Uses the NOT operator. Can be chained to combine queries with AND NOT. """ @spec not_by(URI.t(), String.t()) :: URI.t() def not_by(%URI{} = uri, query) when is_binary(query) do sanitised_query = sanitise_input(query) not_query(uri, sanitised_query) end defp not_query(uri, nil), do: uri defp not_query(%URI{query: nil} = uri, query) do query_string = URI.encode_query(%{"q" => "NOT #{query}"}) %URI{uri | query: query_string} end defp not_query(%URI{} = uri, query) do query_string = uri.query |> URI.decode_query() |> Map.get_and_update("q", &add_not_query(&1, query)) |> (fn {_old_query, new_query} -> new_query end).() |> URI.encode_query() %URI{uri | query: query_string} end @doc "Alias for `not_by/2`" def and_not(uri, query), do: not_by(uri, query) defp add_and_query(existing_query, query_addition), do: add_to_query(existing_query, query_addition, " AND ") defp add_or_query(existing_query, query_addition), do: add_to_query(existing_query, query_addition, " OR ") defp add_not_query(existing_query, query_addition), do: add_to_query(existing_query, query_addition, " AND NOT ") defp add_to_query(existing_query, query_addition, delimiter) do new_query = existing_query <> delimiter <> query_addition {existing_query, new_query} end defp sanitise_input(query) when is_binary(query) do case String.trim(query) do "" -> nil query -> query end end end