logstash-output-amazon_es 0.1.0-java

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.
@@ -0,0 +1,103 @@
1
+ require 'faraday'
2
+ require 'faraday_middleware'
3
+ require 'aws-sdk-core'
4
+ require 'elasticsearch'
5
+ require 'elasticsearch-transport'
6
+ require 'manticore'
7
+ require 'faraday/adapter/manticore'
8
+
9
+ #
10
+ require 'uri'
11
+
12
+ require_relative "aws_v4_signer"
13
+
14
+
15
+ module Elasticsearch
16
+ module Transport
17
+ module Transport
18
+ module HTTP
19
+
20
+ # Transport implementation, which V4 Signs requests using the [_Faraday_](https://rubygems.org/gems/faraday)
21
+ # library for abstracting the HTTP client.
22
+ #
23
+ # @see Transport::Base
24
+ #
25
+ class AWS
26
+ include Elasticsearch::Transport::Transport::Base
27
+
28
+
29
+ DEFAULT_PORT = 80
30
+ DEFAULT_PROTOCOL = "http"
31
+
32
+ CredentialConfig = Struct.new(
33
+ :access_key_id,
34
+ :secret_access_key,
35
+ :session_token,
36
+ :profile
37
+ )
38
+
39
+ # Performs the request by invoking {Transport::Base#perform_request} with a block.
40
+ #
41
+ # @return [Response]
42
+ # @see Transport::Base#perform_request
43
+ #
44
+ def perform_request(method, path, params={}, body=nil)
45
+ super do |connection, url|
46
+ response = connection.connection.run_request \
47
+ method.downcase.to_sym,
48
+ url,
49
+ ( body ? __convert_to_json(body) : nil ),
50
+ {}
51
+
52
+ Response.new response.status, response.body, response.headers
53
+ end
54
+ end
55
+
56
+ # Builds and returns a collection of connections.
57
+ #
58
+ # @return [Connections::Collection]
59
+ #
60
+ def __build_connections
61
+ region = options[:region]
62
+ access_key_id = options[:aws_access_key_id] || nil
63
+ secret_access_key = options[:aws_secret_access_key] || nil
64
+ session_token = options[:session_token] || nil
65
+ profile = options[:profile] || 'default'
66
+
67
+ credential_config = CredentialConfig.new(access_key_id, secret_access_key, session_token, profile)
68
+ credentials = Aws::CredentialProviderChain.new(credential_config).resolve
69
+
70
+ Connections::Collection.new \
71
+ :connections => hosts.map { |host|
72
+ host[:protocol] = host[:scheme] || DEFAULT_PROTOCOL
73
+ host[:port] ||= DEFAULT_PORT
74
+ url = __full_url(host)
75
+
76
+ aes_connection = ::Faraday::Connection.new(url, (options[:transport_options] || {})) do |faraday|
77
+ faraday.request :aws_v4_signer,
78
+ credentials: credentials,
79
+ service_name: 'es',
80
+ region: region
81
+ faraday.adapter :manticore
82
+ end
83
+
84
+ Connections::Connection.new \
85
+ :host => host,
86
+ :connection => aes_connection
87
+ },
88
+ :selector_class => options[:selector_class],
89
+ :selector => options[:selector]
90
+ end
91
+
92
+ # Returns an array of implementation specific connection errors.
93
+ #
94
+ # @return [Array]
95
+ #
96
+ def host_unreachable_exceptions
97
+ [::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError]
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,7 @@
1
+ require 'faraday'
2
+ require_relative 'aws_v4_signer_impl'
3
+
4
+ module FaradayMiddleware
5
+ Faraday::Request.register_middleware :aws_v4_signer => lambda { AwsV4Signer }
6
+ end
7
+
@@ -0,0 +1,58 @@
1
+ require 'aws-sdk-core'
2
+ require 'faraday'
3
+
4
+
5
+ class AwsV4Signer < Faraday::Middleware
6
+ class Request
7
+ def initialize(env)
8
+ @env = env
9
+ end
10
+
11
+ def headers
12
+ @env.request_headers
13
+ end
14
+
15
+ def set_header(header)
16
+ @env.request_headers = header
17
+ end
18
+
19
+ def body
20
+ @env.body || ''
21
+ end
22
+
23
+ def endpoint
24
+ @env.url
25
+ end
26
+
27
+ def http_method
28
+ @env.method.to_s.upcase
29
+ end
30
+ end
31
+
32
+ def initialize(app, options = nil)
33
+ super(app)
34
+ credentials = options.fetch(:credentials)
35
+ service_name = options.fetch(:service_name)
36
+ region = options.fetch(:region)
37
+ @signer = Aws::Signers::V4.new(credentials, service_name, region)
38
+ @net_http = app.is_a?(Faraday::Adapter::NetHttp)
39
+ end
40
+
41
+ def call(env)
42
+ normalize_for_net_http!(env)
43
+ req = Request.new(env)
44
+ @signer.sign(req)
45
+ @app.call(env)
46
+ end
47
+
48
+ private
49
+ def normalize_for_net_http!(env)
50
+ return unless @net_http
51
+
52
+ if Net::HTTP::HAVE_ZLIB
53
+ env.request_headers['Accept-Encoding'] ||= 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3'
54
+ end
55
+
56
+ env.request_headers['Accept'] ||= '*/*'
57
+ end
58
+ end
@@ -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,117 @@
1
+ require "logstash/outputs/amazon_es"
2
+ require "cabin"
3
+ require "base64"
4
+ require "elasticsearch"
5
+
6
+ require_relative "aws_transport"
7
+
8
+ module LogStash::Outputs::AES
9
+ class HttpClient
10
+ attr_reader :client, :options, :client_options
11
+ DEFAULT_OPTIONS = {
12
+ :port => 80
13
+ }
14
+
15
+ def initialize(options={})
16
+ @logger = Cabin::Channel.get
17
+ @options = DEFAULT_OPTIONS.merge(options)
18
+ @client = build_client(@options)
19
+ end
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
+ def bulk(actions)
30
+ bulk_body = actions.collect do |action, args, source|
31
+ if action == 'update'
32
+ if args[:_id]
33
+ source = { 'doc' => source }
34
+ if @options[:doc_as_upsert]
35
+ source['doc_as_upsert'] = true
36
+ else
37
+ source['upsert'] = args[:_upsert] if args[:_upsert]
38
+ end
39
+ else
40
+ raise(LogStash::ConfigurationError, "Specifying action => 'update' without a document '_id' is not supported.")
41
+ end
42
+ end
43
+
44
+ args.delete(:_upsert)
45
+
46
+ if source
47
+ next [ { action => args }, source ]
48
+ else
49
+ next { action => args }
50
+ end
51
+ end.flatten
52
+
53
+ bulk_response = @client.bulk(:body => bulk_body)
54
+
55
+ self.class.normalize_bulk_response(bulk_response)
56
+ end
57
+
58
+ private
59
+ def build_client(options)
60
+ hosts = options[:hosts]
61
+ port = options[:port]
62
+ client_settings = options[:client_settings] || {}
63
+
64
+ uris = hosts.map do |host|
65
+ "http://#{host}:#{port}#{client_settings[:path]}".gsub(/[\/]+$/,'')
66
+ end
67
+
68
+ @client_options = {
69
+ :hosts => uris,
70
+ :region => options[:region],
71
+ :aws_access_key_id => options[:aws_access_key_id],
72
+ :aws_secret_access_key => options[:aws_secret_access_key],
73
+ :transport_options => {
74
+ :request => {:open_timeout => 0, :timeout => 60}, # ELB timeouts are set at 60
75
+ :proxy => client_settings[:proxy],
76
+ },
77
+ :transport_class => Elasticsearch::Transport::Transport::HTTP::AWS
78
+ }
79
+
80
+ if options[:user] && options[:password] then
81
+ token = Base64.strict_encode64(options[:user] + ":" + options[:password])
82
+ @client_options[:headers] = { "Authorization" => "Basic #{token}" }
83
+ end
84
+
85
+ Elasticsearch::Client.new(client_options)
86
+ end
87
+
88
+ def self.normalize_bulk_response(bulk_response)
89
+ if bulk_response["errors"]
90
+ # The structure of the response from the REST Bulk API is follows:
91
+ # {"took"=>74, "errors"=>true, "items"=>[{"create"=>{"_index"=>"logstash-2014.11.17",
92
+ # "_type"=>"logs",
93
+ # "_id"=>"AUxTS2C55Jrgi-hC6rQF",
94
+ # "_version"=>1,
95
+ # "status"=>400,
96
+ # "error"=>"MapperParsingException[failed to parse]..."}}]}
97
+ # where each `item` is a hash of {OPTYPE => Hash[]}. calling first, will retrieve
98
+ # this hash as a single array with two elements, where the value is the second element (i.first[1])
99
+ # then the status of that item is retrieved.
100
+ {"errors" => true, "statuses" => bulk_response["items"].map { |i| i.first[1]['status'] }}
101
+ else
102
+ {"errors" => false}
103
+ end
104
+ end
105
+
106
+ def template_exists?(name)
107
+ @client.indices.get_template(:name => name)
108
+ return true
109
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
110
+ return false
111
+ end
112
+
113
+ def template_put(name, template)
114
+ @client.indices.put_template(:name => name, :body => template)
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,42 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'logstash-output-amazon_es'
4
+ s.version = '0.1.0'
5
+ s.licenses = ['apache-2.0']
6
+ s.summary = "Logstash Output to Amazon Elasticsearch Service"
7
+ s.description = "Output events to Amazon Elasticsearch Service with V4 signing"
8
+ s.authors = ["Amazon"]
9
+ s.email = 'feedback-prod-elasticsearch@amazon.com'
10
+ s.homepage = "http://logstash.net/"
11
+ s.require_paths = ["lib"]
12
+
13
+ # Files
14
+ s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE','NOTICE.TXT']
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
+ s.add_runtime_dependency "aws-sdk", ['>= 2.1.14', '~> 2.1']
29
+ s.add_runtime_dependency "faraday", '~> 0.9.1'
30
+ s.add_runtime_dependency "faraday_middleware", '~> 0.10.0'
31
+
32
+ s.add_development_dependency 'ftw', '~> 0.0.42'
33
+ s.add_development_dependency 'logstash-input-generator'
34
+
35
+ if RUBY_PLATFORM == 'java'
36
+ s.platform = RUBY_PLATFORM
37
+ s.add_runtime_dependency "manticore", '~> 0.4.2'
38
+ end
39
+
40
+ s.add_development_dependency 'logstash-devutils'
41
+ s.add_development_dependency 'longshoreman'
42
+ end
@@ -0,0 +1,69 @@
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-amazon-es-#{rand(999).to_s}"
9
+ CONTAINER_IMAGE = "elasticsearch"
10
+ CONTAINER_TAG = "1.6"
11
+
12
+ DOCKER_INTEGRATION = ENV["DOCKER_INTEGRATION"]
13
+
14
+ module ESHelper
15
+ def get_host
16
+ DOCKER_INTEGRATION ? Longshoreman.new.get_host_ip : "127.0.0.1"
17
+ end
18
+
19
+ def get_port
20
+ return 9200 unless DOCKER_INTEGRATION
21
+
22
+ container = Longshoreman::Container.new
23
+ container.get(CONTAINER_NAME)
24
+ container.rport(9200)
25
+ end
26
+
27
+ def get_client
28
+ Elasticsearch::Client.new(:host => "#{get_host}:#{get_port}")
29
+ end
30
+ end
31
+
32
+
33
+ RSpec.configure do |config|
34
+ config.include ESHelper
35
+
36
+
37
+ if DOCKER_INTEGRATION
38
+ # this :all hook gets run before every describe block that is tagged with :integration => true.
39
+ config.before(:all, :integration => true) do
40
+
41
+
42
+ # check if container exists already before creating new one.
43
+ begin
44
+ ls = Longshoreman::new
45
+ ls.container.get(CONTAINER_NAME)
46
+ rescue Docker::Error::NotFoundError
47
+ Longshoreman.new("#{CONTAINER_IMAGE}:#{CONTAINER_TAG}", CONTAINER_NAME)
48
+ # TODO(talevy): verify ES is running instead of static timeout
49
+ sleep 10
50
+ end
51
+ end
52
+
53
+ # we want to do a final cleanup after all :integration runs,
54
+ # but we don't want to clean up before the last block.
55
+ # This is a final blind check to see if the ES docker container is running and
56
+ # needs to be cleaned up. If no container can be found and/or docker is not
57
+ # running on the system, we do nothing.
58
+ config.after(:suite) do
59
+ # only cleanup docker container if system has docker and the container is running
60
+ begin
61
+ ls = Longshoreman::new
62
+ ls.container.get(CONTAINER_NAME)
63
+ ls.cleanup
64
+ rescue Docker::Error::NotFoundError, Excon::Errors::SocketError
65
+ # do nothing
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,50 @@
1
+ require_relative "../../../spec/amazon_es_spec_helper"
2
+
3
+ describe "outputs/amazon_es" do
4
+ describe "http client create" do
5
+ require "logstash/outputs/amazon_es"
6
+ require "elasticsearch"
7
+
8
+ let(:options) {
9
+ {
10
+ "index" => "my-index",
11
+ "hosts" => "localhost",
12
+ "path" => "some-path"
13
+ }
14
+ }
15
+
16
+ let(:eso) {LogStash::Outputs::AmazonES.new(options)}
17
+
18
+ let(:manticore_host) {
19
+ eso.client.send(:client).transport.options[:hosts].first
20
+ }
21
+
22
+ around(:each) do |block|
23
+ thread = eso.register
24
+ block.call()
25
+ thread.kill()
26
+ end
27
+
28
+ describe "with path" do
29
+ it "should properly create a URI with the path" do
30
+ expect(eso.path).to eql(options["path"])
31
+ end
32
+
33
+
34
+ it "should properly set the path on the HTTP client" do
35
+ expect(manticore_host).to include("/" + options["path"])
36
+ end
37
+
38
+ context "with extra slashes" do
39
+ let(:path) { "/slashed-path/ "}
40
+ let(:eso) {
41
+ LogStash::Outputs::AmazonES.new(options.merge("path" => "/some-path/"))
42
+ }
43
+
44
+ it "should properly set the path on the HTTP client without adding slashes" do
45
+ expect(manticore_host).to include(options["path"])
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end