Skip to content

Commit e7116f4

Browse files
authored
Implement ECS Compatibility Mode (#952)
Adds support for ECS-compatibility mode to allow users to opt into the use of ECS-compatible templates. Part of the effort to make implicit behaviour more ECS-friendly in future releases of Logstash Related: elastic/logstash#11635 Resolves: #924
1 parent 612e52e commit e7116f4

19 files changed

+157
-32
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ target/
1212
vendor/
1313
/spec/fixtures/server.key
1414
/spec/fixtures/server.crt
15+
/lib/logstash/outputs/elasticsearch/templates/ecs-v*

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 10.6.0
2+
- Added `ecs_compatiblity` mode, for managing ECS-compatable templates [#952](https://github.com/logstash-plugins/logstash-output-elasticsearch/issue/952)
3+
14
## 10.5.1
25
- [DOC] Removed outdated compatibility notices, reworked cloud notice, and fixed formatting for `hosts` examples [#938](https://github.com/logstash-plugins/logstash-output-elasticsearch/pull/938)
36

Rakefile

+36
Original file line numberDiff line numberDiff line change
@@ -1 +1,37 @@
11
require "logstash/devutils/rake"
2+
3+
ECS_VERSIONS = {
4+
v1: 'v1.5.0'
5+
}
6+
7+
ECS_LOGSTASH_INDEX_PATTERNS = %w(
8+
ecs-logstash-*
9+
)
10+
11+
task :'vendor-ecs-schemata' do
12+
download_ecs_schema(:v1, 6)
13+
download_ecs_schema(:v1, 7)
14+
end
15+
task :vendor => :'vendor-ecs-schemata'
16+
17+
def download_ecs_schema(ecs_major_version, es_major)
18+
$stderr.puts("Vendoring ECS #{ecs_major_version} template for Elasticsearch #{es_major}")
19+
require 'net/http'
20+
require 'json'
21+
Net::HTTP.start('raw.githubusercontent.com', :use_ssl => true) do |http|
22+
ecs_release_tag = ECS_VERSIONS.fetch(ecs_major_version)
23+
response = http.get("/elastic/ecs/#{ecs_release_tag}/generated/elasticsearch/#{es_major}/template.json")
24+
fail "#{response.code} #{response.message}" unless (200...300).cover?(response.code.to_i)
25+
template_directory = File.expand_path("../lib/logstash/outputs/elasticsearch/templates/ecs-#{ecs_major_version}", __FILE__)
26+
Dir.mkdir(template_directory) unless File.exists?(template_directory)
27+
File.open(File.join(template_directory, "/elasticsearch-#{es_major}x.json"), "w") do |handle|
28+
handle.write(replace_index_patterns(response.body, ECS_LOGSTASH_INDEX_PATTERNS))
29+
end
30+
end
31+
end
32+
33+
def replace_index_patterns(template_json, replacement_index_patterns)
34+
template_obj = JSON.load(template_json)
35+
template_obj.update('index_patterns' => replacement_index_patterns)
36+
JSON.pretty_generate(template_obj)
37+
end

docs/index.asciidoc

+46-4
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,20 @@ https://www.elastic.co/cloud/elasticsearch-service[hosted {es} Service] on
5858
Elastic Cloud. The Elasticsearch Service is available on AWS, Google Cloud
5959
Platform, and Microsoft Azure. {ess-trial}[Try the {es} Service for free].
6060

61+
==== Compatibility with the Elastic Common Schema (ECS)
62+
63+
This plugin will persist events to Elasticsearch in the shape produced by
64+
your pipeline, and _cannot_ be used to re-shape the event structure into a
65+
shape that complies with ECS. To produce events that fully comply with ECS,
66+
you will need to populate ECS-defined fields throughout your pipeline
67+
definition.
68+
69+
However, the Elasticsearch Index Templates it manages can be configured to
70+
be ECS-compatible by setting <<plugins-{type}s-{plugin}-ecs_compatibility>>.
71+
By having an ECS-compatible template in place, we can ensure that Elasticsearch
72+
is prepared to create and index fields in a way that is compatible with ECS,
73+
and will correctly reject events with fields that conflict and cannot be coerced.
74+
6175
==== Writing to different indices: best practices
6276

6377
[NOTE]
@@ -234,6 +248,7 @@ This plugin supports the following configuration options plus the <<plugins-{typ
234248
| <<plugins-{type}s-{plugin}-doc_as_upsert>> |<<boolean,boolean>>|No
235249
| <<plugins-{type}s-{plugin}-document_id>> |<<string,string>>|No
236250
| <<plugins-{type}s-{plugin}-document_type>> |<<string,string>>|No
251+
| <<plugins-{type}s-{plugin}-ecs_compatibility>> | <<string,string>>|No
237252
| <<plugins-{type}s-{plugin}-failure_type_logging_whitelist>> |<<array,array>>|No
238253
| <<plugins-{type}s-{plugin}-healthcheck_path>> |<<string,string>>|No
239254
| <<plugins-{type}s-{plugin}-hosts>> |<<uri,uri>>|No
@@ -393,6 +408,25 @@ If you don't set a value for this option:
393408
- for elasticsearch clusters 6.x: the value of 'doc' will be used;
394409
- for elasticsearch clusters 5.x and below: the event's 'type' field will be used, if the field is not present the value of 'doc' will be used.
395410

411+
[id="plugins-{type}s-{plugin}-ecs_compatibility"]
412+
===== `ecs_compatibility`
413+
414+
* Value type is <<string,string>>
415+
* Supported values are:
416+
** `disabled`: does not provide ECS-compatible templates
417+
** `v1`: provides defaults that are compatible with v1 of the Elastic Common Schema
418+
* Default value depends on which version of Logstash is running:
419+
** When Logstash provides a `pipeline.ecs_compatibility` setting, its value is used as the default
420+
** Otherwise, the default value is `disabled`.
421+
422+
Controls this plugin's compatibility with the {ecs-ref}}[Elastic Common Schema (ECS)],
423+
including the installation of ECS-compatible index templates.
424+
The value of this setting affects the _default_ values of:
425+
426+
* <<plugins-{type}s-{plugin}-index>>
427+
* <<plugins-{type}s-{plugin}-template_name>>
428+
* <<plugins-{type}s-{plugin}-ilm_rollover_alias>>
429+
396430
[id="plugins-{type}s-{plugin}-failure_type_logging_whitelist"]
397431
===== `failure_type_logging_whitelist`
398432

@@ -500,7 +534,9 @@ NOTE: If this setting is specified, the policy must already exist in Elasticsear
500534
===== `ilm_rollover_alias`
501535

502536
* Value type is <<string,string>>
503-
* Default value is `logstash`
537+
* Default value depends on whether <<plugins-{type}s-{plugin}-ecs_compatibility>> is enabled:
538+
** ECS Compatibility disabled: `logstash`
539+
** ECS Compatibility enabled: `ecs-logstash`
504540

505541
The rollover alias is the alias where indices managed using Index Lifecycle Management will be written to.
506542

@@ -514,7 +550,9 @@ NOTE: `ilm_rollover_alias` does NOT support dynamic variable substitution as `in
514550
===== `index`
515551

516552
* Value type is <<string,string>>
517-
* Default value is `"logstash-%{+yyyy.MM.dd}"`
553+
* Default value depends on whether <<plugins-{type}s-{plugin}-ecs_compatibility>> is enabled:
554+
** ECS Compatibility disabled: `"logstash-%{+yyyy.MM.dd}"`
555+
** ECS Compatibility enabled: `"ecs-logstash-%{+yyyy.MM.dd}"`
518556

519557
The index to write events to. This can be dynamic using the `%{foo}` syntax.
520558
The default value will partition your indices by day so you can more easily
@@ -548,7 +586,8 @@ Set the keystore password
548586
* Default value is `true`
549587

550588
From Logstash 1.3 onwards, a template is applied to Elasticsearch during
551-
Logstash's startup if one with the name `template_name` does not already exist.
589+
Logstash's startup if one with the name <<plugins-{type}s-{plugin}-template_name>>
590+
does not already exist.
552591
By default, the contents of this template is the default template for
553592
`logstash-%{+YYYY.MM.dd}` which always matches indices based on the pattern
554593
`logstash-*`. Should you require support for other index names, or would like
@@ -799,7 +838,10 @@ If not set, the included template will be used.
799838
===== `template_name`
800839

801840
* Value type is <<string,string>>
802-
* Default value is `"logstash"`
841+
* Default value depends on whether <<plugins-{type}s-{plugin}-ecs_compatibility>> is enabled:
842+
** ECS Compatibility disabled: `logstash`
843+
** ECS Compatibility enabled: `ecs-logstash`
844+
803845

804846
This configuration option defines how the template is named inside Elasticsearch.
805847
Note that if you have used the template management features and subsequently

lib/logstash/outputs/elasticsearch.rb

+33
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ class LogStash::Outputs::ElasticSearch < LogStash::Outputs::Base
9292
require "logstash/outputs/elasticsearch/common"
9393
require "logstash/outputs/elasticsearch/ilm"
9494

95+
require 'logstash/plugin_mixins/ecs_compatibility_support'
96+
9597
# Protocol agnostic (i.e. non-http, non-java specific) configs go here
9698
include(LogStash::Outputs::ElasticSearch::CommonConfigs)
9799

@@ -101,6 +103,9 @@ class LogStash::Outputs::ElasticSearch < LogStash::Outputs::Base
101103
# Methods for ILM support
102104
include(LogStash::Outputs::ElasticSearch::Ilm)
103105

106+
# ecs_compatibility option, provided by Logstash core or the support adapter.
107+
include(LogStash::PluginMixins::ECSCompatibilitySupport)
108+
104109
config_name "elasticsearch"
105110

106111
# The Elasticsearch action to perform. Valid actions are:
@@ -242,6 +247,34 @@ class LogStash::Outputs::ElasticSearch < LogStash::Outputs::Base
242247
# Custom Headers to send on each request to elasticsearch nodes
243248
config :custom_headers, :validate => :hash, :default => {}
244249

250+
def initialize(*params)
251+
super
252+
setup_ecs_compatibility_related_defaults
253+
end
254+
255+
def setup_ecs_compatibility_related_defaults
256+
case ecs_compatibility
257+
when :disabled
258+
@default_index = "logstash-%{+yyyy.MM.dd}"
259+
@default_ilm_rollover_alias = "logstash"
260+
@default_template_name = 'logstash'
261+
when :v1
262+
@default_index = "ecs-logstash-%{+yyyy.MM.dd}"
263+
@default_ilm_rollover_alias = "ecs-logstash"
264+
@default_template_name = 'ecs-logstash'
265+
else
266+
fail("unsupported ECS Compatibility `#{ecs_compatibility}`")
267+
end
268+
269+
@index ||= default_index
270+
@ilm_rollover_alias ||= default_ilm_rollover_alias
271+
@template_name ||= default_template_name
272+
end
273+
274+
attr_reader :default_index
275+
attr_reader :default_ilm_rollover_alias
276+
attr_reader :default_template_name
277+
245278
# @override to handle proxy => '' as if none was set
246279
def config_init(params)
247280
proxy = params['proxy']

lib/logstash/outputs/elasticsearch/common.rb

+7-7
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ def successful_connection?
6060
!!maximum_seen_major_version
6161
end
6262

63-
def use_event_type?(client)
64-
client.maximum_seen_major_version < 8
63+
def use_event_type?
64+
maximum_seen_major_version < 8
6565
end
6666

6767
# Convert the event into a 3-tuple of action, params, and event
@@ -74,7 +74,7 @@ def event_action_tuple(event)
7474
routing_field_name => @routing ? event.sprintf(@routing) : nil
7575
}
7676

77-
params[:_type] = get_event_type(event) if use_event_type?(client)
77+
params[:_type] = get_event_type(event) if use_event_type?
7878

7979
if @pipeline
8080
params[:pipeline] = event.sprintf(@pipeline)
@@ -347,11 +347,11 @@ def get_event_type(event)
347347
type = if @document_type
348348
event.sprintf(@document_type)
349349
else
350-
if client.maximum_seen_major_version < 6
350+
if maximum_seen_major_version < 6
351351
event.get("type") || DEFAULT_EVENT_TYPE_ES6
352-
elsif client.maximum_seen_major_version == 6
352+
elsif maximum_seen_major_version == 6
353353
DEFAULT_EVENT_TYPE_ES6
354-
elsif client.maximum_seen_major_version == 7
354+
elsif maximum_seen_major_version == 7
355355
DEFAULT_EVENT_TYPE_ES7
356356
else
357357
nil
@@ -436,7 +436,7 @@ def safe_bulk(actions)
436436
end
437437

438438
def default_index?(index)
439-
@index == LogStash::Outputs::ElasticSearch::CommonConfigs::DEFAULT_INDEX_NAME
439+
@index == @default_index
440440
end
441441

442442
def dlq_enabled?

lib/logstash/outputs/elasticsearch/common_configs.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def self.included(mod)
1717
# For weekly indexes ISO 8601 format is recommended, eg. logstash-%{+xxxx.ww}.
1818
# LS uses Joda to format the index pattern from event timestamp.
1919
# Joda formats are defined http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html[here].
20-
mod.config :index, :validate => :string, :default => DEFAULT_INDEX_NAME
20+
mod.config :index, :validate => :string
2121

2222
mod.config :document_type,
2323
:validate => :string,
@@ -44,7 +44,7 @@ def self.included(mod)
4444
# `curl -XDELETE <http://localhost:9200/_template/OldTemplateName?pretty>`
4545
#
4646
# where `OldTemplateName` is whatever the former setting was.
47-
mod.config :template_name, :validate => :string, :default => "logstash"
47+
mod.config :template_name, :validate => :string
4848

4949
# You can set the path to your own template here, if you so desire.
5050
# If not set, the included template will be used.
@@ -153,7 +153,7 @@ def self.included(mod)
153153
mod.config :ilm_enabled, :validate => [true, false, 'true', 'false', 'auto'], :default => 'auto'
154154

155155
# Rollover alias used for indexing data. If rollover alias doesn't exist, Logstash will create it and map it to the relevant index
156-
mod.config :ilm_rollover_alias, :validate => :string, :default => DEFAULT_ROLLOVER_ALIAS
156+
mod.config :ilm_rollover_alias, :validate => :string
157157

158158
# appends “{now/d}-000001” by default for new index creation, subsequent rollover indices will increment based on this pattern i.e. “000002”
159159
# {now/d} is date math, and will insert the appropriate value automatically.

lib/logstash/outputs/elasticsearch/ilm.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def setup_ilm
1212
end
1313

1414
def default_rollover_alias?(rollover_alias)
15-
rollover_alias == LogStash::Outputs::ElasticSearch::DEFAULT_ROLLOVER_ALIAS
15+
rollover_alias == default_ilm_rollover_alias
1616
end
1717

1818
def ilm_alias_set?

lib/logstash/outputs/elasticsearch/template_manager.rb

+12-9
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ class TemplateManager
33
# To be mixed into the elasticsearch plugin base
44
def self.install_template(plugin)
55
return unless plugin.manage_template
6-
if plugin.template.nil?
7-
plugin.logger.info("Using default mapping template")
8-
else
6+
if plugin.template
97
plugin.logger.info("Using mapping template from", :path => plugin.template)
8+
template = read_template_file(plugin.template)
9+
else
10+
plugin.logger.info("Using a default mapping template", :es_version => plugin.maximum_seen_major_version,
11+
:ecs_compatibility => plugin.ecs_compatibility)
12+
template = load_default_template(plugin.maximum_seen_major_version, plugin.ecs_compatibility)
1013
end
1114

12-
13-
template = get_template(plugin.template, plugin.maximum_seen_major_version)
1415
add_ilm_settings_to_template(plugin, template) if plugin.ilm_in_use?
1516
plugin.logger.info("Attempting to install template", :manage_template => template)
1617
install(plugin.client, template_name(plugin), template, plugin.template_overwrite)
@@ -19,9 +20,11 @@ def self.install_template(plugin)
1920
end
2021

2122
private
22-
def self.get_template(path, es_major_version)
23-
template_path = path || default_template_path(es_major_version)
23+
def self.load_default_template(es_major_version, ecs_compatibility)
24+
template_path = default_template_path(es_major_version, ecs_compatibility)
2425
read_template_file(template_path)
26+
rescue => e
27+
fail "Failed to load default template for Elasticsearch v#{es_major_version} with ECS #{ecs_compatibility}; caused by: #{e.inspect}"
2528
end
2629

2730
def self.install(client, template_name, template, template_overwrite)
@@ -46,9 +49,9 @@ def self.template_name(plugin)
4649
plugin.ilm_in_use? && !plugin.original_params.key?('template_name') ? plugin.ilm_rollover_alias : plugin.template_name
4750
end
4851

49-
def self.default_template_path(es_major_version)
52+
def self.default_template_path(es_major_version, ecs_compatibility=:disabled)
5053
template_version = es_major_version == 1 ? 2 : es_major_version
51-
default_template_name = "elasticsearch-template-es#{template_version}x.json"
54+
default_template_name = "templates/ecs-#{ecs_compatibility}/elasticsearch-#{template_version}x.json"
5255
::File.expand_path(default_template_name, ::File.dirname(__FILE__))
5356
end
5457

logstash-output-elasticsearch.gemspec

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Gem::Specification.new do |s|
22
s.name = 'logstash-output-elasticsearch'
3-
s.version = '10.5.1'
3+
s.version = '10.6.0'
44

55
s.licenses = ['apache-2.0']
66
s.summary = "Stores logs in Elasticsearch"
@@ -25,6 +25,7 @@ Gem::Specification.new do |s|
2525
s.add_runtime_dependency 'stud', ['>= 0.0.17', '~> 0.0']
2626
s.add_runtime_dependency 'cabin', ['~> 0.6']
2727
s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
28+
s.add_runtime_dependency 'logstash-mixin-ecs_compatibility_support', '~>1.0'
2829

2930
s.add_development_dependency 'logstash-codec-plain'
3031
s.add_development_dependency 'logstash-devutils'

spec/integration/outputs/ilm_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,9 @@
249249
}
250250
let (:small_max_doc_policy) { max_docs_policy(3) }
251251
let (:large_max_doc_policy) { max_docs_policy(1000000) }
252-
let (:expected_index) { LogStash::Outputs::ElasticSearch::DEFAULT_ROLLOVER_ALIAS }
252+
let (:expected_index) { elasticsearch_output_plugin.default_ilm_rollover_alias }
253253

254-
subject { LogStash::Outputs::ElasticSearch.new(settings) }
254+
subject(:elasticsearch_output_plugin) { LogStash::Outputs::ElasticSearch.new(settings) }
255255

256256
before :each do
257257
# Delete all templates first.

spec/unit/outputs/elasticsearch/template_manager_spec.rb

+9-3
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,24 @@
88
describe ".default_template_path" do
99
context "elasticsearch 1.x" do
1010
it "chooses the 2x template" do
11-
expect(described_class.default_template_path(1)).to match(/elasticsearch-template-es2x.json/)
11+
expect(described_class.default_template_path(1)).to end_with("/templates/ecs-disabled/elasticsearch-2x.json")
1212
end
1313
end
1414
context "elasticsearch 2.x" do
1515
it "chooses the 2x template" do
16-
expect(described_class.default_template_path(2)).to match(/elasticsearch-template-es2x.json/)
16+
expect(described_class.default_template_path(2)).to end_with("/templates/ecs-disabled/elasticsearch-2x.json")
1717
end
1818
end
1919
context "elasticsearch 5.x" do
2020
it "chooses the 5x template" do
21-
expect(described_class.default_template_path(5)).to match(/elasticsearch-template-es5x.json/)
21+
expect(described_class.default_template_path(5)).to end_with("/templates/ecs-disabled/elasticsearch-5x.json")
2222
end
2323
end
2424
end
25+
26+
context 'when ECS v1 is requested' do
27+
it 'resolves' do
28+
expect(described_class.default_template_path(7, :v1)).to end_with("/templates/ecs-v1/elasticsearch-7x.json")
29+
end
30+
end
2531
end

spec/unit/outputs/elasticsearch_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
describe LogStash::Outputs::ElasticSearch do
77
subject { described_class.new(options) }
88
let(:options) { {} }
9-
let(:maximum_seen_major_version) { rand(100) }
9+
let(:maximum_seen_major_version) { [1,2,5,6,7,8].sample }
1010

1111
let(:do_register) { true }
1212

0 commit comments

Comments
 (0)