fluent-plugin-elasticsearch 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8544959ad2a4289ea64e3057372eafee047d707c
4
- data.tar.gz: 4434e04393705d4d696ef0d7bc4e9ac32e1755db
3
+ metadata.gz: 421e2c4bb196e72e87a6c6e3c8444844b755fe07
4
+ data.tar.gz: 8568c9999361d7d0ab68f5916721c91d0312ef16
5
5
  SHA512:
6
- metadata.gz: c46a10b6978857b09d9741226a8d8238b13ccfaccd63967debc1654dcf1acc6fb215e3934e9b42c27a4b73e29b07d7b6120427ea57ef5ab591c8b06596f00616
7
- data.tar.gz: aaf4966ea945f5966b2abece6836180ddf78f6ca991de090f23427dd6ae983f0cd8e70e9e28d9d7587e7dde2fce0aee8e94d3812c8f43572c5c2965cb9c33fe8
6
+ metadata.gz: 9878a3743fb7ff425713cf822e7d16ec877eb9697a3e702b428f2aa62474bc8c3c1ee8e8438770e8b5ec5e5d2e37e15b6a81cce1eafb013ffce9970ee5d98098
7
+ data.tar.gz: c9cbebe13d2c81e4aa36dd99c666f02d4b0f1d0301ed39ad14dd93cef088a03e949113c6003d635b0ebe603eb6d58521bed47234c7b650603cd87abd4899eb9e
@@ -6,4 +6,4 @@ rvm:
6
6
  - 2.1
7
7
  - 2.2
8
8
 
9
- script: bundle exec ruby -S -Itest test/plugin/test_out_elasticsearch.rb
9
+ script: bundle exec rake test
data/History.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ### Future
4
4
 
5
+ ### 1.1.0
6
+ - Support SSL client verification and custom CA file (#123)
7
+ - Release experimental `type elasticsearch_dynamic` (#127)
8
+
5
9
  ### 1.0.0
6
10
  - password config param is now marked as secret and won't be displayed in logs.
7
11
 
@@ -9,7 +13,7 @@
9
13
  - Add `ssl_verify` option (#108)
10
14
 
11
15
  ### 0.8.0
12
- - Replace Patron with Excon HTTP client
16
+ - Replace Patron with Excon HTTP client (#93)
13
17
 
14
18
  ### 0.7.0
15
19
  - Add new option `time_key` (#85)
data/README.md CHANGED
@@ -8,46 +8,69 @@
8
8
 
9
9
  I wrote this so you can search logs routed through Fluentd.
10
10
 
11
- ## Installation
11
+ * [Installation](#installation)
12
+ * [Usage](#usage)
13
+ + [Index templates](#index-templates)
14
+ * [Configuration](#configuration)
15
+ + [hosts](#hosts)
16
+ + [user, password, path, scheme, ssl_verify](#user-password-path-scheme-ssl_verify)
17
+ + [logstash_format](#logstash_format)
18
+ + [logstash_prefix](#logstash_prefix)
19
+ + [logstash_dateformat](#logstash_dateformat)
20
+ + [time_key](#time_key)
21
+ + [utc_index](#utc_index)
22
+ + [request_timeout](#request_timeout)
23
+ + [reload_connections](#reload_connections)
24
+ + [reload_on_failure](#reload_on_failure)
25
+ + [include_tag_key, tag_key](#include_tag_key-tag_key)
26
+ + [id_key](#id_key)
27
+ + [Client/host certificate options](#clienthost-certificate-options)
28
+ + [Buffered output options](#buffered-output-options)
29
+ + [Not seeing a config you need?](#not-seeing-a-config-you-need)
30
+ + [Dynamic configuration](#dynamic-configuration)
31
+ * [Contact](#contact)
32
+ * [Contributing](#contributing)
33
+ * [Running tests](#running-tests)
12
34
 
13
- $ gem install fluent-plugin-elasticsearch
35
+ ## Installation
14
36
 
15
- * prerequisite : You need to install [libcurl (libcurl-devel)](http://curl.haxx.se/libcurl/) to work with.
37
+ ```sh
38
+ $ gem install fluent-plugin-elasticsearch
39
+ ```
16
40
 
17
41
  ## Usage
18
42
 
19
- In your fluentd configration, use `type elasticsearch`. Additional configuration is optional, default values would look like this:
43
+ In your Fluentd configuration, use `type elasticsearch`. Additional configuration is optional, default values would look like this:
20
44
 
21
45
  ```
22
- host localhost
23
- port 9200
24
- index_name fluentd
25
- type_name fluentd
46
+ <match my.logs>
47
+ type elasticsearch
48
+ host localhost
49
+ port 9200
50
+ index_name fluentd
51
+ type_name fluentd
52
+ </match>
26
53
  ```
27
54
 
28
- **Index templates**
55
+ ### Index templates
29
56
 
30
- This plugin creates ElasticSearch indices by merely writing to them. Consider using [Index Templates](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html) to gain control of what get indexed and how. See [this example](https://github.com/uken/fluent-plugin-elasticsearch/issues/33#issuecomment-38693282) for a good starting point.
57
+ This plugin creates ElasticSearch indices by merely writing to them. Consider using [Index Templates](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html) to gain control of what get indexed and how. See [this example](https://github.com/uken/fluent-plugin-elasticsearch/issues/33#issuecomment-38693282) for a good starting point.
31
58
 
32
- **More options:**
59
+ ## Configuration
33
60
 
34
- **hosts**
61
+ ### hosts
35
62
 
36
63
  ```
37
64
  hosts host1:port1,host2:port2,host3:port3
38
- ```
39
-
40
- or
41
-
42
- ```
65
+ # or
43
66
  hosts https://customhost.com:443/path,https://username:password@host-failover.com:443
44
67
  ```
45
68
 
46
- You can specify multiple elasticsearch hosts with separator ",".
69
+ You can specify multiple ElasticSearch hosts with separator ",".
47
70
 
48
- If you specify multiple hosts, this plugin will load balance updates to elasticsearch. This is an [elasticsearch-ruby](https://github.com/elasticsearch/elasticsearch-ruby) feature, the default strategy is round-robin.
71
+ If you specify multiple hosts, this plugin will load balance updates to ElasticSearch. This is an [elasticsearch-ruby](https://github.com/elasticsearch/elasticsearch-ruby) feature, the default strategy is round-robin.
49
72
 
50
- **user, password, path, scheme, ssl_verify**
73
+ ### user, password, path, scheme, ssl_verify
51
74
 
52
75
  If you specify this option, host and port options are ignored.
53
76
 
@@ -62,21 +85,21 @@ You can specify user and password for HTTP basic auth. If used in conjunction wi
62
85
 
63
86
  Specify `ssl_verify false` to skip ssl verification (defaults to true)
64
87
 
65
- **logstash_format**
88
+ ### logstash_format
66
89
 
67
90
  ```
68
91
  logstash_format true # defaults to false
69
92
  ```
70
93
 
71
- This is meant to make writing data into elasticsearch compatible to what logstash writes. By doing this, one could take advantade of [kibana](http://kibana.org/).
94
+ This is meant to make writing data into ElasticSearch compatible to what [Logstash](https://www.elastic.co/products/logstash) writes. By doing this, one could take advantage of [Kibana](https://www.elastic.co/products/kibana).
72
95
 
73
- **logstash_prefix**
96
+ ### logstash_prefix
74
97
 
75
98
  ```
76
99
  logstash_prefix mylogs # defaults to "logstash"
77
100
  ```
78
101
 
79
- **logstash_dateformat**
102
+ ### logstash_dateformat
80
103
 
81
104
  By default, the records inserted into index `logstash-YYMMDD`. This option allows to insert into specified index like `mylogs-YYYYMM` for a monthly index.
82
105
 
@@ -84,15 +107,15 @@ By default, the records inserted into index `logstash-YYMMDD`. This option allow
84
107
  logstash_dateformat %Y.%m. # defaults to "%Y.%m.%d"
85
108
  ```
86
109
 
87
- **time_key**
110
+ ### time_key
88
111
 
89
- By default, when inserting records in logstash format, @timestamp is dynamically created with the time at log ingestion. If you'd like to use a custom time. Include an @timestamp with your record.
112
+ By default, when inserting records in [Logstash](https://www.elastic.co/products/logstash) format, `@timestamp` is dynamically created with the time at log ingestion. If you'd like to use a custom time, include an `@timestamp` with your record.
90
113
 
91
114
  ```
92
115
  {"@timestamp":"2014-04-07T000:00:00-00:00"}
93
116
  ```
94
117
 
95
- You can specify an option `time_key` (like the option described in [tail Input Plugin](http://docs.fluentd.org/articles/in_tail)) if you don't like `@timestamp`.
118
+ You can specify an option `time_key` (like the option described in [tail Input Plugin](http://docs.fluentd.org/articles/in_tail)) to replace `@timestamp` key.
96
119
 
97
120
  Suppose you have settings
98
121
 
@@ -118,33 +141,33 @@ The output will be
118
141
  }
119
142
  ```
120
143
 
121
- **utc_index**
144
+ ### utc_index
122
145
 
123
146
  ```
124
147
  utc_index true
125
148
  ```
126
149
 
127
- By default, the records inserted into index `logstash-YYMMDD` with utc (Coordinated Universal Time). This option allows to use local time if you describe utc_index to false.
150
+ By default, the records inserted into index `logstash-YYMMDD` with UTC (Coordinated Universal Time). This option allows to use local time if you describe utc_index to false.
128
151
 
129
- **request_timeout**
152
+ ### request_timeout
153
+
154
+ You can specify HTTP request timeout.
155
+
156
+ This is useful when ElasticSearch cannot return response for bulk request within the default of 5 seconds.
130
157
 
131
158
  ```
132
159
  request_timeout 15s # defaults to 5s
133
160
  ```
134
161
 
135
- You can specify HTTP request timeout.
136
-
137
- This is useful when Elasticsearch cannot return response for bulk request within the default of 5 seconds.
138
-
139
- **reload_connections**
162
+ ### reload_connections
140
163
 
141
164
  ```
142
165
  reload_connections false # defaults to true
143
166
  ```
144
167
 
145
- **reload_on_failure**
168
+ ### reload_on_failure
146
169
 
147
- You can tune how the elasticsearch-transport host reloading feature works. By default it will reload the host list from the server every 10,000th request to spread the load. This can be an issue if your ElasticSearch cluster is behind a Reverse Proxy, as fluentd process may not have direct network access to the ElasticSearch nodes.
170
+ You can tune how the elasticsearch-transport host reloading feature works. By default it will reload the host list from the server every 10,000th request to spread the load. This can be an issue if your ElasticSearch cluster is behind a Reverse Proxy, as Fluentd process may not have direct network access to the ElasticSearch nodes.
148
171
 
149
172
  ```
150
173
  reload_on_failure true # defaults to false
@@ -153,14 +176,14 @@ reload_on_failure true # defaults to false
153
176
  Indicates that the elasticsearch-transport will try to reload the nodes addresses if there is a failure while making the
154
177
  request, this can be useful to quickly remove a dead node from the list of addresses.
155
178
 
156
- **include_tag_key, tag_key**
179
+ ### include_tag_key, tag_key
157
180
 
158
181
  ```
159
182
  include_tag_key true # defaults to false
160
183
  tag_key tag # defaults to tag
161
184
  ```
162
185
 
163
- This will add the fluentd tag in the json record. For instance, if you have a config like this:
186
+ This will add the Fluentd tag in the JSON record. For instance, if you have a config like this:
164
187
 
165
188
  ```
166
189
  <match my.logs>
@@ -170,19 +193,19 @@ This will add the fluentd tag in the json record. For instance, if you have a co
170
193
  </match>
171
194
  ```
172
195
 
173
- The record inserted into elasticsearch would be
196
+ The record inserted into ElasticSearch would be
174
197
 
175
198
  ```
176
199
  {"_key":"my.logs", "name":"Johnny Doeie"}
177
200
  ```
178
201
 
179
- **id_key**
202
+ ### id_key
180
203
 
181
204
  ```
182
205
  id_key request_id # use "request_id" field as a record id in ES
183
206
  ```
184
207
 
185
- By default, all records inserted into elasticsearch get a random _id. This option allows to use a field in the record as an identifier.
208
+ By default, all records inserted into ElasticSearch get a random _id. This option allows to use a field in the record as an identifier.
186
209
 
187
210
  This following record `{"name":"Johnny","request_id":"87d89af7daffad6"}` will trigger the following ElasticSearch command
188
211
 
@@ -191,9 +214,23 @@ This following record `{"name":"Johnny","request_id":"87d89af7daffad6"}` will tr
191
214
  { "name": "Johnny", "request_id": "87d89af7daffad6" }
192
215
  ```
193
216
 
194
- **Buffered output options**
217
+ ### Client/host certificate options
218
+
219
+ Need to verify ElasticSearch's certificate? You can use the following parameter to specify a CA instead of using an environment variable.
220
+ ```
221
+ ca_file /path/to/your/ca/cert
222
+ ```
223
+
224
+ Does your ElasticSearch cluster want to verify client connections? You can specify the following parameters to use your client certificate, key, and key password for your connection.
225
+ ```
226
+ client_cert /path/to/your/client/cert
227
+ client_key /path/to/your/private/key
228
+ client_key_pass password
229
+ ```
230
+
231
+ ### Buffered output options
195
232
 
196
- fluentd-plugin-elasticsearch is a buffered output that uses elasticseach's bulk API. So additional buffer configuration would be (with default values):
233
+ `fluentd-plugin-elasticsearch` extends [Fluentd's builtin Buffered Output plugin](http://docs.fluentd.org/articles/buffer-plugin-overview). It adds the following options:
197
234
 
198
235
  ```
199
236
  buffer_type memory
@@ -203,9 +240,11 @@ retry_wait 1.0
203
240
  num_threads 1
204
241
  ```
205
242
 
206
- **Not seeing a config you need?**
243
+ ### Not seeing a config you need?
207
244
 
208
- We try to keep the scope of this plugin small. If you need more configuration options, please consider using [fluent-plugin-forest](https://github.com/tagomoris/fluent-plugin-forest). For example, to configure multiple tags to be sent to different ElasticSearch indices:
245
+ We try to keep the scope of this plugin small and not add too many configuration options. If you think an option would be useful to others, feel free to open an issue or contribute a Pull Request.
246
+
247
+ Alternatively, consider using [fluent-plugin-forest](https://github.com/tagomoris/fluent-plugin-forest). For example, to configure multiple tags to be sent to different ElasticSearch indices:
209
248
 
210
249
  ```
211
250
  <match my.logs.*>
@@ -219,12 +258,39 @@ We try to keep the scope of this plugin small. If you need more configuration op
219
258
  </match>
220
259
  ```
221
260
 
222
- ## Contributing
261
+ And yet another option is described in Dynamic Configuration section.
262
+
263
+ ### Dynamic configuration
264
+
265
+ If you want configurations to depend on information in messages, you can use `elasticsearch_dynamic`. This is an experimental variation of the ElasticSearch plugin allows configuration values to be specified in ways such as the below:
266
+
267
+ ```
268
+ <match my.logs.*>
269
+ type elasticsearch_dynamic
270
+ hosts ${record['host1']}:9200,${record['host2']}:9200
271
+ index_name my_index.${Time.at(time).getutc.strftime(@logstash_dateformat)}
272
+ logstash_prefix ${tag_parts[3]}
273
+ port ${9200+rand(4)}
274
+ index_name ${tag_parts[2]}-${Time.at(time).getutc.strftime(@logstash_dateformat)}
275
+ </match>
276
+ ```
277
+
278
+ **Please note, this uses Ruby's `eval` for every message, so there are performance and security implications.**
223
279
 
224
- 1. Fork it
225
- 2. Create your feature branch (`git checkout -b my-new-feature`)
226
- 3. Commit your changes (`git commit -am 'Add some feature'`)
227
- 4. Push to the branch (`git push origin my-new-feature`)
228
- 5. Create new Pull Request
280
+ ## Contact
229
281
 
230
282
  If you have a question, [open an Issue](https://github.com/uken/fluent-plugin-elasticsearch/issues).
283
+
284
+ ## Contributing
285
+
286
+ Pull Requests are welcomed.
287
+
288
+ ## Running tests
289
+
290
+ Install dev dependencies:
291
+
292
+ ```sh
293
+ $ gem install bundler
294
+ $ bundle install
295
+ $ bundle exec rake test
296
+ ```
@@ -3,7 +3,7 @@ $:.push File.expand_path('../lib', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = 'fluent-plugin-elasticsearch'
6
- s.version = '1.0.0'
6
+ s.version = '1.1.0'
7
7
  s.authors = ['diogo', 'pitr']
8
8
  s.email = ['pitr.vern@gmail.com', 'me@diogoterror.com']
9
9
  s.description = %q{ElasticSearch output plugin for Fluent event collector}
@@ -29,6 +29,10 @@ class Fluent::ElasticsearchOutput < Fluent::BufferedOutput
29
29
  config_param :reload_on_failure, :bool, :default => false
30
30
  config_param :time_key, :string, :default => nil
31
31
  config_param :ssl_verify , :bool, :default => true
32
+ config_param :client_key, :string, :default => nil
33
+ config_param :client_cert, :string, :default => nil
34
+ config_param :client_key_pass, :string, :default => nil
35
+ config_param :ca_file, :string, :default => nil
32
36
 
33
37
  include Fluent::SetTagKeyMixin
34
38
  config_set_default :include_tag_key, false
@@ -47,7 +51,8 @@ class Fluent::ElasticsearchOutput < Fluent::BufferedOutput
47
51
 
48
52
  def client
49
53
  @_es ||= begin
50
- adapter_conf = lambda {|f| f.adapter :excon }
54
+ excon_options = { client_key: @client_key, client_cert: @client_cert, client_key_pass: @client_key_pass }
55
+ adapter_conf = lambda {|f| f.adapter :excon, excon_options }
51
56
  transport = Elasticsearch::Transport::Transport::HTTP::Faraday.new(get_connection_options.merge(
52
57
  options: {
53
58
  reload_connections: @reload_connections,
@@ -55,7 +60,7 @@ class Fluent::ElasticsearchOutput < Fluent::BufferedOutput
55
60
  retry_on_failure: 5,
56
61
  transport_options: {
57
62
  request: { timeout: @request_timeout },
58
- ssl: { verify: @ssl_verify }
63
+ ssl: { verify: @ssl_verify, ca_file: @ca_file }
59
64
  }
60
65
  }), &adapter_conf)
61
66
  es = Elasticsearch::Client.new transport: transport
@@ -0,0 +1,241 @@
1
+ # encoding: UTF-8
2
+ require_relative 'out_elasticsearch'
3
+
4
+ class Fluent::ElasticsearchOutputDynamic < Fluent::ElasticsearchOutput
5
+
6
+ Fluent::Plugin.register_output('elasticsearch_dynamic', self)
7
+
8
+ config_param :delimiter, :string, :default => "."
9
+
10
+ # params overloaded as strings
11
+ config_param :port, :string, :default => "9200"
12
+ config_param :logstash_format, :string, :default => "false"
13
+ config_param :utc_index, :string, :default => "true"
14
+ config_param :reload_connections, :string, :default => "true"
15
+ config_param :reload_on_failure, :string, :default => "false"
16
+ config_param :ssl_verify, :string, :dfeault => "true"
17
+
18
+ def configure(conf)
19
+ super
20
+
21
+ # evaluate all configurations here
22
+ @dynamic_config = Hash.new
23
+ self.instance_variables.each { |var|
24
+ if is_valid_expand_param_type(var)
25
+ value = expand_param(self.instance_variable_get(var), nil, nil, nil)
26
+
27
+ var = var.to_s.gsub(/@(.+)/){ $1 }
28
+ @dynamic_config[var] = value
29
+ end
30
+ }
31
+ # end eval all configs
32
+ @current_config = nil
33
+ end
34
+
35
+ def client(host)
36
+
37
+ # check here to see if we already have a client connection for the given host
38
+ connection_options = get_connection_options(host)
39
+
40
+ @_es = nil unless is_existing_connection(connection_options[:hosts])
41
+
42
+ @_es ||= begin
43
+ @current_config = connection_options[:hosts].clone
44
+ excon_options = { client_key: @dynamic_config['client_key'], client_cert: @dynamic_config['client_cert'], client_key_pass: @dynamic_config['client_key_pass'] }
45
+ adapter_conf = lambda {|f| f.adapter :excon, excon_options }
46
+ transport = Elasticsearch::Transport::Transport::HTTP::Faraday.new(connection_options.merge(
47
+ options: {
48
+ reload_connections: @dynamic_config['reload_connections'],
49
+ reload_on_failure: @dynamic_config['reload_on_failure'],
50
+ retry_on_failure: 5,
51
+ transport_options: {
52
+ request: { timeout: @dynamic_config['request_timeout'] },
53
+ ssl: { verify: @dynamic_config['ssl_verify'], ca_file: @dynamic_config['ca_file'] }
54
+ }
55
+ }), &adapter_conf)
56
+ es = Elasticsearch::Client.new transport: transport
57
+
58
+ begin
59
+ raise ConnectionFailure, "Can not reach Elasticsearch cluster (#{connection_options_description(host)})!" unless es.ping
60
+ rescue *es.transport.host_unreachable_exceptions => e
61
+ raise ConnectionFailure, "Can not reach Elasticsearch cluster (#{connection_options_description(host)})! #{e.message}"
62
+ end
63
+
64
+ log.info "Connection opened to Elasticsearch cluster => #{connection_options_description(host)}"
65
+ es
66
+ end
67
+ end
68
+
69
+ def get_connection_options(con_host)
70
+ raise "`password` must be present if `user` is present" if @dynamic_config['user'] && !@dynamic_config['password']
71
+
72
+ hosts = if con_host || @dynamic_config['hosts']
73
+ (con_host || @dynamic_config['hosts']).split(',').map do |host_str|
74
+ # Support legacy hosts format host:port,host:port,host:port...
75
+ if host_str.match(%r{^[^:]+(\:\d+)?$})
76
+ {
77
+ host: host_str.split(':')[0],
78
+ port: (host_str.split(':')[1] || @dynamic_config['port']).to_i,
79
+ scheme: @dynamic_config['scheme']
80
+ }
81
+ else
82
+ # New hosts format expects URLs such as http://logs.foo.com,https://john:pass@logs2.foo.com/elastic
83
+ uri = URI(host_str)
84
+ %w(user password path).inject(host: uri.host, port: uri.port, scheme: uri.scheme) do |hash, key|
85
+ hash[key.to_sym] = uri.public_send(key) unless uri.public_send(key).nil? || uri.public_send(key) == ''
86
+ hash
87
+ end
88
+ end
89
+ end.compact
90
+ else
91
+ [{host: @dynamic_config['host'], port: @dynamic_config['port'].to_i, scheme: @dynamic_config['scheme']}]
92
+ end.each do |host|
93
+ host.merge!(user: @dynamic_config['user'], password: @dynamic_config['password']) if !host[:user] && @dynamic_config['user']
94
+ host.merge!(path: @dynamic_config['path']) if !host[:path] && @dynamic_config['path']
95
+ end
96
+
97
+ {
98
+ hosts: hosts
99
+ }
100
+ end
101
+
102
+ def connection_options_description(host)
103
+ get_connection_options(host)[:hosts].map do |host_info|
104
+ attributes = host_info.dup
105
+ attributes[:password] = 'obfuscated' if attributes.has_key?(:password)
106
+ attributes.inspect
107
+ end.join(', ')
108
+ end
109
+
110
+ def write(chunk)
111
+
112
+ bulk_message = Hash.new { |h,k| h[k] = [] }
113
+
114
+ chunk.msgpack_each do |tag, time, record|
115
+ next unless record.is_a? Hash
116
+
117
+ # evaluate all configurations here
118
+ self.instance_variables.each { |var|
119
+ if is_valid_expand_param_type(var)
120
+ # check here to determine if we should evaluate
121
+ if @dynamic_config[var[1,var.length-1]] != self.instance_variable_get(var)
122
+ value = expand_param(self.instance_variable_get(var), tag, time, record)
123
+ var = var.to_s.gsub(/@(.+)/){ $1 }
124
+ @dynamic_config[var] = value
125
+ end
126
+ end
127
+ }
128
+ # end eval all configs
129
+
130
+ if eval(@dynamic_config['logstash_format'])
131
+ if record.has_key?("@timestamp")
132
+ time = Time.parse record["@timestamp"]
133
+ elsif record.has_key?(@dynamic_config['time_key'])
134
+ time = Time.parse record[@dynamic_config['time_key']]
135
+ record['@timestamp'] = record[@dynamic_config['time_key']]
136
+ else
137
+ record.merge!({"@timestamp" => Time.at(time).to_datetime.to_s})
138
+ end
139
+
140
+ if eval(@dynamic_config['utc_index'])
141
+ target_index = "#{@dynamic_config['logstash_prefix']}-#{Time.at(time).getutc.strftime("#{@dynamic_config['logstash_dateformat']}")}"
142
+ else
143
+ target_index = "#{@dynamic_config['logstash_prefix']}-#{Time.at(time).strftime("#{@dynamic_config['logstash_dateformat']}")}"
144
+ end
145
+ else
146
+ target_index = @dynamic_config['index_name']
147
+ end
148
+
149
+ if @include_tag_key
150
+ record.merge!(@dynamic_config['tag_key'] => tag)
151
+ end
152
+
153
+ meta = { "index" => {"_index" => target_index, "_type" => @dynamic_config['type_name']} }
154
+ if @dynamic_config['id_key'] && record[@dynamic_config['id_key']]
155
+ meta['index']['_id'] = record[@dynamic_config['id_key']]
156
+ end
157
+
158
+ if @dynamic_config['parent_key'] && record[@dynamic_config['parent_key']]
159
+ meta['index']['_parent'] = record[@dynamic_config['parent_key']]
160
+ end
161
+
162
+ if @dynamic_config['hosts']
163
+ host = @dynamic_config['hosts']
164
+ else
165
+ host = "#{@dynamic_config['host']}:#{@dynamic_config['port']}"
166
+ end
167
+
168
+ bulk_message[host] << meta
169
+ bulk_message[host] << record
170
+
171
+ end
172
+
173
+ bulk_message.each do | hKey, array |
174
+ send(array, hKey) unless array.empty?
175
+ array.clear
176
+ end
177
+
178
+ end
179
+
180
+ def send(data, host)
181
+ retries = 0
182
+ begin
183
+ client(host).bulk body: data
184
+ rescue *client(host).transport.host_unreachable_exceptions => e
185
+ if retries < 2
186
+ retries += 1
187
+ @_es = nil
188
+ log.warn "Could not push logs to Elasticsearch, resetting connection and trying again. #{e.message}"
189
+ sleep 2**retries
190
+ retry
191
+ end
192
+ raise ConnectionFailure, "Could not push logs to Elasticsearch after #{retries} retries. #{e.message}"
193
+ end
194
+ end
195
+
196
+ def expand_param(param, tag, time, record)
197
+ # check for '${ ... }'
198
+ # yes => `eval`
199
+ # no => return param
200
+ return param if (param =~ /\${.+}/).nil?
201
+
202
+ # check for 'tag_parts[]'
203
+ # separated by a delimiter (default '.')
204
+ tag_parts = tag.split(@delimiter) unless (param =~ /tag_parts\[.+\]/).nil? || tag.nil?
205
+
206
+ # pull out section between ${} then eval
207
+ inner = param.clone
208
+ while inner.match(/\${.+}/)
209
+ to_eval = inner.match(/\${(.+?)}/){$1}
210
+
211
+ if !(to_eval =~ /record\[.+\]/).nil? && record.nil?
212
+ return to_eval
213
+ elsif !(to_eval =~/tag_parts\[.+\]/).nil? && tag_parts.nil?
214
+ return to_eval
215
+ elsif !(to_eval =~/time/).nil? && time.nil?
216
+ return to_eval
217
+ else
218
+ inner.sub!(/\${.+?}/, eval( to_eval ))
219
+ end
220
+ end
221
+ inner
222
+ end
223
+
224
+ def is_valid_expand_param_type(param)
225
+ return self.instance_variable_get(param).is_a?(String)
226
+ end
227
+
228
+ def is_existing_connection(host)
229
+ # check if the host provided match the current connection
230
+ return false if @_es.nil?
231
+ return false if host.length != @current_config.length
232
+
233
+ for i in 0...host.length
234
+ if !host[i][:host].eql? @current_config[i][:host] || host[i][:port] != @current_config[i][:port]
235
+ return false
236
+ end
237
+ end
238
+
239
+ return true
240
+ end
241
+ end
@@ -0,0 +1,452 @@
1
+ require 'test/unit'
2
+
3
+ require 'fluent/test'
4
+ require 'fluent/plugin/out_elasticsearch_dynamic'
5
+
6
+ require 'webmock/test_unit'
7
+ require 'date'
8
+
9
+ $:.push File.expand_path("../..", __FILE__)
10
+ $:.push File.dirname(__FILE__)
11
+
12
+ require 'helper'
13
+
14
+ WebMock.disable_net_connect!
15
+
16
+ class ElasticsearchOutputDynamic < Test::Unit::TestCase
17
+ attr_accessor :index_cmds, :index_command_counts
18
+
19
+ def setup
20
+ Fluent::Test.setup
21
+ @driver = nil
22
+ end
23
+
24
+ def driver(tag='test', conf='')
25
+ @driver ||= Fluent::Test::BufferedOutputTestDriver.new(Fluent::ElasticsearchOutputDynamic, tag).configure(conf)
26
+ end
27
+
28
+ def sample_record
29
+ {'age' => 26, 'request_id' => '42', 'parent_id' => 'parent'}
30
+ end
31
+
32
+ def stub_elastic_ping(url="http://localhost:9200")
33
+ stub_request(:head, url).to_return(:status => 200, :body => "", :headers => {})
34
+ end
35
+
36
+ def stub_elastic(url="http://localhost:9200/_bulk")
37
+ stub_request(:post, url).with do |req|
38
+ @index_cmds = req.body.split("\n").map {|r| JSON.parse(r) }
39
+ end
40
+ end
41
+
42
+ def stub_elastic_unavailable(url="http://localhost:9200/_bulk")
43
+ stub_request(:post, url).to_return(:status => [503, "Service Unavailable"])
44
+ end
45
+
46
+ def stub_elastic_with_store_index_command_counts(url="http://localhost:9200/_bulk")
47
+ if @index_command_counts == nil
48
+ @index_command_counts = {}
49
+ @index_command_counts.default = 0
50
+ end
51
+
52
+ stub_request(:post, url).with do |req|
53
+ index_cmds = req.body.split("\n").map {|r| JSON.parse(r) }
54
+ @index_command_counts[url] += index_cmds.size
55
+ end
56
+ end
57
+
58
+ def test_configure
59
+ config = %{
60
+ host logs.google.com
61
+ port 777
62
+ scheme https
63
+ path /es/
64
+ user john
65
+ password doe
66
+ }
67
+ instance = driver('test', config).instance
68
+
69
+ assert_equal 'logs.google.com', instance.host
70
+ assert_equal "777", instance.port
71
+ assert_equal 'https', instance.scheme
72
+ assert_equal '/es/', instance.path
73
+ assert_equal 'john', instance.user
74
+ assert_equal 'doe', instance.password
75
+ end
76
+
77
+ def test_legacy_hosts_list
78
+ config = %{
79
+ hosts host1:50,host2:100,host3
80
+ scheme https
81
+ path /es/
82
+ port 123
83
+ }
84
+ instance = driver('test', config).instance
85
+
86
+ assert_equal 3, instance.get_connection_options(nil)[:hosts].length
87
+ host1, host2, host3 = instance.get_connection_options(nil)[:hosts]
88
+
89
+ assert_equal 'host1', host1[:host]
90
+ assert_equal 50, host1[:port]
91
+ assert_equal 'https', host1[:scheme]
92
+ assert_equal '/es/', host2[:path]
93
+ assert_equal 'host3', host3[:host]
94
+ assert_equal 123, host3[:port]
95
+ assert_equal 'https', host3[:scheme]
96
+ assert_equal '/es/', host3[:path]
97
+ end
98
+
99
+ def test_hosts_list
100
+ config = %{
101
+ hosts https://john:password@host1:443/elastic/,http://host2
102
+ path /default_path
103
+ user default_user
104
+ password default_password
105
+ }
106
+ instance = driver('test', config).instance
107
+
108
+ assert_equal 2, instance.get_connection_options(nil)[:hosts].length
109
+ host1, host2 = instance.get_connection_options(nil)[:hosts]
110
+
111
+ assert_equal 'host1', host1[:host]
112
+ assert_equal 443, host1[:port]
113
+ assert_equal 'https', host1[:scheme]
114
+ assert_equal 'john', host1[:user]
115
+ assert_equal 'password', host1[:password]
116
+ assert_equal '/elastic/', host1[:path]
117
+
118
+ assert_equal 'host2', host2[:host]
119
+ assert_equal 'http', host2[:scheme]
120
+ assert_equal 'default_user', host2[:user]
121
+ assert_equal 'default_password', host2[:password]
122
+ assert_equal '/default_path', host2[:path]
123
+ end
124
+
125
+ def test_single_host_params_and_defaults
126
+ config = %{
127
+ host logs.google.com
128
+ user john
129
+ password doe
130
+ }
131
+ instance = driver('test', config).instance
132
+
133
+ assert_equal 1, instance.get_connection_options(nil)[:hosts].length
134
+ host1 = instance.get_connection_options(nil)[:hosts][0]
135
+
136
+ assert_equal 'logs.google.com', host1[:host]
137
+ assert_equal 9200, host1[:port]
138
+ assert_equal 'http', host1[:scheme]
139
+ assert_equal 'john', host1[:user]
140
+ assert_equal 'doe', host1[:password]
141
+ assert_equal nil, host1[:path]
142
+ end
143
+
144
+ def test_writes_to_default_index
145
+ stub_elastic_ping
146
+ stub_elastic
147
+ driver.emit(sample_record)
148
+ driver.run
149
+ assert_equal('fluentd', index_cmds.first['index']['_index'])
150
+ end
151
+
152
+ def test_writes_to_default_type
153
+ stub_elastic_ping
154
+ stub_elastic
155
+ driver.emit(sample_record)
156
+ driver.run
157
+ assert_equal('fluentd', index_cmds.first['index']['_type'])
158
+ end
159
+
160
+ def test_writes_to_speficied_index
161
+ driver.configure("index_name myindex\n")
162
+ stub_elastic_ping
163
+ stub_elastic
164
+ driver.emit(sample_record)
165
+ driver.run
166
+ assert_equal('myindex', index_cmds.first['index']['_index'])
167
+ end
168
+
169
+ def test_writes_to_speficied_type
170
+ driver.configure("type_name mytype\n")
171
+ stub_elastic_ping
172
+ stub_elastic
173
+ driver.emit(sample_record)
174
+ driver.run
175
+ assert_equal('mytype', index_cmds.first['index']['_type'])
176
+ end
177
+
178
+ def test_writes_to_speficied_host
179
+ driver.configure("host 192.168.33.50\n")
180
+ stub_elastic_ping("http://192.168.33.50:9200")
181
+ elastic_request = stub_elastic("http://192.168.33.50:9200/_bulk")
182
+ driver.emit(sample_record)
183
+ driver.run
184
+ assert_requested(elastic_request)
185
+ end
186
+
187
+ def test_writes_to_speficied_port
188
+ driver.configure("port 9201\n")
189
+ stub_elastic_ping("http://localhost:9201")
190
+ elastic_request = stub_elastic("http://localhost:9201/_bulk")
191
+ driver.emit(sample_record)
192
+ driver.run
193
+ assert_requested(elastic_request)
194
+ end
195
+
196
+ def test_writes_to_multi_hosts
197
+ hosts = [['192.168.33.50', 9201], ['192.168.33.51', 9201], ['192.168.33.52', 9201]]
198
+ hosts_string = hosts.map {|x| "#{x[0]}:#{x[1]}"}.compact.join(',')
199
+
200
+ driver.configure("hosts #{hosts_string}")
201
+
202
+ hosts.each do |host_info|
203
+ host, port = host_info
204
+ stub_elastic_ping("http://#{host}:#{port}")
205
+ stub_elastic_with_store_index_command_counts("http://#{host}:#{port}/_bulk")
206
+ end
207
+
208
+ 1000.times do
209
+ driver.emit(sample_record.merge('age'=>rand(100)))
210
+ end
211
+
212
+ driver.run
213
+
214
+ # @note: we cannot make multi chunks with options (flush_interval, buffer_chunk_limit)
215
+ # it's Fluentd test driver's constraint
216
+ # so @index_command_counts.size is always 1
217
+
218
+ assert(@index_command_counts.size > 0, "not working with hosts options")
219
+
220
+ total = 0
221
+ @index_command_counts.each do |url, count|
222
+ total += count
223
+ end
224
+ assert_equal(2000, total)
225
+ end
226
+
227
+ def test_makes_bulk_request
228
+ stub_elastic_ping
229
+ stub_elastic
230
+ driver.emit(sample_record)
231
+ driver.emit(sample_record.merge('age' => 27))
232
+ driver.run
233
+ assert_equal(4, index_cmds.count)
234
+ end
235
+
236
+ def test_all_records_are_preserved_in_bulk
237
+ stub_elastic_ping
238
+ stub_elastic
239
+ driver.emit(sample_record)
240
+ driver.emit(sample_record.merge('age' => 27))
241
+ driver.run
242
+ assert_equal(26, index_cmds[1]['age'])
243
+ assert_equal(27, index_cmds[3]['age'])
244
+ end
245
+
246
+ def test_writes_to_logstash_index
247
+ driver.configure("logstash_format true\n")
248
+ time = Time.parse Date.today.to_s
249
+ logstash_index = "logstash-#{time.getutc.strftime("%Y.%m.%d")}"
250
+ stub_elastic_ping
251
+ stub_elastic
252
+ driver.emit(sample_record, time)
253
+ driver.run
254
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
255
+ end
256
+
257
+ def test_writes_to_logstash_utc_index
258
+ driver.configure("logstash_format true
259
+ utc_index false")
260
+ time = Time.parse Date.today.to_s
261
+ utc_index = "logstash-#{time.strftime("%Y.%m.%d")}"
262
+ stub_elastic_ping
263
+ stub_elastic
264
+ driver.emit(sample_record, time)
265
+ driver.run
266
+ assert_equal(utc_index, index_cmds.first['index']['_index'])
267
+ end
268
+
269
+ def test_writes_to_logstash_index_with_specified_prefix
270
+ driver.configure("logstash_format true
271
+ logstash_prefix myprefix")
272
+ time = Time.parse Date.today.to_s
273
+ logstash_index = "myprefix-#{time.getutc.strftime("%Y.%m.%d")}"
274
+ stub_elastic_ping
275
+ stub_elastic
276
+ driver.emit(sample_record, time)
277
+ driver.run
278
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
279
+ end
280
+
281
+ def test_writes_to_logstash_index_with_specified_dateformat
282
+ driver.configure("logstash_format true
283
+ logstash_dateformat %Y.%m")
284
+ time = Time.parse Date.today.to_s
285
+ logstash_index = "logstash-#{time.getutc.strftime("%Y.%m")}"
286
+ stub_elastic_ping
287
+ stub_elastic
288
+ driver.emit(sample_record, time)
289
+ driver.run
290
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
291
+ end
292
+
293
+ def test_writes_to_logstash_index_with_specified_prefix_and_dateformat
294
+ driver.configure("logstash_format true
295
+ logstash_prefix myprefix
296
+ logstash_dateformat %Y.%m")
297
+ time = Time.parse Date.today.to_s
298
+ logstash_index = "myprefix-#{time.getutc.strftime("%Y.%m")}"
299
+ stub_elastic_ping
300
+ stub_elastic
301
+ driver.emit(sample_record, time)
302
+ driver.run
303
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
304
+ end
305
+
306
+ def test_doesnt_add_logstash_timestamp_by_default
307
+ stub_elastic_ping
308
+ stub_elastic
309
+ driver.emit(sample_record)
310
+ driver.run
311
+ assert_nil(index_cmds[1]['@timestamp'])
312
+ end
313
+
314
+ def test_adds_logstash_timestamp_when_configured
315
+ driver.configure("logstash_format true\n")
316
+ stub_elastic_ping
317
+ stub_elastic
318
+ ts = DateTime.now.to_s
319
+ driver.emit(sample_record)
320
+ driver.run
321
+ assert(index_cmds[1].has_key? '@timestamp')
322
+ assert_equal(index_cmds[1]['@timestamp'], ts)
323
+ end
324
+
325
+ def test_uses_custom_timestamp_when_included_in_record
326
+ driver.configure("logstash_format true\n")
327
+ stub_elastic_ping
328
+ stub_elastic
329
+ ts = DateTime.new(2001,2,3).to_s
330
+ driver.emit(sample_record.merge!('@timestamp' => ts))
331
+ driver.run
332
+ assert(index_cmds[1].has_key? '@timestamp')
333
+ assert_equal(index_cmds[1]['@timestamp'], ts)
334
+ end
335
+
336
+ def test_uses_custom_time_key
337
+ driver.configure("logstash_format true
338
+ time_key vtm\n")
339
+ stub_elastic_ping
340
+ stub_elastic
341
+ ts = DateTime.new(2001,2,3).to_s
342
+ driver.emit(sample_record.merge!('vtm' => ts))
343
+ driver.run
344
+ assert(index_cmds[1].has_key? '@timestamp')
345
+ assert_equal(index_cmds[1]['@timestamp'], ts)
346
+ end
347
+
348
+ def test_doesnt_add_tag_key_by_default
349
+ stub_elastic_ping
350
+ stub_elastic
351
+ driver.emit(sample_record)
352
+ driver.run
353
+ assert_nil(index_cmds[1]['tag'])
354
+ end
355
+
356
+ def test_adds_tag_key_when_configured
357
+ driver('mytag').configure("include_tag_key true\n")
358
+ stub_elastic_ping
359
+ stub_elastic
360
+ driver.emit(sample_record)
361
+ driver.run
362
+ assert(index_cmds[1].has_key?('tag'))
363
+ assert_equal(index_cmds[1]['tag'], 'mytag')
364
+ end
365
+
366
+ def test_adds_id_key_when_configured
367
+ driver.configure("id_key request_id\n")
368
+ stub_elastic_ping
369
+ stub_elastic
370
+ driver.emit(sample_record)
371
+ driver.run
372
+ assert_equal(index_cmds[0]['index']['_id'], '42')
373
+ end
374
+
375
+ def test_doesnt_add_id_key_if_missing_when_configured
376
+ driver.configure("id_key another_request_id\n")
377
+ stub_elastic_ping
378
+ stub_elastic
379
+ driver.emit(sample_record)
380
+ driver.run
381
+ assert(!index_cmds[0]['index'].has_key?('_id'))
382
+ end
383
+
384
+ def test_adds_id_key_when_not_configured
385
+ stub_elastic_ping
386
+ stub_elastic
387
+ driver.emit(sample_record)
388
+ driver.run
389
+ assert(!index_cmds[0]['index'].has_key?('_id'))
390
+ end
391
+
392
+ def test_adds_parent_key_when_configured
393
+ driver.configure("parent_key parent_id\n")
394
+ stub_elastic_ping
395
+ stub_elastic
396
+ driver.emit(sample_record)
397
+ driver.run
398
+ assert_equal(index_cmds[0]['index']['_parent'], 'parent')
399
+ end
400
+
401
+ def test_doesnt_add_parent_key_if_missing_when_configured
402
+ driver.configure("parent_key another_parent_id\n")
403
+ stub_elastic_ping
404
+ stub_elastic
405
+ driver.emit(sample_record)
406
+ driver.run
407
+ assert(!index_cmds[0]['index'].has_key?('_parent'))
408
+ end
409
+
410
+ def test_adds_parent_key_when_not_configured
411
+ stub_elastic_ping
412
+ stub_elastic
413
+ driver.emit(sample_record)
414
+ driver.run
415
+ assert(!index_cmds[0]['index'].has_key?('_parent'))
416
+ end
417
+
418
+ def test_request_error
419
+ stub_elastic_ping
420
+ stub_elastic_unavailable
421
+ driver.emit(sample_record)
422
+ assert_raise(Elasticsearch::Transport::Transport::Errors::ServiceUnavailable) {
423
+ driver.run
424
+ }
425
+ end
426
+
427
+ def test_garbage_record_error
428
+ stub_elastic_ping
429
+ stub_elastic
430
+ driver.emit("some garbage string")
431
+ driver.run
432
+ end
433
+
434
+ def test_connection_failed_retry
435
+ connection_resets = 0
436
+
437
+ stub_elastic_ping(url="http://localhost:9200").with do |req|
438
+ connection_resets += 1
439
+ end
440
+
441
+ stub_request(:post, "http://localhost:9200/_bulk").with do |req|
442
+ raise Faraday::ConnectionFailed, "Test message"
443
+ end
444
+
445
+ driver.emit(sample_record)
446
+
447
+ assert_raise(Fluent::ElasticsearchOutput::ConnectionFailure) {
448
+ driver.run
449
+ }
450
+ assert_equal(connection_resets, 3)
451
+ end
452
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-elasticsearch
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - diogo
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-07-27 00:00:00.000000000 Z
12
+ date: 2015-10-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: fluentd
@@ -127,8 +127,10 @@ files:
127
127
  - Rakefile
128
128
  - fluent-plugin-elasticsearch.gemspec
129
129
  - lib/fluent/plugin/out_elasticsearch.rb
130
+ - lib/fluent/plugin/out_elasticsearch_dynamic.rb
130
131
  - test/helper.rb
131
132
  - test/plugin/test_out_elasticsearch.rb
133
+ - test/plugin/test_out_elasticsearch_dynamic.rb
132
134
  homepage: https://github.com/uken/fluent-plugin-elasticsearch
133
135
  licenses:
134
136
  - MIT
@@ -149,10 +151,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
149
151
  version: '0'
150
152
  requirements: []
151
153
  rubyforge_project:
152
- rubygems_version: 2.4.5
154
+ rubygems_version: 2.2.2
153
155
  signing_key:
154
156
  specification_version: 4
155
157
  summary: ElasticSearch output plugin for Fluent event collector
156
158
  test_files:
157
159
  - test/helper.rb
158
160
  - test/plugin/test_out_elasticsearch.rb
161
+ - test/plugin/test_out_elasticsearch_dynamic.rb