diff --git a/CHANGELOG.md b/CHANGELOG.md index 77b20ae..0674e29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ Please mark backwards incompatible changes with an exclamation mark at the start ## [Unreleased] +### Added +- The `#stats` method to the `Elasticsearch::Client` class. The method returns + an object that can be used to retrieve statistics about the Cluster. For the + moment only `#indices` is available, which returns index-related statistics. + ## [28.0.0] - 2025-05-09 ### Added diff --git a/documentation/source/user_guidelines/elasticsearch/stats.rst b/documentation/source/user_guidelines/elasticsearch/stats.rst new file mode 100644 index 0000000..4dfe27a --- /dev/null +++ b/documentation/source/user_guidelines/elasticsearch/stats.rst @@ -0,0 +1,47 @@ +Stats +===== + +The ``Stats`` class gives you access to various statistics about the +Elasticsearch cluster. The class can be accessed by calling the ``#stats`` +method on an instance of the ``Elasticsearch::Client`` class. For example: + +.. code-block:: ruby + + require 'jay_api/elasticsearch/client_factory' + + client_factory = JayAPI::Elasticsearch::ClientFactory.new(...) + client = client_factory.create + + stats = client.stats + +The ``Stats`` class has the following methods: + +#indices +-------- + +This method gives you access to index-related statistics. The method returns an +instance of the ``Stats::Indices`` class, which in turn allows you to access +information on each of the individual indices through these methods: + +#all +++++ + +This method returns an ``Enumerator`` whose elements are ``Stats::Index`` +objects, one for each of the indices on the cluster, including system indices. + +#system ++++++++ + +This method returns an ``Enumerator`` whose elements are ``Stats::Index`` +objects, one for each of the system indices on the cluster. + +#user ++++++ + +This method returns an ``Enumerator`` whose elements are ``Stats::Index`` +objects, one for each of the user-created indices on the cluster. + +The ``Stats::Index`` objects have the following methods: + +``#name`` + The name of the index. diff --git a/lib/jay_api/elasticsearch.rb b/lib/jay_api/elasticsearch.rb index 7eac892..6d1dc8c 100644 --- a/lib/jay_api/elasticsearch.rb +++ b/lib/jay_api/elasticsearch.rb @@ -10,6 +10,7 @@ require_relative 'elasticsearch/query_results' require_relative 'elasticsearch/response' require_relative 'elasticsearch/search_after_results' +require_relative 'elasticsearch/stats' require_relative 'elasticsearch/tasks' require_relative 'elasticsearch/time' diff --git a/lib/jay_api/elasticsearch/client.rb b/lib/jay_api/elasticsearch/client.rb index 58adfb5..2507557 100644 --- a/lib/jay_api/elasticsearch/client.rb +++ b/lib/jay_api/elasticsearch/client.rb @@ -6,6 +6,7 @@ require 'forwardable' require_relative '../abstract/connection' +require_relative 'stats' module JayAPI module Elasticsearch @@ -91,6 +92,12 @@ def task_by_id(**args) retry_request { transport_client.tasks.get(**args) } end + # @return [JayAPI::Elasticsearch::Stats] An instance of the +Stats+ class, + # which gives the caller access to Elasticsearch's Statistics API. + def stats + @stats ||= ::JayAPI::Elasticsearch::Stats.new(transport_client) + end + private # @param [Proc] block The block to execute. diff --git a/lib/jay_api/elasticsearch/stats.rb b/lib/jay_api/elasticsearch/stats.rb new file mode 100644 index 0000000..be978d4 --- /dev/null +++ b/lib/jay_api/elasticsearch/stats.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require_relative 'stats/index' +require_relative 'stats/indices' + +module JayAPI + module Elasticsearch + # This class provides access to Elasticsearch's Cluster Statistic API. + class Stats + attr_reader :transport_client, :logger + + # @param [Elasticsearch::Transport::Client] transport_client The transport + # client to use to make requests to the cluster. + def initialize(transport_client) + @transport_client = transport_client + end + + # @return [JayAPI::Elasticsearch::Stats::Indices] Information about the + # indices that exist in the Elasticsearch cluster. + # @raise [Elasticsearch::Transport::Transport::ServerError] If the request + # to the Statistics API endpoint fails. + def indices + # DO NOT MEMOIZE! Leave it to the caller. + ::JayAPI::Elasticsearch::Stats::Indices.new(response['indices']) + end + + private + + # @return [Hash] The Hash with the statistics returned by the + # Elasticsearch cluster. + # @raise [Elasticsearch::Transport::Transport::ServerError] If the + # request fails. + def response + # DO NOT MEMOIZE! Leave it to the caller. + transport_client.indices.stats + end + end + end +end diff --git a/lib/jay_api/elasticsearch/stats/index.rb b/lib/jay_api/elasticsearch/stats/index.rb new file mode 100644 index 0000000..7c0eb2b --- /dev/null +++ b/lib/jay_api/elasticsearch/stats/index.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module JayAPI + module Elasticsearch + class Stats + # Holds information about an Elasticsearch Index. + class Index + attr_reader :name + + # @param [String] name The name of the index. + # @param [Hash] data Information about the index. + def initialize(name, data) + @name = name + @data = data + end + end + end + end +end diff --git a/lib/jay_api/elasticsearch/stats/indices.rb b/lib/jay_api/elasticsearch/stats/indices.rb new file mode 100644 index 0000000..2770993 --- /dev/null +++ b/lib/jay_api/elasticsearch/stats/indices.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require_relative 'index' + +module JayAPI + module Elasticsearch + class Stats + # Provides access to the list of indices returned by Elasticsearch's + # Stats API + class Indices + # A lambda used to select / reject system indices (indices whose name + # starts with dot). + SYSTEM_SELECTOR = ->(name, _data) { name.starts_with?('.') } + + # @param [Hash{String=>Hash}] indices A +Hash+ with the information + # about the indices. Its keys are the names of the indices, its values + # hashes with information about each of the indices. + def initialize(indices) + @indices = indices + end + + # @return [Enumerator::Lazy] A lazy + # enumerator of +Index+ objects, one for each of the indexes. All + # indices (system and user-defined are included). + def all + @all ||= with_lazy_instantiation { indices } + end + + # @return [Enumerator::Lazy] A lazy + # enumerator of +Index+ objects. Includes only the system indices. + def system + @system ||= with_lazy_instantiation { indices.select(&SYSTEM_SELECTOR) } + end + + # @return [Enumerator::Lazy] A lazy + # enumerator of +Index+ objects. Includes only the user-defined + # indices. + def user + @user ||= with_lazy_instantiation { indices.reject(&SYSTEM_SELECTOR) } + end + + private + + attr_reader :indices + + # @param [Array(String, Hash)] args An array with two elements, the name + # of the index and its information. + # @return [JayAPI::Elasticsearch::Stats::Index] An +Index+ object + # representing the given index. + def build_index(args) + JayAPI::Elasticsearch::Stats::Index.new(*args) + end + + # Calls the given block and turns its return value into a lazy + # enumerator that instantiates an +Index+ object for each of the + # elements of the collection returned by block. + # @return [Enumerator::Lazy] + def with_lazy_instantiation(&block) + block.call.lazy.map(&method(:build_index)) + end + end + end + end +end diff --git a/spec/integration/jay_api/elasticsearch/client_spec.rb b/spec/integration/jay_api/elasticsearch/client_spec.rb index be8e741..42dd8e2 100644 --- a/spec/integration/jay_api/elasticsearch/client_spec.rb +++ b/spec/integration/jay_api/elasticsearch/client_spec.rb @@ -296,4 +296,27 @@ method_call end end + + describe '#stats' do + subject(:method_call) { client.stats } + + let(:stats) do + instance_double( + JayAPI::Elasticsearch::Stats + ) + end + + before do + allow(JayAPI::Elasticsearch::Stats).to receive(:new).and_return(stats) + end + + it 'initializes an instance of JayAPI::Elasticsearch::Stats and passes the transport client to it' do + expect(JayAPI::Elasticsearch::Stats).to receive(:new).with(transport_client) + method_call + end + + it 'returns the JayAPI::Elasticsearch::Stats instance' do + expect(method_call).to be(stats) + end + end end diff --git a/spec/jay_api/elasticsearch/stats/index_spec.rb b/spec/jay_api/elasticsearch/stats/index_spec.rb new file mode 100644 index 0000000..252d14a --- /dev/null +++ b/spec/jay_api/elasticsearch/stats/index_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'jay_api/elasticsearch/stats/index' + +RSpec.describe JayAPI::Elasticsearch::Stats::Index do + subject(:index) { described_class.new(name, data) } + + let(:name) { 'xyz01_integration_tests' } + let(:data) { {} } + + describe '#initialize' do + subject(:method_call) { index } + + it 'stores the given name' do + method_call + expect(index.name).to be(name) + end + end +end diff --git a/spec/jay_api/elasticsearch/stats/indices_spec.rb b/spec/jay_api/elasticsearch/stats/indices_spec.rb new file mode 100644 index 0000000..eb793eb --- /dev/null +++ b/spec/jay_api/elasticsearch/stats/indices_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'jay_api/elasticsearch/stats/indices' + +RSpec.describe JayAPI::Elasticsearch::Stats::Indices do + subject(:indices) { described_class.new(indices_hash) } + + let(:indices_hash) do + { + 'xyz01_integration_test' => { + 'uuid' => 'OXRb8_IYTseG2epNa9Ls3g' + }, + 'xyz01_unit_tests' => { + 'uuid' => 'hxDdhi-3TFSndxhLesspFw' + }, + '.kibana_views' => { + 'uuid' => 'pr-VjrPARlG3lAoAfPqNog' + }, + 'xyz02_manual_verification' => { + 'uuid' => 'uaZ_kKQuSM-HaKH_LcI7BQ' + }, + '.backup' => { + 'uuid' => 'N7TZOstjRHu8mTwsLZuQ5w' + } + } + end + + shared_examples_for '#all' do + it 'returns an Enumerator::Lazy' do + expect(method_call).to be_a(Enumerator::Lazy) + end + + it 'includes the expected number of indices' do + expect(method_call.size).to eq(expected_indices_size) + end + + it 'includes only instances of JayAPI::Elasticsearch::Stats::Index' do + expect(method_call).to all(be_a(JayAPI::Elasticsearch::Stats::Index)) + end + + it 'includes the expected list of indices' do + # #to_a is needed here because of the lazy enumerator. + expect(method_call.map(&:name).to_a).to eq(expected_indices) + end + end + + describe '#all' do + subject(:method_call) { indices.all } + + let(:expected_indices_size) { 5 } + + let(:expected_indices) do + %w[xyz01_integration_test xyz01_unit_tests .kibana_views xyz02_manual_verification .backup] + end + + it_behaves_like '#all' + end + + describe '#system' do + subject(:method_call) { indices.system } + + let(:expected_indices_size) { 2 } + let(:expected_indices) { %w[.kibana_views .backup] } + + it_behaves_like '#all' + end + + describe '#user' do + subject(:method_call) { indices.user } + + let(:expected_indices_size) { 3 } + + let(:expected_indices) do + %w[xyz01_integration_test xyz01_unit_tests xyz02_manual_verification] + end + + it_behaves_like '#all' + end +end diff --git a/spec/jay_api/elasticsearch/stats_spec.rb b/spec/jay_api/elasticsearch/stats_spec.rb new file mode 100644 index 0000000..742b44f --- /dev/null +++ b/spec/jay_api/elasticsearch/stats_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'elasticsearch/transport/transport/errors' +require 'jay_api/elasticsearch/stats' + +RSpec.describe JayAPI::Elasticsearch::Stats do + subject(:stats) { described_class.new(transport_client) } + + let(:stats_hash) do + { + '_all' => { + 'total' => { + 'store' => { + 'size_in_bytes' => 830_124_136, + 'reserved_in_bytes' => 0 + } + } + }, + 'indices' => { + 'xyz01_integration_test' => { + 'uuid' => 'OXRb8_IYTseG2epNa9Ls3g' + }, + 'xyz01_unit_tests' => { + 'uuid' => 'hxDdhi-3TFSndxhLesspFw' + }, + '.kibana_views' => { + 'uuid' => 'pr-VjrPARlG3lAoAfPqNog' + }, + 'xyz02_manual_verification' => { + 'uuid' => 'uaZ_kKQuSM-HaKH_LcI7BQ' + }, + '.backup' => { + 'uuid' => 'N7TZOstjRHu8mTwsLZuQ5w' + } + } + } + end + + let(:indices_client) do + instance_double( + Elasticsearch::API::Indices::IndicesClient, + stats: stats_hash + ) + end + + let(:transport_client) do + instance_double( + Elasticsearch::Transport::Client, + indices: indices_client + ) + end + + describe '#indices' do + subject(:method_call) { stats.indices } + + let(:indices) do + instance_double( + JayAPI::Elasticsearch::Stats::Indices + ) + end + + let(:expected_indices_data) do + { + 'xyz01_integration_test' => { + 'uuid' => 'OXRb8_IYTseG2epNa9Ls3g' + }, + 'xyz01_unit_tests' => { + 'uuid' => 'hxDdhi-3TFSndxhLesspFw' + }, + '.kibana_views' => { + 'uuid' => 'pr-VjrPARlG3lAoAfPqNog' + }, + 'xyz02_manual_verification' => { + 'uuid' => 'uaZ_kKQuSM-HaKH_LcI7BQ' + }, + '.backup' => { + 'uuid' => 'N7TZOstjRHu8mTwsLZuQ5w' + } + } + end + + before do + allow(JayAPI::Elasticsearch::Stats::Indices).to receive(:new).and_return(indices) + end + + it "requests the indices' statistics from the Elasticsearch cluster" do + expect(transport_client).to receive(:indices) + expect(indices_client).to receive(:stats) + method_call + end + + context 'when the API request fails' do + let(:error) do + [ + Elasticsearch::Transport::Transport::Errors::Unauthorized, + 'Authentication failed' + ] + end + + before do + allow(indices_client).to receive(:stats).and_raise(*error) + end + + it 're-raises the error' do + expect { method_call }.to raise_error(*error) + end + end + + it 'creates an instance of JayAPI::Elasticsearch::Stats::Indices and passes the indices data to it' do + expect(JayAPI::Elasticsearch::Stats::Indices) + .to receive(:new).with(expected_indices_data) + + method_call + end + + it 'returns the instance of JayAPI::Elasticsearch::Stats::Indices' do + expect(method_call).to be(indices) + end + end +end