Skip to content

Commit b5b9822

Browse files
Logging improvements when plugin version mismatches with connected ES version (#247) (#250)
* Logging improvements: informing which ES version considered when building this plugin version, when plugin and connected ES versions do not match warn/inform incompatibility risk and show the guidance. * Preflight check initialization optimization. * Refactored messages in a way for better visibility. Any major mismatch is warning and connected ES minor ahead is warning as well. Co-authored-by: Ry Biesemeyer <[email protected]> * Standardize log outputs on console. --------- Co-authored-by: Ry Biesemeyer <[email protected]> (cherry picked from commit 021e8c7) Co-authored-by: Mashhur <[email protected]>
1 parent c1d9d50 commit b5b9822

File tree

9 files changed

+201
-73
lines changed

9 files changed

+201
-73
lines changed

lib/logstash/filters/elastic_integration.rb

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ def register
135135
initialize_event_processor!
136136

137137
perform_preflight_check!
138+
check_versions_alignment
138139
end # def register
139140

140141
def filter(event)
@@ -345,8 +346,8 @@ def initialize_elasticsearch_rest_client!
345346

346347
if serverless?
347348
@elasticsearch_rest_client = _elasticsearch_rest_client(config) do |builder|
348-
builder.configureElasticApi { |elasticApi| elasticApi.setApiVersion(ELASTIC_API_VERSION) }
349-
end
349+
builder.configureElasticApi { |elasticApi| elasticApi.setApiVersion(ELASTIC_API_VERSION) }
350+
end
350351
end
351352
end
352353

@@ -374,13 +375,24 @@ def initialize_event_processor!
374375
end
375376

376377
def perform_preflight_check!
378+
connected_es_version_info
377379
check_user_privileges!
378380
check_es_cluster_license!
379381
end
380382

381-
def check_user_privileges!
383+
def preflight_check_instance
382384
java_import('co.elastic.logstash.filters.elasticintegration.PreflightCheck')
383-
PreflightCheck.new(@elasticsearch_rest_client).checkUserPrivileges
385+
@preflight_check ||= PreflightCheck.new(@elasticsearch_rest_client)
386+
end
387+
388+
def connected_es_version_info
389+
@connected_es_version_info ||= preflight_check_instance.getElasticsearchVersionInfo
390+
rescue => e
391+
raise_config_error!(e.message)
392+
end
393+
394+
def check_user_privileges!
395+
preflight_check_instance.checkUserPrivileges
384396
rescue => e
385397
security_error_message = "no handler found for uri [/_security/user/_has_privileges]"
386398
if e.message.include?(security_error_message)
@@ -403,17 +415,13 @@ def check_user_privileges!
403415
end
404416

405417
def check_es_cluster_license!
406-
java_import('co.elastic.logstash.filters.elasticintegration.PreflightCheck')
407-
PreflightCheck.new(@elasticsearch_rest_client).checkLicense
418+
preflight_check_instance.checkLicense
408419
rescue => e
409420
raise_config_error!(e.message)
410421
end
411422

412423
def serverless?
413-
java_import('co.elastic.logstash.filters.elasticintegration.PreflightCheck')
414-
PreflightCheck.new(@elasticsearch_rest_client).isServerless
415-
rescue => e
416-
raise_config_error!(e.message)
424+
connected_es_version_info["build_flavor"] == 'serverless'
417425
end
418426

419427
##
@@ -454,4 +462,44 @@ def ensure_java_major_version!(minimum_major_version)
454462
ERR
455463
end
456464
end
465+
466+
##
467+
# compares the current plugin version with the Elasticsearch version connected to
468+
# generates a warning or info message based on the situation where the plugin is ahead or behind of the connected Elasticsearch
469+
def check_versions_alignment
470+
plugin_major_version, plugin_minor_version = VERSION.split('.').map(&:to_i)
471+
es_major_version, es_minor_version = connected_es_version_info["number"].split('.').first(2).map(&:to_i)
472+
473+
logger.info("This #{VERSION} version of plugin embedded Ingest node components from Elasticsearch #{plugin_major_version}.#{plugin_minor_version}")
474+
475+
es_full_version = connected_es_version_info["number"]
476+
477+
if es_major_version > plugin_major_version
478+
logger.warn "This plugin v#{VERSION} is connected to a newer MAJOR " +
479+
"version of Elasticsearch v#{es_full_version}, and may " +
480+
"have trouble loading or running pipelines that use new " +
481+
"features; for the best experience, update this plugin " +
482+
"to at least v#{es_major_version}.#{es_minor_version}."
483+
elsif es_major_version < plugin_major_version
484+
logger.warn "This plugin v#{VERSION} is connected to an older MAJOR " +
485+
"version of Elasticsearch v#{es_full_version}, and may " +
486+
"have trouble loading or running pipelines that use " +
487+
"features that were deprecated before Elasticsearch " +
488+
"v#{plugin_major_version}.0; for the best experience, " +
489+
"align major/minor versions across the Elastic Stack."
490+
elsif es_minor_version > plugin_minor_version
491+
logger.warn "This plugin v#{VERSION} is connected to a newer MINOR " +
492+
"version of Elasticsearch v#{es_full_version}, and may " +
493+
"have trouble loading or running pipelines that use new " +
494+
"features; for the best experience, update this plugin to " +
495+
"at least v#{es_major_version}.#{es_minor_version}."
496+
elsif es_minor_version < plugin_minor_version
497+
logger.info "This plugin v#{VERSION} is connected to an older MINOR " +
498+
"version of Elasticsearch v#{es_full_version}; for the best experience, " +
499+
"align major/minor versions across the Elastic Stack."
500+
else
501+
logger.debug "This plugin v#{VERSION} is connected to the same MAJOR/MINOR " +
502+
"version of Elasticsearch v#{es_full_version}."
503+
end
504+
end
457505
end

spec/integration/elastic_integration_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,7 @@ def path; @path; end
13001300
# plugin register fails
13011301
expected_message = "The cluster privilege `monitor` is REQUIRED in order to validate Elasticsearch license"
13021302
expect(subject).to receive(:serverless?).and_return(false)
1303+
expect(subject).to receive(:connected_es_version_info).and_return({'number' => LogStash::Filters::ElasticIntegration::VERSION, 'build_flavor' => 'default'})
13031304
expect{ subject.register }.to raise_error(LogStash::ConfigurationError).with_message(expected_message)
13041305

13051306
# send event and check

spec/unit/elastic_integration_spec.rb

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
subject(:plugin) { LogStash::Filters::ElasticIntegration.new(config) }
1717

1818
let(:mock_geoip_database_manager) { double("LogStash::GeoipDatabaseManagement::Manager", :enabled? => false) }
19-
before(:each) { allow(plugin).to receive(:load_geoip_database_manager!).and_return(mock_geoip_database_manager) }
19+
before(:each) {
20+
allow(plugin).to receive(:load_geoip_database_manager!).and_return(mock_geoip_database_manager)
21+
allow(plugin).to receive(:logger).and_return(mock_logger)
22+
}
23+
24+
let(:mock_logger) { double("Logger").as_null_object }
2025

2126
describe 'the plugin class' do
2227
subject { described_class }
@@ -29,7 +34,7 @@
2934
allow(java.lang.System).to receive(:getProperty).with("java.specification.version").and_return("11.0.16.1")
3035
end
3136

32-
it 'prevents initialization and presents helpful guidancee' do
37+
it 'prevents initialization and presents helpful guidance' do
3338
expect { described_class.new({}) }.to raise_error(LogStash::EnvironmentError)
3439
.with_message(including("requires Java 17 or later", # reason +
3540
"current JVM version `11.0.16.1`",
@@ -65,12 +70,16 @@
6570

6671
describe "plugin register" do
6772

68-
before(:each) { allow(plugin).to receive(:perform_preflight_check!).and_return(true) }
73+
before(:each) { allow(plugin).to receive(:connected_es_version_info).and_return(connected_es_version_info) }
74+
before(:each) { allow(plugin).to receive(:check_user_privileges!).and_return(true) }
75+
before(:each) { allow(plugin).to receive(:check_es_cluster_license!).and_return(true) }
6976
before(:each) { allow(plugin).to receive(:serverless?).and_return(false) }
7077

7178
let(:registered_plugin) { plugin.tap(&:register) }
7279
after(:each) { plugin.close }
7380

81+
let(:connected_es_version_info) { {'number' => '8.17.0', 'build_flavor' => 'default'} }
82+
7483
shared_examples "validate `ssl_enabled`" do
7584
it "enables SSL" do
7685
expect(registered_plugin.ssl_enabled).to be_truthy
@@ -722,5 +731,80 @@
722731
end
723732
end
724733

734+
describe "plugin vs connected ES versions compatibility" do
735+
let(:config) { super().merge("hosts" => %w[127.0.0.2:9300]) }
736+
let(:version) { LogStash::Filters::ElasticIntegration::VERSION }
737+
let(:plugin_major_version) { version.split('.').first.to_i }
738+
let(:plugin_minor_version) { version.split('.')[1].to_i }
739+
let(:base_message) { "This #{version} version of plugin embedded Ingest node components from Elasticsearch #{plugin_major_version}.#{plugin_minor_version}" }
740+
let(:expected_message) { }
741+
742+
before(:each) { plugin.tap(&:check_versions_alignment) }
743+
744+
it "informs which version of ES the plugin is built from" do
745+
expected_message =
746+
"This plugin v#{version} is connected to the same MAJOR/MINOR version " +
747+
"of Elasticsearch v#{connected_es_version_info['number']}."
748+
749+
expect(mock_logger).to have_received(:info).with(base_message)
750+
expect(mock_logger).to have_received(:debug).with(expected_message)
751+
end
752+
753+
shared_examples "version mismatch" do |version_type, ahead_or_behind, first_log_level, second_log_level|
754+
let(:connected_es_version_info) do
755+
major = plugin_major_version
756+
minor = plugin_minor_version
757+
major += ahead_or_behind == :ahead ? -1 : 1 if version_type == :major
758+
minor += ahead_or_behind == :ahead ? -1 : 1 if version_type == :minor
759+
{ "number" => "#{major}.#{minor}.10", "major" => "#{major}", "minor" => "#{minor}", "build_flavor" => "default" }
760+
end
761+
762+
it "informs to update" do
763+
expect(mock_logger).to have_received(first_log_level).with(base_message)
764+
expect(mock_logger).to have_received(second_log_level).with(expected_message)
765+
end
766+
end
767+
768+
context "plugin major version is behind" do
769+
let(:expected_message) {
770+
"This plugin v#{version} is connected to a newer MAJOR version of " +
771+
"Elasticsearch v#{connected_es_version_info['number']}, and may have trouble loading or " +
772+
"running pipelines that use new features; for the best experience, " +
773+
"update this plugin to at least v#{connected_es_version_info['major']}.#{connected_es_version_info['minor']}."
774+
}
775+
include_examples "version mismatch", :major, :behind, :info, :warn
776+
end
777+
778+
context "plugin major version is ahead" do
779+
let(:expected_message) {
780+
"This plugin v#{version} is connected to an older MAJOR version of " +
781+
"Elasticsearch v#{connected_es_version_info['number']}, and may have trouble loading or " +
782+
"running pipelines that use features that were deprecated before " +
783+
"Elasticsearch v#{plugin_major_version}.0; for the best experience, " +
784+
"align major/minor versions across the Elastic Stack."
785+
}
786+
include_examples "version mismatch", :major, :ahead, :info, :warn
787+
end
788+
789+
context "plugin minor version is behind" do
790+
let(:expected_message) {
791+
"This plugin v#{version} is connected to a newer MINOR version of " +
792+
"Elasticsearch v#{connected_es_version_info['number']}, and may have trouble loading or " +
793+
"running pipelines that use new features; for the best experience, " +
794+
"update this plugin to at least v#{connected_es_version_info['major']}.#{connected_es_version_info['minor']}."
795+
}
796+
include_examples "version mismatch", :minor, :behind, :info, :warn
797+
end
798+
799+
context "plugin minor version is ahead" do
800+
let(:expected_message) {
801+
"This plugin v#{version} is connected to an older MINOR version of " +
802+
"Elasticsearch v#{connected_es_version_info['number']}; for the best experience, " +
803+
"align major/minor versions across the Elastic Stack."
804+
}
805+
include_examples "version mismatch", :minor, :ahead, :info, :info
806+
end
807+
808+
end
725809
end
726810
end

src/main/java/co/elastic/logstash/filters/elasticintegration/PreflightCheck.java

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,38 @@ public PreflightCheck(final RestClient elasticsearchRestClient) {
5050
this.elasticsearchRestClient = elasticsearchRestClient;
5151
}
5252

53+
/**
54+
* Get Elasticsearch cluster info and store version number and build flavor.
55+
* @return {@link Map} containing
56+
* <ul>
57+
* <li>"number": the key for a version number (e.g., "8.17.0")</li>
58+
* <li>"build_flavor": the key for a build flavor (e.g., "default" "oss" or "serverless")</li>
59+
* </ul>
60+
* Throws {@link Failure} if an error occurs while sending the request, parsing the response or extracting the
61+
* required information.
62+
*/
63+
public Map<String, String> getElasticsearchVersionInfo() {
64+
try {
65+
final Request request = new Request("GET", "/");
66+
final Response response = elasticsearchRestClient.performRequest(request);
67+
68+
final String resBody = new String(response.getEntity().getContent().readAllBytes(), StandardCharsets.UTF_8);
69+
logger.debug(() -> String.format("Elasticsearch '/' RAW: %s", resBody));
70+
71+
final JsonNode versionNode = OBJECT_MAPPER.readTree(resBody).get("version");
72+
73+
final String elasticsearchVersion = versionNode.get("number").asText();
74+
logger.info(String.format("Connected to Elasticsearch version: %s", elasticsearchVersion));
75+
76+
final String elasticsearchBuildFlavor = versionNode.get("build_flavor").asText();
77+
logger.info(String.format("Elasticsearch build_flavor: %s", elasticsearchBuildFlavor));
78+
79+
return Map.of("number", elasticsearchVersion, "build_flavor", elasticsearchBuildFlavor);
80+
} catch (Exception e) {
81+
throw new Failure(String.format("Fetching Elasticsearch version information failed: %s", e.getMessage()), e);
82+
}
83+
}
84+
5385
public void checkUserPrivileges() {
5486
try {
5587
final Request hasPrivilegesRequest = new Request("POST", "/_security/user/_has_privileges");
@@ -105,26 +137,6 @@ public void checkLicense() {
105137
}
106138
}
107139

108-
public boolean isServerless() {
109-
try {
110-
final Request req = new Request("GET", "/");
111-
final Response res = elasticsearchRestClient.performRequest(req);
112-
113-
final String resBody = new String(res.getEntity().getContent().readAllBytes(), StandardCharsets.UTF_8);
114-
logger.debug(() -> String.format("Elasticsearch '/' RAW: %s", resBody));
115-
116-
final JsonNode versionNode = OBJECT_MAPPER.readTree(resBody).get("version");
117-
118-
final String buildFlavor = versionNode.get("build_flavor").asText();
119-
logger.info(String.format("Elasticsearch build_flavor: %s", buildFlavor));
120-
121-
return buildFlavor.equals("serverless");
122-
} catch (Exception e) {
123-
logger.error(String.format("Exception checking serverless: %s", e.getMessage()));
124-
throw new Failure(String.format("Preflight check failed: %s", e.getMessage()), e);
125-
}
126-
}
127-
128140
public static class Failure extends RuntimeException {
129141
public Failure(String message, Throwable cause) {
130142
super(message, cause);

src/test/java/co/elastic/logstash/filters/elasticintegration/ElasticsearchRestClientBuilderTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ public void testElasticApiConfigAddsHeaders() throws HttpException, IOException
156156
for (HttpRequestInterceptor interceptor : interceptors) {
157157
interceptor.process(request, null);
158158
}
159+
assertThat(request.getFirstHeader("Elastic-Api-Version").getValue(),
160+
is("2023-10-31"));
159161
assertThat(request.getFirstHeader("x-elastic-product-origin").getValue(),
160162
is("logstash-filter-elastic_integration"));
161163
}

0 commit comments

Comments
 (0)