diff --git a/.github/workflows/issue-auto-closer.yml b/.github/workflows/issue-auto-closer.yml
deleted file mode 100644
index c1497bc4..00000000
--- a/.github/workflows/issue-auto-closer.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-name: Autocloser
-on: [issues]
-jobs:
- autoclose:
- runs-on: ubuntu-latest
- steps:
- - name: Autoclose issues that did not follow issue template
- uses: roots/issue-closer-action@v1.2
- with:
- repo-token: ${{ secrets.GITHUB_TOKEN }}
- issue-close-message: "@${issue.user.login} this issue was automatically closed because it did not follow the issue template."
- issue-pattern: "(.*Problem.*)|(.*Expected Behavior or What you need to ask.*)|(.*Using Fluentd and ES plugin versions.*)|(.*Is your feature request related to a problem? Please describe.*)|(.*Describe the solution you'd like.*)|(.*Describe alternatives you've considered.*)"
diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml
index e0d1e39c..0f3cdf0f 100644
--- a/.github/workflows/linux.yml
+++ b/.github/workflows/linux.yml
@@ -1,6 +1,5 @@
name: Testing on Ubuntu
on:
- - push
- pull_request
permissions:
contents: read
diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml
deleted file mode 100644
index 2cbc2b2b..00000000
--- a/.github/workflows/macos.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-name: Testing on macOS
-on:
- - push
- - pull_request
-permissions:
- contents: read
-
-jobs:
- build:
- runs-on: ${{ matrix.os }}
- strategy:
- fail-fast: false
- matrix:
- ruby: [ '3.0', '3.1', '3.2', '3.3', '3.4' ]
- os:
- - macOS-latest
- name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }}
- steps:
- - uses: actions/checkout@v5
- - uses: ruby/setup-ruby@v1
- with:
- ruby-version: ${{ matrix.ruby }}
- - name: unit testing
- env:
- CI: true
- run: |
- gem install bundler rake
- bundle install --jobs 4 --retry 3
- bundle exec rake test
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
deleted file mode 100644
index 19eec108..00000000
--- a/.github/workflows/windows.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-name: Testing on Windows
-on:
- - push
- - pull_request
-permissions:
- contents: read
-
-jobs:
- build:
- runs-on: ${{ matrix.os }}
- strategy:
- fail-fast: false
- matrix:
- ruby: [ '3.0', '3.1', '3.2', '3.3', '3.4' ]
- os:
- - windows-latest
- name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }}
- steps:
- - uses: actions/checkout@v5
- - uses: ruby/setup-ruby@v1
- with:
- ruby-version: ${{ matrix.ruby }}
- - name: unit testing
- env:
- CI: true
- run: |
- gem install bundler rake
- bundle install --jobs 4 --retry 3
- bundle exec rake test
diff --git a/README.compat.md b/README.compat.md
new file mode 100644
index 00000000..7457e5a8
--- /dev/null
+++ b/README.compat.md
@@ -0,0 +1,77 @@
+# Compat
+
+## Mixed Elasticsearch Version Environments
+
+### Compatibility Matrix
+
+| Elasticsearch Gem | ES 7.x Server | ES 8.x Server | ES 9.x Server |
+|-------------------|---------------|---------------|---------------|
+| gem 7.17.x | ✅ Works | ✅ Works | ❌ No |
+| gem 8.15.x | ✅ Works | ✅ Works | ❌ No |
+| gem 9.1.x + patch | ✅ With config| ✅ With config| ✅ Works |
+
+### Configuration Options
+
+#### force_content_type
+
+Type: String
+Default: nil (auto-detect)
+Valid values: "application/json", "application/x-ndjson"
+Manually override the Content-Type header. Required when using elasticsearch gem 9.x with ES 7.x or 8.x servers.
+
+```ruby
+
+ @type elasticsearch
+ force_content_type application/json
+
+```
+
+#### ignore_version_content_type_mismatch
+
+Type: Bool
+Default: false
+Automatically fallback to application/json if Content-Type version mismatch occurs. Enables seamless operation across mixed ES 7/8/9 environments.
+
+Example:
+
+```ruby
+
+ @type elasticsearch
+ force_content_type application/json
+ ignore_version_content_type_mismath true
+
+```
+
+### Recommended Configuration
+
+#### For ES 7/8 environments (Recommended)
+
+Use elasticsearch gem 8.x - works with both versions, no configuration needed:
+
+```ruby
+# Gemfile
+gem 'elasticsearch', '~> 8.15.0'
+
+# fluent.conf
+
+ @type elasticsearch
+ hosts es7:9200,es8:9200
+ # No special config needed!
+
+```
+
+#### For gem 9.x with ES 7/8 (Not recommended, but supported)
+
+```ruby
+# Gemfile
+gem 'elasticsearch', '~> 9.1.0'
+
+# fluent.conf
+
+ @type elasticsearch
+ hosts es7:9200,es8:9200
+
+ # REQUIRED: Force compatible content type
+ force_content_type application/json
+
+```
diff --git a/lib/fluent/plugin/elasticsearch_api_bugfix.rb b/lib/fluent/plugin/elasticsearch_api_bugfix.rb
new file mode 100644
index 00000000..dff59e9b
--- /dev/null
+++ b/lib/fluent/plugin/elasticsearch_api_bugfix.rb
@@ -0,0 +1,42 @@
+# Elasticsearch API 9.x Compatibility Patch
+#
+# Fixes crashes in elasticsearch-api gem 9.1.2 when connecting to ES 7.x/8.x servers.
+#
+# Bug: The gem expects ES 9 headers and crashes with NoMethodError when they're nil
+#
+# This patch is only needed if using elasticsearch gem 9.x
+# Not needed if using elasticsearch gem 7.x or 8.x
+
+require 'elasticsearch/api'
+
+module Elasticsearch
+ module API
+ module Utils
+ class << self
+ if method_defined?(:update_ndjson_headers!)
+ alias_method :original_update_ndjson_headers!, :update_ndjson_headers!
+
+ def update_ndjson_headers!(headers, client_headers)
+ return headers unless client_headers.is_a?(Hash)
+
+ current_content = client_headers.keys.find { |c| c.to_s.match?(/content[-_]?type/i) }
+ return headers unless current_content
+
+ content_value = client_headers[current_content]
+ return headers unless content_value
+
+ # ES 7/8 compatibility: Only process ES9-specific headers
+ # If no "compatible-with" present, this is ES 7/8 format
+ return headers unless content_value.to_s.include?('compatible-with')
+
+ # ES 9 detected, safe to call original
+ original_update_ndjson_headers!(headers, client_headers)
+ rescue StandardError => e
+ warn "[elasticsearch-api-compat] Failed to update headers: #{e.class} - #{e.message}"
+ headers
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fluent/plugin/out_elasticsearch.rb b/lib/fluent/plugin/out_elasticsearch.rb
index 5fef8528..ef732a57 100644
--- a/lib/fluent/plugin/out_elasticsearch.rb
+++ b/lib/fluent/plugin/out_elasticsearch.rb
@@ -27,6 +27,7 @@
require_relative 'elasticsearch_index_lifecycle_management'
require_relative 'elasticsearch_tls'
require_relative 'elasticsearch_fallback_selector'
+require_relative 'elasticsearch_api_bugfix'
begin
require_relative 'oj_serializer'
rescue LoadError
@@ -177,6 +178,12 @@ def initialize(retry_stream)
config_param :use_legacy_template, :bool, :default => true
config_param :catch_transport_exception_on_retry, :bool, :default => true
config_param :target_index_affinity, :bool, :default => false
+ config_param :force_content_type, :string, :default => nil,
+ :desc => "Force specific Content-Type header (e.g., 'application/json' or 'application/x-ndjson'). " \
+ "Overrides automatic version detection. Useful for mixed ES environments."
+ config_param :ignore_version_content_type_mismatch, :bool, :default => false,
+ :desc => "Automatically fallback to application/json if Content-Type version mismatch occurs. " \
+ "Enables seamless operation across mixed ES 7/8/9 environments."
config_section :metadata, param_name: :metainfo, multi: false do
config_param :include_chunk_id, :bool, :default => false
@@ -361,14 +368,31 @@ class << self
@type_name = nil
end
@accept_type = nil
- if @content_type != ES9_CONTENT_TYPE
+
+ # Only set ES9 content type if not overridden and mismatch handling is not enabled
+ if @content_type.to_s != ES9_CONTENT_TYPE && !@ignore_version_content_type_mismatch
log.trace "Detected ES 9.x or above: Content-Type will be adjusted."
@content_type = ES9_CONTENT_TYPE
@accept_type = ES9_CONTENT_TYPE
+ elsif @ignore_version_content_type_mismatch
+ log.info "Ignoring ES version for Content-Type, using application/json for compatibility"
+ @content_type = :'application/json'
+ @accept_type = nil
end
end
end
+ if @content_type.nil?
+ log.warn "content_type was nil, defaulting to application/json"
+ @content_type = :'application/json'
+ end
+
+ if @force_content_type
+ log.info "Forcing Content-Type to: #{@force_content_type}"
+ @content_type = @force_content_type
+ @accept_type = nil
+ end
+
if @validate_client_version && !dry_run?
if @last_seen_major_version != client_library_version.to_i
raise Fluent::ConfigError, <<-EOC
@@ -623,11 +647,17 @@ def client(host = nil, compress_connection = false)
else
{}
end
- headers = { 'Content-Type' => @content_type.to_s }
+
+ content_type_value = @content_type ? @content_type.to_s : 'application/json'
+ accept_type_value = @accept_type ? @accept_type.to_s : nil
+ content_type_value = 'application/json' if content_type_value.strip.empty?
+
+ headers = { 'Content-Type' => content_type_value }
.merge(@custom_headers)
.merge(@api_key_header)
.merge(gzip_headers)
- headers.merge!('Accept' => @accept_type) if @accept_type
+
+ headers.merge!('Accept' => accept_type_value) if accept_type_value && !accept_type_value.strip.empty?
ssl_options = { verify: @ssl_verify, ca_file: @ca_file}.merge(@ssl_version_options)
diff --git a/test/es-compat/docker-compose.yaml b/test/es-compat/docker-compose.yaml
new file mode 100644
index 00000000..aab77e87
--- /dev/null
+++ b/test/es-compat/docker-compose.yaml
@@ -0,0 +1,40 @@
+services:
+ elasticsearch7:
+ image: docker.elastic.co/elasticsearch/elasticsearch:7.17.16
+ container_name: es7
+ environment:
+ - discovery.type=single-node
+ - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
+ - xpack.security.enabled=false
+ ports:
+ - "9207:9200"
+ networks:
+ - elastic
+
+ elasticsearch8:
+ image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
+ container_name: es8
+ environment:
+ - discovery.type=single-node
+ - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
+ - xpack.security.enabled=false
+ ports:
+ - "9208:9200"
+ networks:
+ - elastic
+
+ elasticsearch9:
+ image: docker.elastic.co/elasticsearch/elasticsearch:9.1.5
+ container_name: es9
+ environment:
+ - discovery.type=single-node
+ - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
+ - xpack.security.enabled=false
+ ports:
+ - "9209:9200"
+ networks:
+ - elastic
+
+networks:
+ elastic:
+ driver: bridge
diff --git a/test/es-compat/es-compat-test.sh b/test/es-compat/es-compat-test.sh
new file mode 100755
index 00000000..03fa8fea
--- /dev/null
+++ b/test/es-compat/es-compat-test.sh
@@ -0,0 +1,545 @@
+#!/bin/bash
+# e2e_test.sh - End-to-End Test for Elasticsearch Plugin
+# Tests fluent-plugin-elasticsearch against ES 7.x, 8.x, and 9.x
+
+set -e
+
+# Color output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+NC='\033[0m' # No Color
+
+TOTAL_TESTS=0
+PASSED_TESTS=0
+FAILED_TESTS=0
+declare -a FAILED_TEST_NAMES
+declare -a PASSED_TEST_NAMES
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+TEST_DIR="${SCRIPT_DIR}/e2e_test_temp"
+
+find_project_root() {
+ local dir="$SCRIPT_DIR"
+ while [ "$dir" != "/" ]; do
+ if [ -d "$dir/lib/fluent/plugin" ]; then
+ echo "$dir"
+ return 0
+ fi
+ dir="$(dirname "$dir")"
+ done
+
+ echo "$SCRIPT_DIR"
+}
+
+PROJECT_ROOT=$(find_project_root)
+LIB_PATH="${PROJECT_ROOT}/lib"
+
+log_info() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+log_success() {
+ echo -e "${GREEN}[✓]${NC} $1"
+}
+
+log_error() {
+ echo -e "${RED}[✗]${NC} $1"
+}
+
+log_warning() {
+ echo -e "${YELLOW}[!]${NC} $1"
+}
+
+print_banner() {
+ echo ""
+ echo -e "${CYAN}╔════════════════════════════════════════════════════════════════╗${NC}"
+ echo -e "${CYAN}║${NC} $1"
+ echo -e "${CYAN}╚════════════════════════════════════════════════════════════════╝${NC}"
+ echo ""
+}
+
+print_section() {
+ echo ""
+ echo -e "${BLUE}========================================${NC}"
+ echo -e "${BLUE}$1${NC}"
+ echo -e "${BLUE}========================================${NC}"
+}
+
+print_subsection() {
+ echo ""
+ echo -e "${YELLOW}--- $1 ---${NC}"
+}
+
+increment_test() {
+ TOTAL_TESTS=$((TOTAL_TESTS + 1))
+}
+
+pass_test() {
+ PASSED_TESTS=$((PASSED_TESTS + 1))
+ PASSED_TEST_NAMES+=("$1")
+ log_success "$1"
+}
+
+fail_test() {
+ FAILED_TESTS=$((FAILED_TESTS + 1))
+ FAILED_TEST_NAMES+=("$1")
+ log_error "$1"
+}
+
+cleanup() {
+ print_section "Cleanup"
+
+ if [ -d "$TEST_DIR" ]; then
+ log_info "Removing test directory: $TEST_DIR"
+ rm -rf "$TEST_DIR"
+ fi
+
+ log_info "Stopping Docker containers..."
+ docker-compose down -v 2>/dev/null || true
+
+ for port in 9207 9208 9209; do
+ if curl -s "http://localhost:${port}/_cat/health" > /dev/null 2>&1; then
+ log_info "Cleaning up test indices on port ${port}..."
+ curl -s -X DELETE "http://localhost:${port}/test-*,fluentd-*" > /dev/null 2>&1 || true
+ fi
+ done
+
+ log_success "Cleanup complete"
+}
+
+trap cleanup EXIT
+
+check_prerequisites() {
+ print_section "Checking Prerequisites"
+
+ local missing_deps=()
+
+ if ! command -v docker &> /dev/null; then
+ missing_deps+=("docker")
+ fi
+
+ if ! command -v docker-compose &> /dev/null; then
+ missing_deps+=("docker-compose")
+ fi
+
+ if ! command -v curl &> /dev/null; then
+ missing_deps+=("curl")
+ fi
+
+ if ! command -v jq &> /dev/null; then
+ missing_deps+=("jq")
+ fi
+
+ if ! command -v bundle &> /dev/null; then
+ missing_deps+=("bundle")
+ fi
+
+ if [ ${#missing_deps[@]} -ne 0 ]; then
+ log_error "Missing required dependencies: ${missing_deps[*]}"
+ log_info "Please install missing dependencies and try again"
+ exit 1
+ fi
+
+ if [ ! -d "$LIB_PATH/fluent/plugin" ]; then
+ log_error "Cannot find lib/fluent/plugin directory"
+ log_error "Expected at: $LIB_PATH"
+ log_error "Please run this script from the project root or a subdirectory"
+ exit 1
+ fi
+
+ local es_version=$(bundle exec ruby -e "require 'elasticsearch'; puts Elasticsearch::VERSION" 2>/dev/null || echo "unknown")
+ log_info "Elasticsearch gem version: $es_version"
+
+ local gem_major=$(echo "$es_version" | cut -d. -f1)
+
+ log_success "All prerequisites satisfied"
+ log_info "Project root: $PROJECT_ROOT"
+ log_info "Lib path: $LIB_PATH"
+}
+
+setup_test_environment() {
+ print_section "Setting Up Test Environment"
+
+ mkdir -p "$TEST_DIR"
+ log_success "Created test directory: $TEST_DIR"
+}
+
+start_elasticsearch() {
+ print_section "Starting Elasticsearch Containers"
+
+ log_info "Starting Docker Compose..."
+ docker-compose up -d
+
+ log_info "Waiting for Elasticsearch instances to be ready..."
+
+ local ports=(9207 9208 9209)
+ local names=("ES 7.x" "ES 8.x" "ES 9.x")
+
+ for i in "${!ports[@]}"; do
+ local port=${ports[$i]}
+ local name=${names[$i]}
+ local max_attempts=60
+ local attempt=0
+
+ log_info "Checking ${name} on port ${port}..."
+
+ while [ $attempt -lt $max_attempts ]; do
+ if curl -s "http://localhost:${port}/_cluster/health" > /dev/null 2>&1; then
+ local version=$(curl -s "http://localhost:${port}" | jq -r '.version.number' 2>/dev/null || echo "unknown")
+ log_success "${name} is ready (version: ${version})"
+ break
+ fi
+ attempt=$((attempt + 1))
+ if [ $((attempt % 10)) -eq 0 ]; then
+ echo -n "."
+ fi
+ sleep 1
+ done
+
+ if [ $attempt -eq $max_attempts ]; then
+ log_error "${name} failed to start after ${max_attempts} seconds"
+ exit 1
+ fi
+ done
+
+ echo ""
+}
+
+create_test_script() {
+ local es_version=$1
+ local es_port=$2
+ local test_name=$3
+ local extra_config=$4
+
+ cat > "${TEST_DIR}/test_${test_name}.rb" << EOF
+require 'bundler/setup'
+require 'fluent/test'
+require 'fluent/test/driver/output'
+
+# Add lib directory to load path
+\$LOAD_PATH.unshift('${LIB_PATH}')
+require 'fluent/plugin/out_elasticsearch'
+
+config = %[
+ host localhost
+ port ${es_port}
+ logstash_format true
+ logstash_prefix test-${test_name}
+ type_name _doc
+ ${extra_config}
+]
+
+driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::ElasticsearchOutput).configure(config)
+
+begin
+ driver.run(default_tag: 'test') do
+ driver.feed(Time.now.to_i, {
+ "message" => "Test message for ${test_name}",
+ "version" => "${es_version}",
+ "test_name" => "${test_name}",
+ "timestamp" => Time.now.iso8601
+ })
+ end
+ puts "SUCCESS"
+ exit 0
+rescue => e
+ puts "FAILED: \#{e.class}: \#{e.message}"
+ puts e.backtrace.first(5).join("\n") if ENV['DEBUG']
+ exit 1
+end
+EOF
+}
+
+run_test() {
+ local test_name=$1
+ local es_version=$2
+ local es_port=$3
+ local extra_config=$4
+
+ increment_test
+
+ create_test_script "$es_version" "$es_port" "$test_name" "$extra_config"
+
+ if timeout 30 bundle exec ruby "${TEST_DIR}/test_${test_name}.rb" > "${TEST_DIR}/${test_name}.log" 2>&1; then
+ pass_test "${test_name}"
+ return 0
+ else
+ fail_test "${test_name}"
+ if [ -f "${TEST_DIR}/${test_name}.log" ]; then
+ # Show abbreviated error
+ local error_msg=$(grep -E "FAILED:|Error|error" "${TEST_DIR}/${test_name}.log" | head -3)
+ if [ -n "$error_msg" ]; then
+ echo " └─ $error_msg"
+ fi
+ fi
+ return 1
+ fi
+}
+
+verify_data() {
+ local port=$1
+ local index_pattern=$2
+ local expected_count=$3
+ local test_description=$4
+
+ increment_test
+
+ sleep 2
+
+ local result=$(curl -s "http://localhost:${port}/${index_pattern}/_search?size=0" || echo '{}')
+ local actual_count=$(echo "$result" | jq -r '.hits.total.value // .hits.total // 0' 2>/dev/null || echo "0")
+
+ if [ "$actual_count" -ge "$expected_count" ]; then
+ pass_test "${test_description}: Found ${actual_count} docs"
+ return 0
+ else
+ fail_test "${test_description}: Found ${actual_count} docs (expected >= ${expected_count})"
+ return 1
+ fi
+}
+
+test_baseline() {
+ print_subsection "Test 1: Baseline - Default Configuration"
+
+ run_test "es7-baseline" "7" "9207" ""
+ verify_data "9207" "test-es7-baseline-*" 1 "ES7 Baseline Data"
+
+ run_test "es8-baseline" "8" "9208" ""
+ verify_data "9208" "test-es8-baseline-*" 1 "ES8 Baseline Data"
+
+ run_test "es9-baseline" "9" "9209" ""
+ verify_data "9209" "test-es9-baseline-*" 1 "ES9 Baseline Data"
+}
+
+test_without_logstash_format() {
+ print_subsection "Test 2: Without Logstash Format"
+
+ local config="logstash_format false
+ index_name test-direct"
+
+ run_test "es7-direct" "7" "9207" "$config"
+ verify_data "9207" "test-direct" 1 "ES7 Direct Index"
+
+ run_test "es8-direct" "8" "9208" "$config"
+ verify_data "9208" "test-direct" 1 "ES8 Direct Index"
+
+ run_test "es9-direct" "9" "9209" "$config"
+ verify_data "9209" "test-direct" 1 "ES9 Direct Index"
+}
+
+test_bulk_writes() {
+ print_subsection "Test 3: Bulk Writes (10 documents)"
+
+ for es_ver in 7 8 9; do
+ local port=$((9207 + es_ver - 7))
+ local test_name="es${es_ver}-bulk"
+
+ increment_test
+
+ cat > "${TEST_DIR}/test_${test_name}.rb" << EOF
+require 'bundler/setup'
+require 'fluent/test'
+require 'fluent/test/driver/output'
+
+\$LOAD_PATH.unshift('${LIB_PATH}')
+require 'fluent/plugin/out_elasticsearch'
+
+config = %[
+ host localhost
+ port ${port}
+ logstash_format true
+ logstash_prefix test-${test_name}
+ type_name _doc
+]
+
+driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::ElasticsearchOutput).configure(config)
+
+begin
+ driver.run(default_tag: 'test') do
+ 10.times do |i|
+ driver.feed(Time.now.to_i, {
+ "message" => "Bulk message \#{i}",
+ "index" => i,
+ "version" => "${es_ver}",
+ "test_name" => "${test_name}"
+ })
+ end
+ end
+ puts "SUCCESS"
+ exit 0
+rescue => e
+ puts "FAILED: \#{e.class}: \#{e.message}"
+ exit 1
+end
+EOF
+
+ if timeout 30 bundle exec ruby "${TEST_DIR}/test_${test_name}.rb" > "${TEST_DIR}/${test_name}.log" 2>&1; then
+ pass_test "${test_name}"
+ else
+ fail_test "${test_name}"
+ fi
+
+ verify_data "$port" "test-${test_name}-*" 10 "ES${es_ver} Bulk Data"
+ done
+}
+
+show_data_summary() {
+ print_section "Data Summary in Elasticsearch"
+
+ for es_ver in 7 8 9; do
+ local port=$((9207 + es_ver - 7))
+ print_subsection "ES ${es_ver}.x (port ${port})"
+
+ local indices=$(curl -s "http://localhost:${port}/_cat/indices/test-*?h=index,docs.count,store.size" 2>/dev/null || echo "")
+
+ if [ -n "$indices" ]; then
+ echo "$indices" | while read -r line; do
+ echo " ${line}"
+ done
+ else
+ echo " No test indices found"
+ fi
+ done
+}
+
+create_compatibility_report() {
+ print_section "Compatibility Report"
+
+ local gem_version=$(bundle exec ruby -e "require 'elasticsearch'; puts Elasticsearch::VERSION" 2>/dev/null || echo "unknown")
+
+ echo ""
+ echo "Gem Version: elasticsearch ${gem_version}"
+ echo ""
+
+ local es7_pass=0
+ local es7_fail=0
+ local es8_pass=0
+ local es8_fail=0
+ local es9_pass=0
+ local es9_fail=0
+
+ for test_name in "${PASSED_TEST_NAMES[@]}"; do
+ if [[ "$test_name" == *"es7"* ]]; then
+ es7_pass=$((es7_pass + 1))
+ elif [[ "$test_name" == *"es8"* ]]; then
+ es8_pass=$((es8_pass + 1))
+ elif [[ "$test_name" == *"es9"* ]]; then
+ es9_pass=$((es9_pass + 1))
+ fi
+ done
+
+ for test_name in "${FAILED_TEST_NAMES[@]}"; do
+ if [[ "$test_name" == *"es7"* ]]; then
+ es7_fail=$((es7_fail + 1))
+ elif [[ "$test_name" == *"es8"* ]]; then
+ es8_fail=$((es8_fail + 1))
+ elif [[ "$test_name" == *"es9"* ]]; then
+ es9_fail=$((es9_fail + 1))
+ fi
+ done
+
+ printf "%-15s | %-6s | %-6s | %-10s\n" "ES Version" "Passed" "Failed" "Status"
+ printf "%-15s-+-%-6s-+-%-6s-+-%-10s\n" "---------------" "------" "------" "----------"
+
+ local es7_status="✓ OK"
+ [ $es7_fail -gt 0 ] && es7_status="✗ FAILED"
+ printf "%-15s | %-6s | %-6s | %-10s\n" "ES 7.x" "$es7_pass" "$es7_fail" "$es7_status"
+
+ local es8_status="✓ OK"
+ [ $es8_fail -gt 0 ] && es8_status="✗ FAILED"
+ printf "%-15s | %-6s | %-6s | %-10s\n" "ES 8.x" "$es8_pass" "$es8_fail" "$es8_status"
+
+ local es9_status="✓ OK"
+ [ $es9_fail -gt 0 ] && es9_status="✗ FAILED"
+ printf "%-15s | %-6s | %-6s | %-10s\n" "ES 9.x" "$es9_pass" "$es9_fail" "$es9_status"
+
+ echo ""
+
+ if [ $es7_fail -gt 0 ] || [ $es8_fail -gt 0 ]; then
+ log_warning "Some tests failed with ES 7.x/8.x"
+ if [[ "$gem_version" == 9.* ]]; then
+ log_warning "You're using elasticsearch gem 9.x which has compatibility issues with ES 7/8"
+ log_info "Recommendation: Use elasticsearch gem ~> 7.17 or ~> 8.x for ES 7/8 servers"
+ fi
+ fi
+
+ if [ $es9_fail -gt 0 ]; then
+ log_warning "Some tests failed with ES 9.x"
+ if [[ "$gem_version" == 7.* ]] || [[ "$gem_version" == 8.* ]]; then
+ log_info "Note: ES 9.x is not released yet, testing against ES 8.x placeholder"
+ fi
+ fi
+}
+
+print_summary() {
+ print_banner "Test Results Summary"
+
+ echo -e "${CYAN}Total Tests:${NC} ${TOTAL_TESTS}"
+ echo -e "${GREEN}Passed:${NC} ${PASSED_TESTS}"
+ echo -e "${RED}Failed:${NC} ${FAILED_TESTS}"
+
+ local pass_rate=0
+ if [ $TOTAL_TESTS -gt 0 ]; then
+ pass_rate=$((PASSED_TESTS * 100 / TOTAL_TESTS))
+ fi
+
+ echo -e "${CYAN}Pass Rate:${NC} ${pass_rate}%"
+
+ if [ $FAILED_TESTS -gt 0 ]; then
+ echo ""
+ echo -e "${RED}Failed Tests:${NC}"
+ for test_name in "${FAILED_TEST_NAMES[@]}"; do
+ echo -e " ${RED}✗${NC} ${test_name}"
+ if [ -f "${TEST_DIR}/${test_name}.log" ]; then
+ echo -e " └─ Log: ${TEST_DIR}/${test_name}.log"
+ fi
+ done
+ echo ""
+ echo -e "${YELLOW}To view detailed logs:${NC}"
+ echo " cat ${TEST_DIR}/.log"
+ fi
+
+ echo ""
+
+ create_compatibility_report
+
+ if [ $FAILED_TESTS -eq 0 ]; then
+ echo -e "${GREEN}╔════════════════════════════════════╗${NC}"
+ echo -e "${GREEN}║ 🎉 ALL TESTS PASSED! 🎉 ║${NC}"
+ echo -e "${GREEN}╚════════════════════════════════════╝${NC}"
+ return 0
+ else
+ echo -e "${RED}╔════════════════════════════════════╗${NC}"
+ echo -e "${RED}║ ❌ SOME TESTS FAILED ║${NC}"
+ echo -e "${RED}╚════════════════════════════════════╝${NC}"
+ return 1
+ fi
+}
+
+main() {
+ print_banner "Elasticsearch Plugin E2E Test Suite (Vanilla)"
+
+ check_prerequisites
+ setup_test_environment
+ start_elasticsearch
+
+ print_section "Running Tests"
+
+ test_baseline
+ test_without_logstash_format
+ test_bulk_writes
+
+ show_data_summary
+ print_summary
+
+ if [ $FAILED_TESTS -gt 0 ]; then
+ exit 1
+ else
+ exit 0
+ fi
+}
+
+main