fluent-plugin-ip2location 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5b7b81c42ee55b5a20fcbf953dcfb6edd6076ae22af638e917bce7a779441ad8
4
+ data.tar.gz: 778f82f161d577d1a43b0b729c0f273730cbfc4a51fcb938666cd2695fd01743
5
+ SHA512:
6
+ metadata.gz: cc6b58c35506a9862123f96bcb4f1f2c138d4d13e349b548ee0eb40114419789ba0c9c73ef9a4d0e9db0766ce53dd1ec648e11902aa366f1b53981b3a5a21d59
7
+ data.tar.gz: 4eb502dae00e7f66f6a872e2baf37f37b034bc867705150b91ac752eae78261a6e27ca611c19271e8a220f64ac5eef9e4d4144f210572258e57203f3c746a6bc
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-06-24
4
+
5
+ - Initial release.
6
+ - Adds Fluentd filter plugin `@type ip2location`.
7
+ - Supports nested input and output fields through Fluentd record accessor syntax.
8
+ - Supports IP2Location BIN lookup, field selection, private IP skipping, error handling and in-memory lookup cache.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 IP2Location.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # IP2Location Fluentd Filter Plugin
2
+
3
+ `fluent-plugin-ip2location` is a Fluentd filter plugin that enriches Fluentd events with IP geolocation data from an IP2Location BIN database.
4
+
5
+ It reads an IP address from a configured record field, queries the IP2Location Ruby library against an IP2Location BIN database, and writes selected lookup fields back into the event record.
6
+
7
+ ## Features
8
+
9
+ - Reads IP address from a top-level or nested record field, for example `ip`, `remote_addr`, or `$.client.ip`.
10
+ - Supports nested output fields, for example `ip2location` or `$.enrichment.ip2location`.
11
+ - Supports IP2Location geolocation fields including country, region, city, latitude, longitude, ISP, domain, ZIP code, time zone, usage type, ASN and AS name.
12
+ - Skips invalid IP addresses and private/local IP addresses by default.
13
+ - Supports configurable field selection.
14
+ - Supports an in-memory lookup cache.
15
+
16
+ ## Installation
17
+
18
+ Install it into the Ruby environment used by Fluentd:
19
+
20
+ ```bash
21
+ fluent-gem install fluent-plugin-ip2location
22
+ ```
23
+
24
+ Or add it to your Gemfile:
25
+
26
+ ```ruby
27
+ gem "fluent-plugin-ip2location"
28
+ ```
29
+
30
+ Then run:
31
+
32
+ ```bash
33
+ bundle install
34
+ ```
35
+
36
+ ## IP2Location database
37
+
38
+ This plugin requires an IP2Location BIN database file. For DB26, use a file such as:
39
+
40
+ ```text
41
+ IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER-MOBILE-ELEVATION-USAGETYPE-ADDRESSTYPE-CATEGORY-DISTRICT-ASN.BIN
42
+ ```
43
+
44
+ Place the BIN file somewhere readable by the Fluentd process, for example:
45
+
46
+ ```text
47
+ /opt/ip2location/IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER-MOBILE-ELEVATION-USAGETYPE-ADDRESSTYPE-CATEGORY-DISTRICT-ASN.BIN
48
+ ```
49
+
50
+ ## Fluentd configuration
51
+
52
+ ```apache
53
+ <filter app.**>
54
+ @type ip2location
55
+ database /opt/ip2location/IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER-MOBILE-ELEVATION-USAGETYPE-ADDRESSTYPE-CATEGORY-DISTRICT-ASN.BIN
56
+ ip_field $.client.ip
57
+ output_field $.ip2location
58
+ fields country_short,country_long,region,city,latitude,longitude,isp,domain,zipcode,timezone,usagetype,asn,as
59
+ skip_private_ip true
60
+ skip_invalid_ip true
61
+ cache_size 10000
62
+ on_error warn
63
+ </filter>
64
+ ```
65
+
66
+ ### Input record
67
+
68
+ ```json
69
+ {
70
+ "client": {
71
+ "ip": "8.8.8.8"
72
+ },
73
+ "message": "hello"
74
+ }
75
+ ```
76
+
77
+ ### Output record
78
+
79
+ ```json
80
+ {
81
+ "client": {
82
+ "ip": "8.8.8.8"
83
+ },
84
+ "message": "hello",
85
+ "ip2location": {
86
+ "country_short": "US",
87
+ "country_long": "United States of America",
88
+ "region": "California",
89
+ "city": "Mountain View",
90
+ "latitude": 37.40599,
91
+ "longitude": -122.078514,
92
+ "isp": "Google LLC",
93
+ "zipcode": "94043",
94
+ "timezone": "-08:00",
95
+ "asn": "15169",
96
+ "as": "Google LLC"
97
+ }
98
+ }
99
+ ```
100
+
101
+ ## Configuration parameters
102
+
103
+ | Parameter | Required | Default | Description |
104
+ |---|---:|---|---|
105
+ | `database` | Yes | - | Absolute path to the IP2Location BIN database file. |
106
+ | `ip_field` | Yes | - | Record field containing the IP address. Supports Fluentd record accessor syntax, for example `ip`, `remote_addr`, or `$.client.ip`. |
107
+ | `output_field` | No | `ip2location` | Destination field for enrichment output. Supports record accessor syntax. Ignored when `merge_record true`. |
108
+ | `fields` | No | `all` | Comma-separated list of IP2Location fields to include. Use `all` to include every supported field. |
109
+ | `merge_record` | No | `false` | When `true`, writes enrichment fields into the root record instead of a nested object. |
110
+ | `prefix` | No | `ip2location_` | Prefix used when `merge_record true`. |
111
+ | `skip_invalid_ip` | No | `true` | When `true`, invalid IP values are ignored. When `false`, invalid IP values raise an error. |
112
+ | `skip_private_ip` | No | `true` | When `true`, private, loopback and link-local IPs are not looked up. |
113
+ | `include_unknown` | No | `false` | When `false`, empty values and `-` values returned by IP2Location are omitted. |
114
+ | `cache_size` | No | `10000` | Maximum number of IP lookup results kept in memory. Set `0` to disable caching. |
115
+ | `on_error` | No | `warn` | Lookup error behavior. One of `ignore`, `warn`, or `raise`. |
116
+
117
+ ## Supported fields
118
+
119
+ ```text
120
+ country_short,country_long,region,city,isp,latitude,longitude,domain,zipcode,timezone,netspeed,iddcode,areacode,weatherstationcode,weatherstationname,mcc,mnc,mobilebrand,elevation,usagetype,addresstype,category,district,asn,as,as_domain,as_usagetype,as_cidr
121
+ ```
122
+
123
+ ## Merge fields into the root record
124
+
125
+ ```apache
126
+ <filter app.**>
127
+ @type ip2location
128
+ database /opt/ip2location/IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER-MOBILE-ELEVATION-USAGETYPE-ADDRESSTYPE-CATEGORY-DISTRICT-ASN.BIN
129
+ ip_field remote_addr
130
+ merge_record true
131
+ prefix geoip_
132
+ fields country_short,country_long,city,asn,as
133
+ </filter>
134
+ ```
135
+
136
+ Output example:
137
+
138
+ ```json
139
+ {
140
+ "remote_addr": "8.8.8.8",
141
+ "geoip_country_short": "US",
142
+ "geoip_country_long": "United States of America",
143
+ "geoip_city": "Mountain View",
144
+ "geoip_asn": "15169",
145
+ "geoip_as": "Google LLC"
146
+ }
147
+ ```
148
+
149
+ ## Development
150
+
151
+ Install dependencies:
152
+
153
+ ```bash
154
+ bundle install
155
+ ```
156
+
157
+ Run tests:
158
+
159
+ ```bash
160
+ bundle exec rake
161
+ ```
162
+
163
+ Build the gem locally:
164
+
165
+ ```bash
166
+ gem build fluent-plugin-ip2location.gemspec
167
+ ```
168
+
169
+ Install the local gem:
170
+
171
+ ```bash
172
+ gem install fluent-plugin-ip2location-0.1.0.gem
173
+ ```
174
+
175
+ Run the example configuration from the project directory:
176
+
177
+ ```bash
178
+ bundle exec fluentd -c example/fluent.conf -p ./lib
179
+ ```
180
+
181
+ Update the `database` path in `example/fluent.conf` before running it.
182
+
183
+ ## License
184
+
185
+ LICENSE.txt
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fluent/plugin/filter"
4
+ require "ip2location_ruby"
5
+ require "ipaddr"
6
+ require "thread"
7
+
8
+ module Fluent
9
+ module Plugin
10
+ class IP2LocationFilter < Filter
11
+ Fluent::Plugin.register_filter("ip2location", self)
12
+
13
+ helpers :record_accessor
14
+
15
+ KNOWN_FIELDS = %w[
16
+ country_short
17
+ country_long
18
+ region
19
+ city
20
+ isp
21
+ latitude
22
+ longitude
23
+ domain
24
+ zipcode
25
+ timezone
26
+ netspeed
27
+ iddcode
28
+ areacode
29
+ weatherstationcode
30
+ weatherstationname
31
+ mcc
32
+ mnc
33
+ mobilebrand
34
+ elevation
35
+ usagetype
36
+ addresstype
37
+ category
38
+ district
39
+ asn
40
+ as
41
+ as_domain
42
+ as_usagetype
43
+ as_cidr
44
+ ].freeze
45
+
46
+ VALID_ERROR_MODES = %w[ignore warn raise].freeze
47
+
48
+ config_param :database, :string
49
+ config_param :ip_field, :string
50
+ config_param :output_field, :string, default: "ip2location"
51
+ config_param :fields, :string, default: "all"
52
+ config_param :merge_record, :bool, default: false
53
+ config_param :prefix, :string, default: "ip2location_"
54
+ config_param :skip_invalid_ip, :bool, default: true
55
+ config_param :skip_private_ip, :bool, default: true
56
+ config_param :include_unknown, :bool, default: false
57
+ config_param :cache_size, :integer, default: 10_000
58
+ config_param :on_error, :string, default: "warn"
59
+
60
+ def configure(conf)
61
+ super
62
+
63
+ raise Fluent::ConfigError, "database does not exist: #{@database}" unless File.file?(@database)
64
+ raise Fluent::ConfigError, "cache_size must be zero or greater" if @cache_size.negative?
65
+ raise Fluent::ConfigError, "on_error must be one of: #{VALID_ERROR_MODES.join(', ')}" unless VALID_ERROR_MODES.include?(@on_error)
66
+ raise Fluent::ConfigError, "prefix cannot be empty when merge_record is true" if @merge_record && @prefix.empty?
67
+
68
+ @selected_fields = parse_fields(@fields)
69
+ @ip_accessor = record_accessor_create(@ip_field)
70
+ @output_accessor = record_accessor_create(@output_field) unless @merge_record
71
+ @cache = {}
72
+ @cache_order = []
73
+ @cache_mutex = Mutex.new
74
+ end
75
+
76
+ def start
77
+ super
78
+ @ip2location = Ip2location.new.open(@database)
79
+ rescue StandardError => e
80
+ raise Fluent::ConfigError, "failed to open IP2Location database #{@database}: #{e.message}"
81
+ end
82
+
83
+ def shutdown
84
+ @ip2location.close if @ip2location&.respond_to?(:close)
85
+ ensure
86
+ super
87
+ end
88
+
89
+ def filter(_tag, _time, record)
90
+ ip = normalize_ip(@ip_accessor.call(record))
91
+ return record if ip.nil?
92
+
93
+ case ip_status(ip)
94
+ when :invalid
95
+ return handle_invalid_ip(record, ip)
96
+ when :private
97
+ return record
98
+ end
99
+
100
+ enriched = lookup(ip)
101
+ return record if enriched.empty?
102
+
103
+ if @merge_record
104
+ enriched.each { |key, value| record["#{@prefix}#{key}"] = value }
105
+ else
106
+ @output_accessor.set(record, enriched)
107
+ end
108
+
109
+ record
110
+ rescue StandardError => e
111
+ handle_lookup_error(e, ip)
112
+ record
113
+ end
114
+
115
+ private
116
+
117
+ def parse_fields(value)
118
+ raw = value.to_s.strip
119
+ return KNOWN_FIELDS if raw.empty? || raw.casecmp("all").zero?
120
+
121
+ fields = raw.split(",").map(&:strip).reject(&:empty?)
122
+ invalid = fields - KNOWN_FIELDS
123
+ raise Fluent::ConfigError, "unknown fields: #{invalid.join(', ')}. Valid fields: #{KNOWN_FIELDS.join(', ')}" unless invalid.empty?
124
+
125
+ fields
126
+ end
127
+
128
+ def normalize_ip(value)
129
+ return nil if value.nil?
130
+
131
+ ip = value.to_s.strip
132
+ ip.empty? ? nil : ip
133
+ end
134
+
135
+ def ip_status(ip)
136
+ addr = IPAddr.new(ip)
137
+ return :private if @skip_private_ip && private_or_local_address?(addr)
138
+
139
+ :valid
140
+ rescue IPAddr::InvalidAddressError
141
+ :invalid
142
+ end
143
+
144
+ def private_or_local_address?(addr)
145
+ %i[private? loopback? link_local?].any? do |method_name|
146
+ addr.respond_to?(method_name) && addr.public_send(method_name)
147
+ end
148
+ end
149
+
150
+ def handle_invalid_ip(record, ip)
151
+ if @skip_invalid_ip
152
+ log.debug "Skipping invalid IP address: #{ip}"
153
+ return record
154
+ end
155
+
156
+ raise ArgumentError, "invalid IP address: #{ip}"
157
+ end
158
+
159
+ def lookup(ip)
160
+ cached = cache_read(ip)
161
+ return cached unless cached.nil?
162
+
163
+ raw = @ip2location.get_all(ip)
164
+ enriched = normalize_record(raw)
165
+ cache_write(ip, enriched)
166
+ enriched
167
+ end
168
+
169
+ def normalize_record(raw)
170
+ hash = raw.respond_to?(:to_h) ? raw.to_h : raw
171
+ return {} unless hash.respond_to?(:key?)
172
+
173
+ @selected_fields.each_with_object({}) do |field, output|
174
+ next unless hash.key?(field)
175
+
176
+ value = hash[field]
177
+ next if unknown_value?(value)
178
+
179
+ output[field] = value
180
+ end
181
+ end
182
+
183
+ def unknown_value?(value)
184
+ return false if @include_unknown
185
+ return true if value.nil?
186
+
187
+ string_value = value.to_s.strip
188
+ string_value.empty? || string_value == "-"
189
+ end
190
+
191
+ def cache_read(ip)
192
+ return nil if @cache_size.zero?
193
+
194
+ @cache_mutex.synchronize { @cache[ip] }
195
+ end
196
+
197
+ def cache_write(ip, value)
198
+ return if @cache_size.zero?
199
+
200
+ @cache_mutex.synchronize do
201
+ unless @cache.key?(ip)
202
+ @cache_order << ip
203
+ if @cache_order.length > @cache_size
204
+ oldest = @cache_order.shift
205
+ @cache.delete(oldest)
206
+ end
207
+ end
208
+ @cache[ip] = value
209
+ end
210
+ end
211
+
212
+ def handle_lookup_error(error, ip)
213
+ message = "IP2Location lookup failed"
214
+ message = "#{message} for #{ip}" unless ip.nil?
215
+ message = "#{message}: #{error.message}"
216
+
217
+ case @on_error
218
+ when "ignore"
219
+ log.debug message
220
+ when "warn"
221
+ log.warn message
222
+ when "raise"
223
+ raise error
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fluent
4
+ module Plugin
5
+ module IP2Location
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-ip2location
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - IP2Location
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: fluentd
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.16'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '1.16'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2'
32
+ - !ruby/object:Gem::Dependency
33
+ name: ip2location_ruby
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '8.8'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '9'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '8.8'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '9'
52
+ - !ruby/object:Gem::Dependency
53
+ name: rake
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: '13.0'
59
+ type: :development
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - "~>"
64
+ - !ruby/object:Gem::Version
65
+ version: '13.0'
66
+ - !ruby/object:Gem::Dependency
67
+ name: test-unit
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - "~>"
71
+ - !ruby/object:Gem::Version
72
+ version: '3.6'
73
+ type: :development
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '3.6'
80
+ description: Reads an IP address from a Fluentd record, queries an IP2Location BIN
81
+ file with ip2location_ruby, and enriches the event record with geolocation and network
82
+ fields.
83
+ email:
84
+ - support@ip2location.com
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - CHANGELOG.md
90
+ - LICENSE.txt
91
+ - README.md
92
+ - lib/fluent/plugin/filter_ip2location.rb
93
+ - lib/fluent/plugin/ip2location/version.rb
94
+ homepage: https://github.com/ip2location/fluent-plugin-ip2location
95
+ licenses:
96
+ - MIT
97
+ metadata:
98
+ homepage_uri: https://github.com/ip2location/fluent-plugin-ip2location
99
+ source_code_uri: https://github.com/ip2location/fluent-plugin-ip2location
100
+ changelog_uri: https://github.com/ip2location/fluent-plugin-ip2location/blob/main/CHANGELOG.md
101
+ rubygems_mfa_required: 'true'
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '3.2'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubygems_version: 4.0.15
117
+ specification_version: 4
118
+ summary: Fluentd filter plugin for IP2Location BIN database enrichment.
119
+ test_files: []