Skip to content

Commit d6f97ad

Browse files
authored
Feat: event target => namespace support (ECS) (#41)
We're introducing a `target => ...` configuration option for the codec, to aid ECS support. When `target` isn't set in ECS mode, we do the usual info log message.
1 parent e61006e commit d6f97ad

File tree

6 files changed

+99
-41
lines changed

6 files changed

+99
-41
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.1.0
2+
- Feat: event `target => namespace` support (ECS) [#41](https://github.com/logstash-plugins/logstash-codec-json_lines/pull/41)
3+
- Refactor: dropped support for old Logstash versions (< 6.0)
4+
15
## 3.0.6
26
- Support flush method, see https://github.com/logstash-plugins/logstash-codec-json_lines/pull/35
37

Gemfile

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,3 @@ if Dir.exist?(logstash_path) && use_logstash_source
99
gem 'logstash-core', :path => "#{logstash_path}/logstash-core"
1010
gem 'logstash-core-plugin-api', :path => "#{logstash_path}/logstash-core-plugin-api"
1111
end
12-
13-
if RUBY_VERSION == "1.9.3"
14-
gem 'rake', '12.2.1'
15-
end

docs/index.asciidoc

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ Therefore this codec cannot work with line oriented inputs.
3636
|Setting |Input type|Required
3737
| <<plugins-{type}s-{plugin}-charset>> |<<string,string>>, one of `["ASCII-8BIT", "UTF-8", "US-ASCII", "Big5", "Big5-HKSCS", "Big5-UAO", "CP949", "Emacs-Mule", "EUC-JP", "EUC-KR", "EUC-TW", "GB2312", "GB18030", "GBK", "ISO-8859-1", "ISO-8859-2", "ISO-8859-3", "ISO-8859-4", "ISO-8859-5", "ISO-8859-6", "ISO-8859-7", "ISO-8859-8", "ISO-8859-9", "ISO-8859-10", "ISO-8859-11", "ISO-8859-13", "ISO-8859-14", "ISO-8859-15", "ISO-8859-16", "KOI8-R", "KOI8-U", "Shift_JIS", "UTF-16BE", "UTF-16LE", "UTF-32BE", "UTF-32LE", "Windows-31J", "Windows-1250", "Windows-1251", "Windows-1252", "IBM437", "IBM737", "IBM775", "CP850", "IBM852", "CP852", "IBM855", "CP855", "IBM857", "IBM860", "IBM861", "IBM862", "IBM863", "IBM864", "IBM865", "IBM866", "IBM869", "Windows-1258", "GB1988", "macCentEuro", "macCroatian", "macCyrillic", "macGreek", "macIceland", "macRoman", "macRomania", "macThai", "macTurkish", "macUkraine", "CP950", "CP951", "IBM037", "stateless-ISO-2022-JP", "eucJP-ms", "CP51932", "EUC-JIS-2004", "GB12345", "ISO-2022-JP", "ISO-2022-JP-2", "CP50220", "CP50221", "Windows-1256", "Windows-1253", "Windows-1255", "Windows-1254", "TIS-620", "Windows-874", "Windows-1257", "MacJapanese", "UTF-7", "UTF8-MAC", "UTF-16", "UTF-32", "UTF8-DoCoMo", "SJIS-DoCoMo", "UTF8-KDDI", "SJIS-KDDI", "ISO-2022-JP-KDDI", "stateless-ISO-2022-JP-KDDI", "UTF8-SoftBank", "SJIS-SoftBank", "BINARY", "CP437", "CP737", "CP775", "IBM850", "CP857", "CP860", "CP861", "CP862", "CP863", "CP864", "CP865", "CP866", "CP869", "CP1258", "Big5-HKSCS:2008", "ebcdic-cp-us", "eucJP", "euc-jp-ms", "EUC-JISX0213", "eucKR", "eucTW", "EUC-CN", "eucCN", "CP936", "ISO2022-JP", "ISO2022-JP2", "ISO8859-1", "ISO8859-2", "ISO8859-3", "ISO8859-4", "ISO8859-5", "ISO8859-6", "CP1256", "ISO8859-7", "CP1253", "ISO8859-8", "CP1255", "ISO8859-9", "CP1254", "ISO8859-10", "ISO8859-11", "CP874", "ISO8859-13", "CP1257", "ISO8859-14", "ISO8859-15", "ISO8859-16", "CP878", "MacJapan", "ASCII", "ANSI_X3.4-1968", "646", "CP65000", "CP65001", "UTF-8-MAC", "UTF-8-HFS", "UCS-2BE", "UCS-4BE", "UCS-4LE", "CP932", "csWindows31J", "SJIS", "PCK", "CP1250", "CP1251", "CP1252", "external", "locale"]`|No
3838
| <<plugins-{type}s-{plugin}-delimiter>> |<<string,string>>|No
39+
| <<plugins-{type}s-{plugin}-ecs_compatibility>> |<<string,string>>|No
40+
| <<plugins-{type}s-{plugin}-target>> |<<string,string>>|No
3941
|=======================================================================
4042

4143
&nbsp;
@@ -64,4 +66,35 @@ For nxlog users, you'll want to set this to `CP1252`
6466

6567
Change the delimiter that separates lines
6668

69+
[id="plugins-{type}s-{plugin}-ecs_compatibility"]
70+
===== `ecs_compatibility`
71+
72+
* Value type is <<string,string>>
73+
* Supported values are:
74+
** `disabled`: does not use ECS-compatible field names
75+
** `v1`, `v8`: Elastic Common Schema compliant behavior (warns when `target` isn't set)
76+
* Default value depends on which version of Logstash is running:
77+
** When Logstash provides a `pipeline.ecs_compatibility` setting, its value is used as the default
78+
** Otherwise, the default value is `disabled`
79+
80+
Controls this plugin's compatibility with the {ecs-ref}[Elastic Common Schema (ECS)].
81+
82+
[id="plugins-{type}s-{plugin}-target"]
83+
===== `target`
84+
85+
* Value type is <<string,string>>
86+
* There is no default value for this setting.
87+
88+
Define the target field for placing the parsed data. If this setting is not
89+
set, the JSON data will be stored at the root (top level) of the event.
90+
91+
For example, if you want data to be put under the `document` field:
92+
[source,ruby]
93+
input {
94+
http {
95+
codec => json_lines {
96+
target => "[document]"
97+
}
98+
}
99+
}
67100

lib/logstash/codecs/json_lines.rb

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
require "logstash/util/charset"
44
require "logstash/util/buftok"
55
require "logstash/json"
6+
require 'logstash/plugin_mixins/ecs_compatibility_support'
7+
require 'logstash/plugin_mixins/ecs_compatibility_support/target_check'
8+
require 'logstash/plugin_mixins/validator_support/field_reference_validation_adapter'
9+
require 'logstash/plugin_mixins/event_support/event_factory_adapter'
10+
require 'logstash/plugin_mixins/event_support/from_json_helper'
611

712
# This codec will decode streamed JSON that is newline delimited.
813
# Encoding will emit a single JSON string ending in a `@delimiter`
@@ -12,6 +17,15 @@
1217
# terminated lines. The file input will produce a line string without a newline.
1318
# Therefore this codec cannot work with line oriented inputs.
1419
class LogStash::Codecs::JSONLines < LogStash::Codecs::Base
20+
21+
include LogStash::PluginMixins::ECSCompatibilitySupport
22+
include LogStash::PluginMixins::ECSCompatibilitySupport::TargetCheck
23+
24+
extend LogStash::PluginMixins::ValidatorSupport::FieldReferenceValidationAdapter
25+
26+
include LogStash::PluginMixins::EventSupport::EventFactoryAdapter
27+
include LogStash::PluginMixins::EventSupport::FromJsonHelper
28+
1529
config_name "json_lines"
1630

1731
# The character encoding used in this codec. Examples include `UTF-8` and
@@ -28,6 +42,11 @@ class LogStash::Codecs::JSONLines < LogStash::Codecs::Base
2842
# Change the delimiter that separates lines
2943
config :delimiter, :validate => :string, :default => "\n"
3044

45+
# Defines a target field for placing decoded fields.
46+
# If this setting is omitted, data gets stored at the root (top level) of the event.
47+
# The target is only relevant while decoding data into a new event.
48+
config :target, :validate => :field_reference
49+
3150
public
3251

3352
def register
@@ -38,7 +57,7 @@ def register
3857

3958
def decode(data, &block)
4059
@buffer.extract(data).each do |line|
41-
parse(@converter.convert(line), &block)
60+
parse_json(@converter.convert(line), &block)
4261
end
4362
end
4463

@@ -51,31 +70,21 @@ def encode(event)
5170
def flush(&block)
5271
remainder = @buffer.flush
5372
if !remainder.empty?
54-
parse(@converter.convert(remainder), &block)
73+
parse_json(@converter.convert(remainder), &block)
5574
end
5675
end
5776

5877
private
5978

60-
# from_json_parse uses the Event#from_json method to deserialize and directly produce events
61-
def from_json_parse(json, &block)
62-
LogStash::Event.from_json(json).each { |event| yield event }
63-
rescue LogStash::Json::ParserError => e
64-
@logger.warn("JSON parse error, original data now in message field", :error => e, :data => json)
65-
yield LogStash::Event.new("message" => json, "tags" => ["_jsonparsefailure"])
79+
def parse_json(json)
80+
events_from_json(json, targeted_event_factory).each { |event| yield event }
81+
rescue => e
82+
@logger.warn("JSON parse error, original data now in message field", message: e.message, exception: e.class, data: json)
83+
yield parse_json_error_event(json)
6684
end
6785

68-
# legacy_parse uses the LogStash::Json class to deserialize json
69-
def legacy_parse(json, &block)
70-
# ignore empty/blank lines which LogStash::Json#load returns as nil
71-
o = LogStash::Json.load(json)
72-
yield(LogStash::Event.new(o)) if o
73-
rescue LogStash::Json::ParserError => e
74-
@logger.warn("JSON parse error, original data now in message field", :error => e, :data => json)
75-
yield LogStash::Event.new("message" => json, "tags" => ["_jsonparsefailure"])
86+
def parse_json_error_event(json)
87+
event_factory.new_event("message" => json, "tags" => ["_jsonparsefailure"])
7688
end
7789

78-
# keep compatibility with all v2.x distributions. only in 2.3 will the Event#from_json method be introduced
79-
# and we need to keep compatibility for all v2 releases.
80-
alias_method :parse, LogStash::Event.respond_to?(:from_json) ? :from_json_parse : :legacy_parse
8190
end

logstash-codec-json_lines.gemspec

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Gem::Specification.new do |s|
22

33
s.name = 'logstash-codec-json_lines'
4-
s.version = '3.0.6'
4+
s.version = '3.1.0'
55
s.licenses = ['Apache License (2.0)']
66
s.summary = "Reads and writes newline-delimited JSON"
77
s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
@@ -19,11 +19,15 @@ Gem::Specification.new do |s|
1919
# Special flag to let us know this is actually a logstash plugin
2020
s.metadata = { "logstash_plugin" => "true", "logstash_group" => "codec" }
2121

22+
s.required_ruby_version = '>= 2.3' # Event.from_json exists at least since LS 5.6
23+
2224
# Gem dependencies
2325
s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
26+
s.add_runtime_dependency 'logstash-mixin-ecs_compatibility_support', '~> 1.3'
27+
s.add_runtime_dependency 'logstash-mixin-event_support', '~> 1.0', '>= 1.0.1'
28+
s.add_runtime_dependency 'logstash-mixin-validator_support', '~> 1.0'
2429

25-
s.add_runtime_dependency 'logstash-codec-line', '>= 2.1.0'
26-
30+
s.add_development_dependency 'logstash-codec-line', '>= 2.1.0'
2731
s.add_development_dependency 'logstash-devutils'
2832
s.add_development_dependency 'insist'
2933
end

spec/codecs/json_lines_spec.rb

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
require "logstash/event"
55
require "logstash/json"
66
require "insist"
7+
require 'logstash/plugin_mixins/ecs_compatibility_support/spec_helper'
78

8-
describe LogStash::Codecs::JSONLines do
9+
describe LogStash::Codecs::JSONLines, :ecs_compatibility_support do
910

1011
let(:codec_options) { {} }
1112

@@ -185,29 +186,40 @@
185186
end
186187
end
187188

188-
end
189+
ecs_compatibility_matrix(:disabled, :v1, :v8 => :v1) do
189190

190-
context "forcing legacy parsing" do
191-
it_behaves_like :codec do
192-
subject do
193-
# register method is called in the constructor
194-
LogStash::Codecs::JSONLines.new(codec_options)
191+
before(:each) do
192+
allow_any_instance_of(described_class).to receive(:ecs_compatibility).and_return(ecs_compatibility)
193+
end
194+
195+
context 'with target' do
196+
let(:input) do
197+
%{{"field": "value1"}
198+
{"field": 2.0}
199+
}
195200
end
196201

197-
before(:each) do
198-
# stub codec parse method to force use of the legacy parser.
199-
# this is very implementation specific but I am not sure how
200-
# this can be tested otherwise.
201-
allow(subject).to receive(:parse) do |line, &block|
202-
subject.send(:legacy_parse, line, &block)
202+
let(:codec_options) { super().merge "target" => 'foo' }
203+
204+
let(:collector) { Array.new }
205+
206+
it 'should generate two events' do
207+
subject.decode(input) do |event|
208+
collector.push(event)
203209
end
210+
expect(collector.size).to eq(2)
211+
expect(collector[0].include?('field')).to be false
212+
expect(collector[0].get('foo')).to eql 'field' => 'value1'
213+
expect(collector[1].include?('field')).to be false
214+
expect(collector[1].get('foo')).to eql 'field' => 2.0
204215
end
205216
end
217+
218+
end
219+
206220
end
207221

208222
context "default parser choice" do
209-
# here we cannot force the use of the Event#from_json since if this test is run in the
210-
# legacy context (no Java Event) it will fail but if in the new context, it will be picked up.
211223
it_behaves_like :codec do
212224
subject do
213225
# register method is called in the constructor

0 commit comments

Comments
 (0)