Skip to content

Commit f9de15b

Browse files
kaisechengyaauie
andauthored
add ecs support (#179)
Add ECS support * skip country_code3 * auto set `target` from `source` Fixed: #163 Co-authored-by: Ry Biesemeyer <[email protected]>
1 parent 118df48 commit f9de15b

File tree

9 files changed

+474
-143
lines changed

9 files changed

+474
-143
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 7.1.0
2+
- Add ECS compatibility [#179](https://github.com/logstash-plugins/logstash-filter-geoip/pull/179)
3+
14
## 7.0.1
25
- [DOC] Add documentation for MaxMind database license change [#177](https://github.com/logstash-plugins/logstash-filter-geoip/pull/177)
36

docs/index.asciidoc

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ This plugin supports the following configuration options plus the <<plugins-{typ
9292
| <<plugins-{type}s-{plugin}-cache_size>> |<<number,number>>|No
9393
| <<plugins-{type}s-{plugin}-database>> |a valid filesystem path|No
9494
| <<plugins-{type}s-{plugin}-default_database_type>> |`City` or `ASN`|No
95+
| <<plugins-{type}s-{plugin}-ecs_compatibility>> | <<string,string>>|No
9596
| <<plugins-{type}s-{plugin}-fields>> |<<array,array>>|No
9697
| <<plugins-{type}s-{plugin}-source>> |<<string,string>>|Yes
9798
| <<plugins-{type}s-{plugin}-tag_on_failure>> |<<array,array>>|No
@@ -164,6 +165,20 @@ For the built-in GeoLite2 City database, the following are available:
164165
`dma_code`, `ip`, `latitude`, `location`, `longitude`, `postal_code`, `region_code`,
165166
`region_name` and `timezone`.
166167

168+
[id="plugins-{type}s-{plugin}-ecs_compatibility"]
169+
===== `ecs_compatibility`
170+
171+
* Value type is <<string,string>>
172+
* Supported values are:
173+
** `disabled`: unstructured geo data added at root level
174+
** `v1`: uses fields that are compatible with Elastic Common Schema (for example, `[client][geo][country_name]`)
175+
* Default value depends on which version of Logstash is running:
176+
** When Logstash provides a `pipeline.ecs_compatibility` setting, its value is used as the default
177+
** Otherwise, the default value is `disabled`.
178+
179+
Controls this plugin's compatibility with the {ecs-ref}[Elastic Common Schema (ECS)].
180+
The value of this setting affects the _default_ value of <<plugins-{type}s-{plugin}-target>>.
181+
167182
[id="plugins-{type}s-{plugin}-source"]
168183
===== `source`
169184

@@ -185,8 +200,15 @@ Tags the event on failure to look up geo information. This can be used in later
185200
[id="plugins-{type}s-{plugin}-target"]
186201
===== `target`
187202

203+
* This is an optional setting with condition.
188204
* Value type is <<string,string>>
189-
* Default value is `"geoip"`
205+
* Default value depends on whether <<plugins-{type}s-{plugin}-ecs_compatibility>> is enabled:
206+
** ECS Compatibility disabled: `geoip`
207+
** ECS Compatibility enabled: If `source` is an `ip` sub-field, eg. `[client][ip]`,
208+
`target` will automatically set to the parent field, in this example `client`,
209+
otherwise, `target` is a required setting
210+
*** `geo` field is nested in `[client][geo]`
211+
*** ECS compatible values are `client`, `destination`, `host`, `observer`, `server`, `source`
190212

191213
Specify the field into which Logstash should store the geoip data.
192214
This can be useful, for example, if you have `src_ip` and `dst_ip` fields and

lib/logstash/filters/geoip.rb

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require "logstash/filters/base"
33
require "logstash/namespace"
44
require "logstash-filter-geoip_jars"
5+
require "logstash/plugin_mixins/ecs_compatibility_support"
56

67

78
# The GeoIP filter adds information about the geographical location of IP addresses,
@@ -31,6 +32,8 @@
3132
# --
3233

3334
class LogStash::Filters::GeoIP < LogStash::Filters::Base
35+
include LogStash::PluginMixins::ECSCompatibilitySupport(:disabled, :v1)
36+
3437
config_name "geoip"
3538

3639
# The path to the GeoLite2 database file which Logstash should use. City and ASN databases are supported.
@@ -60,14 +63,16 @@ class LogStash::Filters::GeoIP < LogStash::Filters::Base
6063
# This can be useful, for example, if you have `src_ip` and `dst_ip` fields and
6164
# would like the GeoIP information of both IPs.
6265
#
63-
# If you save the data to a target field other than `geoip` and want to use the
64-
# `geo_point` related functions in Elasticsearch, you need to alter the template
65-
# provided with the Elasticsearch output and configure the output to use the
66-
# new template.
66+
# ECS disabled/ Legacy default: `geoip`
67+
# ECS default: The `target` is auto-generated from `source` when the `source` specifies an `ip` sub-field
68+
# For example, source => [client][ip], `target` will be `client`
69+
# If `source` is not an `ip` sub-field, source => client_ip, `target` setting is mandatory
70+
#
71+
# Elasticsearch ECS mode expected `geo` fields to be nested at:
72+
# `client`, `destination`, `host`, `observer`, `server`, `source`
6773
#
68-
# Even if you don't use the `geo_point` mapping, the `[target][location]` field
69-
# is still valid GeoJSON.
70-
config :target, :validate => :string, :default => 'geoip'
74+
# `geo` fields are not expected to be used directly at the root of the events
75+
config :target, :validate => :string
7176

7277
# GeoIP lookup is surprisingly expensive. This filter uses an cache to take advantage of the fact that
7378
# IPs agents are often found adjacent to one another in log files and rarely have a random distribution.
@@ -89,7 +94,18 @@ class LogStash::Filters::GeoIP < LogStash::Filters::Base
8994
config :tag_on_failure, :validate => :array, :default => ["_geoip_lookup_failure"]
9095

9196
public
97+
98+
ECS_TARGET_FIELD = %w{
99+
client
100+
destination
101+
host
102+
observer
103+
server
104+
source
105+
}.map(&:freeze).freeze
106+
92107
def register
108+
setup_target_field
93109
setup_filter(select_database_path)
94110
end
95111

@@ -108,10 +124,29 @@ def tag_unsuccessful_lookup(event)
108124
@tag_on_failure.each{|tag| event.tag(tag)}
109125
end
110126

127+
def setup_target_field
128+
if ecs_compatibility == :disabled
129+
@target ||= 'geoip'
130+
else
131+
@target ||= auto_target_from_source!
132+
# normalize top-level fields to not be bracket-wrapped
133+
normalized_target = @target.gsub(/\A\[([^\[\]]+)\]\z/,'\1')
134+
logger.warn("ECS expect `target` value `#{normalized_target}` in #{ECS_TARGET_FIELD}") unless ECS_TARGET_FIELD.include?(normalized_target)
135+
end
136+
end
137+
138+
def auto_target_from_source!
139+
return @source[0...-4] if @source.end_with?('[ip]') && @source.length > 4
140+
141+
fail(LogStash::ConfigurationError, "GeoIP Filter in ECS-Compatiblity mode "\
142+
"requires a `target` when `source` is not an `ip` sub-field, eg. [client][ip]")
143+
end
144+
145+
111146
def setup_filter(database_path)
112147
@database = database_path
113148
@logger.info("Using geoip database", :path => @database)
114-
@geoipfilter = org.logstash.filters.GeoIPFilter.new(@source, @target, @fields, @database, @cache_size)
149+
@geoipfilter = org.logstash.filters.geoip.GeoIPFilter.new(@source, @target, @fields, @database, @cache_size, ecs_compatibility.to_s)
115150
end
116151

117152
def terminate_filter
@@ -125,7 +160,7 @@ def close
125160
end
126161

127162
def select_database_path
128-
vendor_path = ::File.expand_path("../../../vendor/", ::File.dirname(__FILE__))
163+
vendor_path = ::File.expand_path(::File.join("..", "..", "..", "..", "vendor"), __FILE__)
129164

130165
if load_database_manager?
131166
@database_manager = LogStash::Filters::Geoip::DatabaseManager.new(self, @database, @default_database_type, vendor_path)
@@ -137,7 +172,7 @@ def select_database_path
137172

138173
def load_database_manager?
139174
begin
140-
require_relative "#{LogStash::Environment::LOGSTASH_HOME}/x-pack/lib/filters/geoip/database_manager"
175+
require_relative ::File.join(LogStash::Environment::LOGSTASH_HOME, "x-pack", "lib", "filters", "geoip", "database_manager")
141176
true
142177
rescue LoadError => e
143178
@logger.info("DatabaseManager is not in classpath", :version => LOGSTASH_VERSION, :exception => e)

logstash-filter-geoip.gemspec

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

33
s.name = 'logstash-filter-geoip'
4-
s.version = '7.0.1'
4+
s.version = '7.1.0'
55
s.licenses = ['Apache License (2.0)']
66
s.summary = "Adds geographical information about an IP address"
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"
@@ -22,6 +22,7 @@ Gem::Specification.new do |s|
2222

2323
# Gem dependencies
2424
s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
25+
s.add_runtime_dependency 'logstash-mixin-ecs_compatibility_support', '~>1.1'
2526
s.add_development_dependency 'logstash-devutils'
2627
s.add_development_dependency 'insist'
2728
s.add_development_dependency 'benchmark-ips'

spec/filters/geoip_ecs_spec.rb

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# encoding: utf-8
2+
require "logstash/devutils/rspec/spec_helper"
3+
require "logstash/filters/geoip"
4+
require_relative 'test_helper'
5+
require 'logstash/plugin_mixins/ecs_compatibility_support/spec_helper'
6+
7+
CITYDB = ::Dir.glob(::File.expand_path(::File.join("..", "..", "..", "vendor", "GeoLite2-City.mmdb"), __FILE__)).first
8+
ASNDB = ::Dir.glob(::File.expand_path(::File.join("..", "..", "..", "vendor", "GeoLite2-ASN.mmdb"), __FILE__)).first
9+
10+
describe LogStash::Filters::GeoIP do
11+
let(:options) { {} }
12+
let(:plugin) { LogStash::Filters::GeoIP.new(options) }
13+
14+
describe "simple ip filter", :aggregate_failures do
15+
16+
context "when specifying the target", :ecs_compatibility_support do
17+
ecs_compatibility_matrix(:disabled, :v1) do |ecs_select|
18+
19+
let(:ip) { "8.8.8.8" }
20+
let(:event) { LogStash::Event.new("message" => ip) }
21+
let(:target) { "server" }
22+
let(:common_options) { {"source" => "message", "database" => CITYDB, "target" => target} }
23+
24+
before(:each) do
25+
allow_any_instance_of(described_class).to receive(:ecs_compatibility).and_return(ecs_compatibility)
26+
plugin.register
27+
end
28+
29+
context "with city database" do
30+
let(:options) { common_options }
31+
32+
it "should return geo in target" do
33+
plugin.filter(event)
34+
35+
expect( event.get ecs_select[disabled: "[#{target}][ip]", v1: "[#{target}][ip]"] ).to eq ip
36+
expect( event.get ecs_select[disabled: "[#{target}][country_code2]", v1: "[#{target}][geo][country_iso_code]"] ).to eq 'US'
37+
expect( event.get ecs_select[disabled: "[#{target}][country_name]", v1: "[#{target}][geo][country_name]"] ).to eq 'United States'
38+
expect( event.get ecs_select[disabled: "[#{target}][continent_code]", v1: "[#{target}][geo][continent_code]"] ).to eq 'NA'
39+
expect( event.get ecs_select[disabled: "[#{target}][location][lat]", v1: "[#{target}][geo][location][lat]"] ).to eq 37.751
40+
expect( event.get ecs_select[disabled: "[#{target}][location][lon]", v1: "[#{target}][geo][location][lon]"] ).to eq -97.822
41+
42+
if ecs_select.active_mode == :disabled
43+
expect( event.get "[#{target}][country_code3]" ).to eq 'US'
44+
else
45+
expect( event.get "[#{target}][geo][country_code3]" ).to be_nil
46+
expect( event.get "[#{target}][country_code3]" ).to be_nil
47+
end
48+
end
49+
end
50+
51+
52+
context "with ASN database" do
53+
let(:options) { common_options.merge({"database" => ASNDB}) }
54+
55+
it "should return geo in target" do
56+
plugin.filter(event)
57+
58+
expect( event.get ecs_select[disabled: "[#{target}][ip]", v1: "[#{target}][ip]"] ).to eq ip
59+
expect( event.get ecs_select[disabled: "[#{target}][asn]", v1: "[#{target}][as][number]"] ).to eq 15169
60+
expect( event.get ecs_select[disabled: "[#{target}][as_org]", v1: "[#{target}][as][organization][name]"] ).to eq "Google LLC"
61+
end
62+
end
63+
64+
context "with customize fields" do
65+
let(:fields) { ["continent_name", "timezone"] }
66+
let(:options) { common_options.merge({"fields" => fields}) }
67+
68+
it "should return fields" do
69+
plugin.filter(event)
70+
71+
expect( event.get ecs_select[disabled: "[#{target}][ip]", v1: "[#{target}][ip]"] ).to be_nil
72+
expect( event.get ecs_select[disabled: "[#{target}][country_code2]", v1: "[#{target}][geo][country_iso_code]"] ).to be_nil
73+
expect( event.get ecs_select[disabled: "[#{target}][country_name]", v1: "[#{target}][geo][country_name]"] ).to be_nil
74+
expect( event.get ecs_select[disabled: "[#{target}][continent_code]", v1: "[#{target}][geo][continent_code]"] ).to be_nil
75+
expect( event.get ecs_select[disabled: "[#{target}][location][lat]", v1: "[#{target}][geo][location][lat]"] ).to be_nil
76+
expect( event.get ecs_select[disabled: "[#{target}][location][lon]", v1: "[#{target}][geo][location][lon]"] ).to be_nil
77+
78+
expect( event.get ecs_select[disabled: "[#{target}][continent_name]", v1: "[#{target}][geo][continent_name]"] ).to eq "North America"
79+
expect( event.get ecs_select[disabled: "[#{target}][timezone]", v1: "[#{target}][geo][timezone]"] ).to eq "America/Chicago"
80+
end
81+
end
82+
83+
end
84+
end
85+
86+
context "setup target field" do
87+
let(:ip) { "8.8.8.8" }
88+
let(:event) { LogStash::Event.new("message" => ip) }
89+
let(:common_options) { {"source" => "message", "database" => CITYDB} }
90+
91+
context "ECS disabled" do
92+
before do
93+
allow_any_instance_of(described_class).to receive(:ecs_compatibility).and_return(:disabled)
94+
plugin.register
95+
plugin.filter(event)
96+
end
97+
98+
context "`target` is unset" do
99+
let(:options) { common_options }
100+
it "should use 'geoip'" do
101+
expect( event.get "[geoip][ip]" ).to eq ip
102+
end
103+
end
104+
105+
context "`target` is set" do
106+
let(:target) { 'host' }
107+
let(:options) { common_options.merge({"target" => target}) }
108+
it "should use `target`" do
109+
expect( event.get "[#{target}][ip]" ).to eq ip
110+
end
111+
end
112+
end
113+
114+
context "ECS mode" do
115+
before do
116+
allow_any_instance_of(described_class).to receive(:ecs_compatibility).and_return(:v1)
117+
end
118+
119+
context "`target` is unset" do
120+
121+
context "`source` end with [ip]" do
122+
let(:event) { LogStash::Event.new("host" => {"ip" => ip}) }
123+
let(:options) { common_options.merge({"source" => "[host][ip]"}) }
124+
125+
it "should use source's parent as target" do
126+
plugin.register
127+
plugin.filter(event)
128+
expect( event.get "[host][geo][country_iso_code]" ).to eq 'US'
129+
end
130+
end
131+
132+
context "`source` end with [ip] but `target` does not match ECS template" do
133+
let(:event) { LogStash::Event.new("hostname" => {"ip" => ip}) }
134+
let(:options) { common_options.merge({"source" => "[hostname][ip]"}) }
135+
136+
it "should use source's parent as target with warning" do
137+
expect(plugin.logger).to receive(:warn).with(/ECS expect `target`/)
138+
plugin.register
139+
plugin.filter(event)
140+
expect( event.get "[hostname][geo][country_iso_code]" ).to eq 'US'
141+
end
142+
end
143+
144+
context "`source` == [ip]" do
145+
let(:event) { LogStash::Event.new("ip" => ip) }
146+
let(:options) { common_options.merge({"source" => "[ip]"}) }
147+
148+
it "should raise error to require `target`" do
149+
expect { plugin.register }.to raise_error LogStash::ConfigurationError, /requires a `target`/
150+
end
151+
end
152+
153+
context "`source` not end with [ip]" do
154+
let(:event) { LogStash::Event.new("host_ip" => ip) }
155+
let(:options) { common_options.merge({"source" => "host_ip"}) }
156+
157+
it "should raise error to require `target`" do
158+
expect { plugin.register }.to raise_error LogStash::ConfigurationError, /requires a `target`/
159+
end
160+
end
161+
end
162+
163+
context "`target` is set" do
164+
let(:event) { LogStash::Event.new("client" => {"ip" => ip}) }
165+
let(:options) { common_options.merge({"source" => "[client][ip]", "target" => target}) }
166+
167+
context "`target` matches ECS template" do
168+
let(:target) { 'host' }
169+
170+
it "should use `target`" do
171+
plugin.register
172+
plugin.filter(event)
173+
expect( event.get "[#{target}][geo][country_iso_code]" ).to eq 'US'
174+
end
175+
end
176+
177+
context "`target` in canonical field reference syntax matches ECS template" do
178+
let(:target) { '[host]' }
179+
180+
it "should normalize and use `target`" do
181+
expect(plugin.logger).to receive(:warn).never
182+
plugin.register
183+
plugin.filter(event)
184+
expect( event.get "[host][geo][country_iso_code]" ).to eq 'US'
185+
end
186+
end
187+
188+
context "`target` does not match ECS template" do
189+
let(:target) { 'host_ip' }
190+
191+
it "should use `target` with warning" do
192+
expect(plugin.logger).to receive(:warn).with(/ECS expect `target`/)
193+
plugin.register
194+
plugin.filter(event)
195+
expect( event.get "[#{target}][geo][country_iso_code]" ).to eq 'US'
196+
end
197+
end
198+
end
199+
end
200+
end
201+
202+
end
203+
end

spec/filters/geoip_offline_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
require "insist"
44
require "logstash/filters/geoip"
55

6-
CITYDB = ::Dir.glob(::File.expand_path("../../vendor/", ::File.dirname(__FILE__))+"/GeoLite2-City.mmdb").first
7-
ASNDB = ::Dir.glob(::File.expand_path("../../vendor/", ::File.dirname(__FILE__))+"/GeoLite2-ASN.mmdb").first
6+
CITYDB = ::Dir.glob(::File.expand_path(::File.join("..", "..", "..", "vendor", "GeoLite2-City.mmdb"), __FILE__)).first
7+
ASNDB = ::Dir.glob(::File.expand_path(::File.join("..", "..", "..", "vendor", "GeoLite2-ASN.mmdb"), __FILE__)).first
88

99
describe LogStash::Filters::GeoIP do
1010
describe "defaults" do

0 commit comments

Comments
 (0)