logstash-output-elasticsearch-leprechaun-fork 1.0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/CHANGELOG.md +30 -0
  4. data/CONTRIBUTORS +31 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE +13 -0
  7. data/NOTICE.TXT +5 -0
  8. data/README.md +98 -0
  9. data/Rakefile +1 -0
  10. data/lib/logstash-output-elasticsearch_jars.rb +5 -0
  11. data/lib/logstash/outputs/elasticsearch.rb +784 -0
  12. data/lib/logstash/outputs/elasticsearch/elasticsearch-template.json +41 -0
  13. data/lib/logstash/outputs/elasticsearch/protocol.rb +339 -0
  14. data/logstash-output-elasticsearch.gemspec +40 -0
  15. data/spec/es_spec_helper.rb +65 -0
  16. data/spec/integration/outputs/elasticsearch/node_spec.rb +36 -0
  17. data/spec/integration/outputs/index_spec.rb +90 -0
  18. data/spec/integration/outputs/retry_spec.rb +156 -0
  19. data/spec/integration/outputs/routing_spec.rb +114 -0
  20. data/spec/integration/outputs/secure_spec.rb +113 -0
  21. data/spec/integration/outputs/templates_spec.rb +97 -0
  22. data/spec/integration/outputs/transport_create_spec.rb +94 -0
  23. data/spec/integration/outputs/update_spec.rb +87 -0
  24. data/spec/unit/outputs/elasticsearch/protocol_spec.rb +54 -0
  25. data/spec/unit/outputs/elasticsearch_proxy_spec.rb +59 -0
  26. data/spec/unit/outputs/elasticsearch_spec.rb +183 -0
  27. data/spec/unit/outputs/elasticsearch_ssl_spec.rb +82 -0
  28. data/vendor/jar-dependencies/runtime-jars/antlr-runtime-3.5.jar +0 -0
  29. data/vendor/jar-dependencies/runtime-jars/asm-4.1.jar +0 -0
  30. data/vendor/jar-dependencies/runtime-jars/asm-commons-4.1.jar +0 -0
  31. data/vendor/jar-dependencies/runtime-jars/elasticsearch-1.7.0.jar +0 -0
  32. data/vendor/jar-dependencies/runtime-jars/lucene-analyzers-common-4.10.4.jar +0 -0
  33. data/vendor/jar-dependencies/runtime-jars/lucene-core-4.10.4.jar +0 -0
  34. data/vendor/jar-dependencies/runtime-jars/lucene-grouping-4.10.4.jar +0 -0
  35. data/vendor/jar-dependencies/runtime-jars/lucene-highlighter-4.10.4.jar +0 -0
  36. data/vendor/jar-dependencies/runtime-jars/lucene-join-4.10.4.jar +0 -0
  37. data/vendor/jar-dependencies/runtime-jars/lucene-memory-4.10.4.jar +0 -0
  38. data/vendor/jar-dependencies/runtime-jars/lucene-misc-4.10.4.jar +0 -0
  39. data/vendor/jar-dependencies/runtime-jars/lucene-queries-4.10.4.jar +0 -0
  40. data/vendor/jar-dependencies/runtime-jars/lucene-queryparser-4.10.4.jar +0 -0
  41. data/vendor/jar-dependencies/runtime-jars/lucene-sandbox-4.10.4.jar +0 -0
  42. data/vendor/jar-dependencies/runtime-jars/lucene-spatial-4.10.4.jar +0 -0
  43. data/vendor/jar-dependencies/runtime-jars/lucene-suggest-4.10.4.jar +0 -0
  44. data/vendor/jar-dependencies/runtime-jars/spatial4j-0.4.1.jar +0 -0
  45. metadata +246 -0
@@ -0,0 +1,41 @@
1
+ {
2
+ "template" : "logstash-*",
3
+ "settings" : {
4
+ "index.refresh_interval" : "5s"
5
+ },
6
+ "mappings" : {
7
+ "_default_" : {
8
+ "_all" : {"enabled" : true, "omit_norms" : true},
9
+ "dynamic_templates" : [ {
10
+ "message_field" : {
11
+ "match" : "message",
12
+ "match_mapping_type" : "string",
13
+ "mapping" : {
14
+ "type" : "string", "index" : "analyzed", "omit_norms" : true
15
+ }
16
+ }
17
+ }, {
18
+ "string_fields" : {
19
+ "match" : "*",
20
+ "match_mapping_type" : "string",
21
+ "mapping" : {
22
+ "type" : "string", "index" : "analyzed", "omit_norms" : true,
23
+ "fields" : {
24
+ "raw" : {"type": "string", "index" : "not_analyzed", "ignore_above" : 256}
25
+ }
26
+ }
27
+ }
28
+ } ],
29
+ "properties" : {
30
+ "@version": { "type": "string", "index": "not_analyzed" },
31
+ "geoip" : {
32
+ "type" : "object",
33
+ "dynamic": true,
34
+ "properties" : {
35
+ "location" : { "type" : "geo_point" }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,339 @@
1
+ require "logstash/outputs/elasticsearch"
2
+ require "cabin"
3
+ require "base64"
4
+
5
+ module LogStash::Outputs::Elasticsearch
6
+ module Protocols
7
+ class Base
8
+ private
9
+ def initialize(options={})
10
+ # host(s), port, cluster
11
+ @logger = Cabin::Channel.get
12
+ end
13
+
14
+ def client
15
+ return @client if @client
16
+ @client = build_client(@options)
17
+ return @client
18
+ end # def client
19
+
20
+
21
+ def template_install(name, template, force=false)
22
+ if template_exists?(name) && !force
23
+ @logger.debug("Found existing Elasticsearch template. Skipping template management", :name => name)
24
+ return
25
+ end
26
+ template_put(name, template)
27
+ end
28
+
29
+ # Do a bulk request with the given actions.
30
+ #
31
+ # 'actions' is expected to be an array of bulk requests as string json
32
+ # values.
33
+ #
34
+ # Each 'action' becomes a single line in the bulk api call. For more
35
+ # details on the format of each.
36
+ def bulk(actions)
37
+ raise NotImplemented, "You must implement this yourself"
38
+ # bulk([
39
+ # '{ "index" : { "_index" : "test", "_type" : "type1", "_id" : "1" } }',
40
+ # '{ "field1" : "value1" }'
41
+ #])
42
+ end
43
+
44
+ public(:initialize, :template_install)
45
+ end
46
+
47
+ class HTTPClient < Base
48
+ private
49
+
50
+ DEFAULT_OPTIONS = {
51
+ :port => 9200
52
+ }
53
+
54
+ def initialize(options={})
55
+ super
56
+ require "elasticsearch" # gem 'elasticsearch-ruby'
57
+ # manticore http transport
58
+ require "elasticsearch/transport/transport/http/manticore"
59
+ @options = DEFAULT_OPTIONS.merge(options)
60
+ @client = client
61
+ end
62
+
63
+ def build_client(options)
64
+ uri = "#{options[:protocol]}://#{options[:host]}:#{options[:port]}#{options[:client_settings][:path]}"
65
+
66
+ client_options = {
67
+ :host => [uri],
68
+ :ssl => options[:client_settings][:ssl],
69
+ :transport_options => { # manticore settings so we
70
+ :socket_timeout => 0, # do not timeout socket reads
71
+ :request_timeout => 0, # and requests
72
+ :proxy => options[:client_settings][:proxy]
73
+ },
74
+ :transport_class => ::Elasticsearch::Transport::Transport::HTTP::Manticore
75
+ }
76
+
77
+ if options[:user] && options[:password] then
78
+ token = Base64.strict_encode64(options[:user] + ":" + options[:password])
79
+ client_options[:headers] = { "Authorization" => "Basic #{token}" }
80
+ end
81
+
82
+ Elasticsearch::Client.new client_options
83
+ end
84
+
85
+ def self.normalize_bulk_response(bulk_response)
86
+ if bulk_response["errors"]
87
+ # The structure of the response from the REST Bulk API is follows:
88
+ # {"took"=>74, "errors"=>true, "items"=>[{"create"=>{"_index"=>"logstash-2014.11.17",
89
+ # "_type"=>"logs",
90
+ # "_id"=>"AUxTS2C55Jrgi-hC6rQF",
91
+ # "_version"=>1,
92
+ # "status"=>400,
93
+ # "error"=>"MapperParsingException[failed to parse]..."}}]}
94
+ # where each `item` is a hash of {OPTYPE => Hash[]}. calling first, will retrieve
95
+ # this hash as a single array with two elements, where the value is the second element (i.first[1])
96
+ # then the status of that item is retrieved.
97
+ {
98
+ "errors" => true,
99
+ "statuses" => bulk_response["items"].map { |i| i.first[1]['status'] },
100
+ "error_messages" => bulk_response["items"].map { |i| i.first[1]['error'] }
101
+ }
102
+ else
103
+ {"errors" => false}
104
+ end
105
+ end
106
+
107
+ def bulk(actions)
108
+ bulk_response = @client.bulk(:body => actions.collect do |action, args, source|
109
+ if action == 'update'
110
+ if args[:_id]
111
+ source = { 'doc' => source }
112
+ if @options[:doc_as_upsert]
113
+ source['doc_as_upsert'] = true
114
+ else
115
+ source['upsert'] = args[:_upsert] if args[:_upsert]
116
+ end
117
+ else
118
+ raise(LogStash::ConfigurationError, "Specifying action => 'update' without a document '_id' is not supported.")
119
+ end
120
+ end
121
+ args.delete(:_upsert)
122
+ if source
123
+ next [ { action => args }, source ]
124
+ else
125
+ next { action => args }
126
+ end
127
+ end.flatten)
128
+
129
+ self.class.normalize_bulk_response(bulk_response)
130
+ end # def bulk
131
+
132
+ def template_exists?(name)
133
+ @client.indices.get_template(:name => name)
134
+ return true
135
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
136
+ return false
137
+ end # def template_exists?
138
+
139
+ def template_put(name, template)
140
+ @client.indices.put_template(:name => name, :body => template)
141
+ end # template_put
142
+
143
+ public(:bulk)
144
+ end # class HTTPClient
145
+
146
+ class NodeClient < Base
147
+ private
148
+
149
+ DEFAULT_OPTIONS = {
150
+ :port => 9300,
151
+ }
152
+
153
+ def initialize(options={})
154
+ super
155
+ require "java"
156
+ @options = DEFAULT_OPTIONS.merge(options)
157
+ setup(@options)
158
+ @client = client
159
+ end # def initialize
160
+
161
+ def settings
162
+ return @settings
163
+ end
164
+
165
+ def setup(options={})
166
+ @settings = org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder
167
+ if options[:host]
168
+ @settings.put("discovery.zen.ping.multicast.enabled", false)
169
+ @settings.put("discovery.zen.ping.unicast.hosts", NodeClient.hosts(options))
170
+ end
171
+
172
+ @settings.put("node.client", true)
173
+ @settings.put("http.enabled", false)
174
+
175
+ if options[:client_settings]
176
+ options[:client_settings].each do |key, value|
177
+ @settings.put(key, value)
178
+ end
179
+ end
180
+
181
+ return @settings
182
+ end
183
+
184
+ def self.hosts(options)
185
+ # http://www.elasticsearch.org/guide/reference/modules/discovery/zen/
186
+ result = Array.new
187
+ if options[:host].class == Array
188
+ options[:host].each do |host|
189
+ if host.to_s =~ /^.+:.+$/
190
+ # For host in format: host:port, ignore options[:port]
191
+ result << host
192
+ else
193
+ if options[:port].to_s =~ /^\d+-\d+$/
194
+ # port ranges are 'host[port1-port2]'
195
+ result << Range.new(*options[:port].split("-")).collect { |p| "#{host}:#{p}" }
196
+ else
197
+ result << "#{host}:#{options[:port]}"
198
+ end
199
+ end
200
+ end
201
+ else
202
+ if options[:host].to_s =~ /^.+:.+$/
203
+ # For host in format: host:port, ignore options[:port]
204
+ result << options[:host]
205
+ else
206
+ if options[:port].to_s =~ /^\d+-\d+$/
207
+ # port ranges are 'host[port1-port2]' according to
208
+ # http://www.elasticsearch.org/guide/reference/modules/discovery/zen/
209
+ # However, it seems to only query the first port.
210
+ # So generate our own list of unicast hosts to scan.
211
+ range = Range.new(*options[:port].split("-"))
212
+ result << range.collect { |p| "#{options[:host]}:#{p}" }
213
+ else
214
+ result << "#{options[:host]}:#{options[:port]}"
215
+ end
216
+ end
217
+ end
218
+ result.flatten.join(",")
219
+ end # def self.hosts
220
+
221
+ def build_client(options)
222
+ nodebuilder = org.elasticsearch.node.NodeBuilder.nodeBuilder
223
+ return nodebuilder.settings(@settings).node.client
224
+ end # def build_client
225
+
226
+ def self.normalize_bulk_response(bulk_response)
227
+ # TODO(talevy): parse item response objects to retrieve correct 200 (OK) or 201(created) status codes
228
+ if bulk_response.has_failures()
229
+ {
230
+ "errors" => true,
231
+ "statuses" => bulk_response.map { |i| (i.is_failed && i.get_failure.get_status.get_status) || 200 },
232
+ "error_messages" => bulk_response.map { |i| (i.is_failed && i.get_failure.getMessage) || 200 }
233
+ }
234
+ else
235
+ {"errors" => false}
236
+ end
237
+ end
238
+
239
+ def bulk(actions)
240
+ # Actions an array of [ action, action_metadata, source ]
241
+ prep = @client.prepareBulk
242
+ actions.each do |action, args, source|
243
+ prep.add(build_request(action, args, source))
244
+ end
245
+ response = prep.execute.actionGet()
246
+
247
+ self.class.normalize_bulk_response(response)
248
+ end # def bulk
249
+
250
+ def build_request(action, args, source)
251
+ case action
252
+ when "index"
253
+ request = org.elasticsearch.action.index.IndexRequest.new(args[:_index])
254
+ request.id(args[:_id]) if args[:_id]
255
+ request.routing(args[:_routing]) if args[:_routing]
256
+ request.source(source)
257
+ when "delete"
258
+ request = org.elasticsearch.action.delete.DeleteRequest.new(args[:_index])
259
+ request.id(args[:_id])
260
+ request.routing(args[:_routing]) if args[:_routing]
261
+ when "create"
262
+ request = org.elasticsearch.action.index.IndexRequest.new(args[:_index])
263
+ request.id(args[:_id]) if args[:_id]
264
+ request.routing(args[:_routing]) if args[:_routing]
265
+ request.source(source)
266
+ request.opType("create")
267
+ when "create_unless_exists"
268
+ unless args[:_id].nil?
269
+ request = org.elasticsearch.action.index.IndexRequest.new(args[:_index])
270
+ request.id(args[:_id])
271
+ request.routing(args[:_routing]) if args[:_routing]
272
+ request.source(source)
273
+ request.opType("create")
274
+ else
275
+ raise(LogStash::ConfigurationError, "Specifying action => 'create_unless_exists' without a document '_id' is not supported.")
276
+ end
277
+ when "update"
278
+ unless args[:_id].nil?
279
+ request = org.elasticsearch.action.update.UpdateRequest.new(args[:_index], args[:_type], args[:_id])
280
+ request.routing(args[:_routing]) if args[:_routing]
281
+ request.doc(source)
282
+ if @options[:doc_as_upsert]
283
+ request.docAsUpsert(true)
284
+ else
285
+ request.upsert(args[:_upsert]) if args[:_upsert]
286
+ end
287
+ else
288
+ raise(LogStash::ConfigurationError, "Specifying action => 'update' without a document '_id' is not supported.")
289
+ end
290
+ else
291
+ raise(LogStash::ConfigurationError, "action => '#{action_name}' is not currently supported.")
292
+ end # case action
293
+
294
+ request.type(args[:_type]) if args[:_type]
295
+ return request
296
+ end # def build_request
297
+
298
+ def template_exists?(name)
299
+ request = org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesRequestBuilder.new(@client.admin.indices, name)
300
+ response = request.get
301
+ return !response.getIndexTemplates.isEmpty
302
+ end # def template_exists?
303
+
304
+ def template_put(name, template)
305
+ request = org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequestBuilder.new(@client.admin.indices, name)
306
+ request.setSource(LogStash::Json.dump(template))
307
+
308
+ # execute the request and get the response, if it fails, we'll get an exception.
309
+ request.get
310
+ end # template_put
311
+
312
+ public(:initialize, :bulk)
313
+ end # class NodeClient
314
+
315
+ class TransportClient < NodeClient
316
+ private
317
+ def build_client(options)
318
+ client = org.elasticsearch.client.transport.TransportClient.new(settings.build)
319
+
320
+ if options[:host]
321
+ client.addTransportAddress(
322
+ org.elasticsearch.common.transport.InetSocketTransportAddress.new(
323
+ options[:host], options[:port].to_i
324
+ )
325
+ )
326
+ end
327
+
328
+ return client
329
+ end # def build_client
330
+ end # class TransportClient
331
+ end # module Protocols
332
+
333
+ module Requests
334
+ class GetIndexTemplates; end
335
+ class Bulk; end
336
+ class Index; end
337
+ class Delete; end
338
+ end
339
+ end
@@ -0,0 +1,40 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'logstash-output-elasticsearch-leprechaun-fork'
4
+ s.version = '1.0.8'
5
+ s.licenses = ['apache-2.0']
6
+ s.summary = "Logstash Output to Elasticsearch"
7
+ s.description = "Output events to elasticsearch"
8
+ s.authors = ["Elastic", "Laurence MacGuire"]
9
+ s.email = 'leprechaun@gmail.com'
10
+ s.homepage = "http://logstash.net/"
11
+ s.require_paths = ["lib"]
12
+
13
+ # Files
14
+ s.files = `git ls-files`.split($\)
15
+
16
+ # Tests
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+
19
+ # Special flag to let us know this is actually a logstash plugin
20
+ s.metadata = { "logstash_plugin" => "true", "logstash_group" => "output" }
21
+
22
+ # Gem dependencies
23
+ s.add_runtime_dependency 'concurrent-ruby'
24
+ s.add_runtime_dependency 'elasticsearch', ['>= 1.0.10', '~> 1.0']
25
+ s.add_runtime_dependency 'stud', ['>= 0.0.17', '~> 0.0']
26
+ s.add_runtime_dependency 'cabin', ['~> 0.6']
27
+ s.add_runtime_dependency "logstash-core", '>= 1.4.0', '< 2.0.0'
28
+
29
+ s.add_development_dependency 'ftw', '~> 0.0.42'
30
+ s.add_development_dependency 'logstash-input-generator'
31
+
32
+
33
+ if RUBY_PLATFORM == 'java'
34
+ s.platform = RUBY_PLATFORM
35
+ s.add_runtime_dependency "manticore", '~> 0.4.2'
36
+ end
37
+
38
+ s.add_development_dependency 'logstash-devutils'
39
+ s.add_development_dependency 'longshoreman'
40
+ end
@@ -0,0 +1,65 @@
1
+ require "logstash/devutils/rspec/spec_helper"
2
+ require "ftw"
3
+ require "logstash/plugin"
4
+ require "logstash/json"
5
+ require "stud/try"
6
+ require "longshoreman"
7
+
8
+ CONTAINER_NAME = "logstash-output-elasticsearch-#{rand(999).to_s}"
9
+ CONTAINER_IMAGE = "elasticsearch"
10
+ CONTAINER_TAG = "1.6"
11
+
12
+ module ESHelper
13
+
14
+ def get_host
15
+ Longshoreman.new.get_host_ip
16
+ end
17
+
18
+ def get_port(protocol)
19
+ container = Longshoreman::Container.new
20
+ container.get(CONTAINER_NAME)
21
+ case protocol
22
+ when "http"
23
+ container.rport(9200)
24
+ when "transport", "node"
25
+ container.rport(9300)
26
+ end
27
+ end
28
+
29
+ def get_client
30
+ Elasticsearch::Client.new(:host => "#{get_host}:#{get_port('http')}")
31
+ end
32
+ end
33
+
34
+ RSpec.configure do |config|
35
+ config.include ESHelper
36
+
37
+ # this :all hook gets run before every describe block that is tagged with :integration => true.
38
+ config.before(:all, :integration => true) do
39
+ # check if container exists already before creating new one.
40
+ begin
41
+ ls = Longshoreman::new
42
+ ls.container.get(CONTAINER_NAME)
43
+ rescue Docker::Error::NotFoundError
44
+ Longshoreman.new("#{CONTAINER_IMAGE}:#{CONTAINER_TAG}", CONTAINER_NAME)
45
+ # TODO(talevy): verify ES is running instead of static timeout
46
+ sleep 10
47
+ end
48
+ end
49
+
50
+ # we want to do a final cleanup after all :integration runs,
51
+ # but we don't want to clean up before the last block.
52
+ # This is a final blind check to see if the ES docker container is running and
53
+ # needs to be cleaned up. If no container can be found and/or docker is not
54
+ # running on the system, we do nothing.
55
+ config.after(:suite) do
56
+ # only cleanup docker container if system has docker and the container is running
57
+ begin
58
+ ls = Longshoreman::new
59
+ ls.container.get(CONTAINER_NAME)
60
+ ls.cleanup
61
+ rescue Docker::Error::NotFoundError, Excon::Errors::SocketError
62
+ # do nothing
63
+ end
64
+ end
65
+ end