logstash-output-elasticsearch-leprechaun-fork 1.0.8

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.
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