logstash-output-unomaly 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4a098c44ba976c41ad152a148d265a6a7ece4216307104156ae1ff9f578589cd
4
+ data.tar.gz: b1dc31ddc497b25ae5895aa5407117047f695cc03d1ccc99e4f69f605e3cc9e0
5
+ SHA512:
6
+ metadata.gz: 1b9e317eab598594b691766d46199d12dfe3291baa7e49863762cae50e7a9679693f1d35ed80ce69c4d033908171b9026a19e910c78c818b979ca0d5a7b23cf0
7
+ data.tar.gz: cecec9cc21f9dc4c23663010be493597baeb082c73eecec5e3fcd122bdfac1f679b303e275e0a6659d6c669b3fe5a25a829c7be9517a45e4046b04e32e2b9fbb
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ ## 0.1.3
2
+ - Add option `keep_timestamp` defaulted to true.
3
+ ## 0.1.0
4
+ - First version of the plugin
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ logstash_path = ENV["LOGSTASH_PATH"] || "../../logstash"
6
+ use_logstash_source = ENV["LOGSTASH_SOURCE"] && ENV["LOGSTASH_SOURCE"].to_s == "1"
7
+
8
+ if Dir.exist?(logstash_path) && use_logstash_source
9
+ gem 'logstash-core', :path => "#{logstash_path}/logstash-core"
10
+ gem 'logstash-core-plugin-api', :path => "#{logstash_path}/logstash-core-plugin-api"
11
+ end
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Logstash output plugin for Unomaly
2
+
3
+ [![Build Status](https://travis-ci.org/unomaly/logstash-output-unomaly.svg?branch=master)](https://travis-ci.org/unomaly/logstash-output-unomaly)
4
+
5
+ This plugin sends Logstash events to the [Unomaly](https://www.unomaly.com) ingestion API (min version Unomaly 2.27). The minimal configuration looks like this:
6
+
7
+
8
+ ```
9
+ output {
10
+ unomaly {
11
+ host => "https://your-unomaly-instance:443"
12
+ }
13
+ }
14
+ ```
15
+
16
+ # Important options
17
+
18
+
19
+ | Option | Description | Default |
20
+ |----------------------------|----------------------------------------------------------------------------------|------------|
21
+ | host | Unomaly instance address. Must define full path such as "https://my-instance:443"| No default |
22
+ | message_key | The key in the Logstash event that Unomaly should use for anomaly detection. | "message" |
23
+ | source_key | The event key defining the Unomaly system. | "host" |
24
+ | ssl_certificate_validation | Enable or disable SSL certificate validation | "strict" |
25
+
26
+ See the [source code](lib/logstash/outputs/unomaly.rb) for the full list of options
27
+
28
+
29
+ ## Known issues
30
+ - Installation of the plugin fails on Logstash 6.2.1.
31
+
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome. This project is intended to
36
+ be a safe, welcoming space for collaboration.
37
+
38
+ ## Development
39
+
40
+ We use docker to build the plugin. You can build it by running `docker-compose run jruby gem build logstash-output-unomaly.gemspec `
@@ -0,0 +1,333 @@
1
+ # encoding: utf-8
2
+ require "logstash/outputs/base"
3
+ require "logstash/namespace"
4
+ require "logstash/json"
5
+ require 'uri'
6
+ require 'json'
7
+ require 'manticore'
8
+
9
+ # An example output that does nothing.
10
+ class LogStash::Outputs::Unomaly < LogStash::Outputs::Base
11
+ class InvalidHTTPConfigError < StandardError; end
12
+
13
+ concurrency :shared
14
+ config_name "unomaly"
15
+
16
+ # Event batch size to send to Unomaly. Increasing the batch size can increase throughput by reducing HTTP overhead
17
+ config :batch_size, :validate => :number, :default => 50
18
+
19
+ # Unomaly host to send the logs to
20
+ config :host, :validate => :string, :required => true
21
+
22
+ # Key that will be used by Unomaly as the log message
23
+ config :message_key, :validate => :string, :default => "message"
24
+
25
+ # Key that will be used by Unomaly as the system key
26
+ config :source_key, :validate => :string, :default => "host"
27
+
28
+ # Unomaly api path to push events
29
+ config :api_path, :validate => :string, :default => "/v1/batch"
30
+
31
+ # Keep logstash timestamp
32
+ config :keep_timestamp, :validate => :boolean, :default => true
33
+
34
+ # Display debug logs
35
+ config :debug, :validate => :boolean, :default => false
36
+
37
+ # Timeout (in seconds) for the entire request
38
+ config :request_timeout, :validate => :number, :default => 60
39
+
40
+ # Timeout (in seconds) to wait for data on the socket. Default is `10s`
41
+ config :socket_timeout, :validate => :number, :default => 10
42
+
43
+ # Timeout (in seconds) to wait for a connection to be established. Default is `10s`
44
+ config :connect_timeout, :validate => :number, :default => 10
45
+
46
+ # Should redirects be followed? Defaults to `true`
47
+ config :follow_redirects, :validate => :boolean, :default => true
48
+
49
+ # Max number of concurrent connections. Defaults to `50`
50
+ config :pool_max, :validate => :number, :default => 50
51
+
52
+ # Max number of concurrent connections to a single host. Defaults to `25`
53
+ config :pool_max_per_route, :validate => :number, :default => 25
54
+
55
+ # Turn this on to enable HTTP keepalive support. We highly recommend setting `automatic_retries` to at least
56
+ # one with this to fix interactions with broken keepalive implementations.
57
+ config :keepalive, :validate => :boolean, :default => true
58
+
59
+ # How many times should the client retry a failing URL. We highly recommend NOT setting this value
60
+ # to zero if keepalive is enabled. Some servers incorrectly end keepalives early requiring a retry!
61
+ # Note: if `retry_non_idempotent` is set only GET, HEAD, PUT, DELETE, OPTIONS, and TRACE requests will be retried.
62
+ config :automatic_retries, :validate => :number, :default => 1
63
+
64
+ # If `automatic_retries` is enabled this will cause non-idempotent HTTP verbs (such as POST) to be retried.
65
+ config :retry_non_idempotent, :validate => :boolean, :default => false
66
+
67
+ # How long to wait before checking if the connection is stale before executing a request on a connection using keepalive.
68
+ # # You may want to set this lower, possibly to 0 if you get connection errors regularly
69
+ # Quoting the Apache commons docs (this client is based Apache Commmons):
70
+ # 'Defines period of inactivity in milliseconds after which persistent connections must be re-validated prior to being leased to the consumer. Non-positive value passed to this method disables connection validation. This check helps detect connections that have become stale (half-closed) while kept inactive in the pool.'
71
+ # See https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/PoolingHttpClientConnectionManager.html#setValidateAfterInactivity(int)[these docs for more info]
72
+ config :validate_after_inactivity, :validate => :number, :default => 200
73
+
74
+ # refer to https://github.com/cheald/manticore/blob/6764e2d3fb67a1ef244cb4610d5b74ba1dd6694c/lib/manticore/client.rb#L161
75
+ # instead of using symbol, use string "disable" or "false", "browser", "strict"
76
+ config :ssl_certificate_validation, :validate => :string, :default => "strict"
77
+
78
+ # If you need to use a custom X.509 CA (.pem certs) specify the path to that here
79
+ config :cacert, :validate => :path
80
+
81
+ # If you'd like to use a client certificate (note, most people don't want this) set the path to the x509 cert here
82
+ config :client_cert, :validate => :path
83
+ # If you're using a client certificate specify the path to the encryption key here
84
+ config :client_key, :validate => :path
85
+
86
+ # If you need to use a custom keystore (`.jks`) specify that here. This does not work with .pem keys!
87
+ config :keystore, :validate => :path
88
+
89
+ # Specify the keystore password here.
90
+ # Note, most .jks files created with keytool require a password!
91
+ config :keystore_password, :validate => :password
92
+
93
+ # Specify the keystore type here. One of `JKS` or `PKCS12`. Default is `JKS`
94
+ config :keystore_type, :validate => :string, :default => "JKS"
95
+
96
+ # If you need to use a custom truststore (`.jks`) specify that here. This does not work with .pem certs!
97
+ config :truststore, :validate => :path
98
+
99
+ # Specify the truststore password here.
100
+ # Note, most .jks files created with keytool require a password!
101
+ config :truststore_password, :validate => :password
102
+
103
+ # Specify the truststore type here. One of `JKS` or `PKCS12`. Default is `JKS`
104
+ config :truststore_type, :validate => :string, :default => "JKS"
105
+
106
+ # Enable cookie support. With this enabled the client will persist cookies
107
+ # across requests as a normal web browser would. Enabled by default
108
+ config :cookies, :validate => :boolean, :default => true
109
+
110
+ # If you'd like to use an HTTP proxy . This supports multiple configuration syntaxes:
111
+ #
112
+ # 1. Proxy host in form: `http://proxy.org:1234`
113
+ # 2. Proxy host in form: `{host => "proxy.org", port => 80, scheme => 'http', user => 'username@host', password => 'password'}`
114
+ # 3. Proxy host in form: `{url => 'http://proxy.org:1234', user => 'username@host', password => 'password'}`
115
+ config :proxy
116
+
117
+ # Username to use for HTTP auth.
118
+ config :user, :validate => :string
119
+
120
+ # Password to use for HTTP auth
121
+ config :password, :validate => :password
122
+
123
+ public
124
+ def register
125
+ @total = 0
126
+ @total_failed = 0
127
+ logger.info("Initialized Unomaly output plugin with configuration",
128
+ :host => @host,
129
+ :accept_self_signed_cert => @accept_self_signed_cert)
130
+
131
+ end # def register
132
+
133
+ def client_config
134
+ c = {
135
+ connect_timeout: @connect_timeout,
136
+ socket_timeout: @socket_timeout,
137
+ request_timeout: @request_timeout,
138
+ follow_redirects: @follow_redirects,
139
+ automatic_retries: @automatic_retries,
140
+ retry_non_idempotent: @retry_non_idempotent,
141
+ check_connection_timeout: @validate_after_inactivity,
142
+ pool_max: @pool_max,
143
+ pool_max_per_route: @pool_max_per_route,
144
+ cookies: @cookies,
145
+ keepalive: @keepalive
146
+ }
147
+
148
+ if @proxy
149
+ # Symbolize keys if necessary
150
+ c[:proxy] = @proxy.is_a?(Hash) ?
151
+ @proxy.reduce({}) {|memo,(k,v)| memo[k.to_sym] = v; memo} :
152
+ @proxy
153
+ end
154
+
155
+ if @user
156
+ if !@password || !@password.value
157
+ raise ::LogStash::ConfigurationError, "User '#{@user}' specified without password!"
158
+ end
159
+
160
+ # Symbolize keys if necessary
161
+ c[:auth] = {
162
+ :user => @user,
163
+ :password => @password.value,
164
+ :eager => true
165
+ }
166
+ end
167
+
168
+ c[:ssl] = {}
169
+ if @ssl_certificate_validation == "disable" || @ssl_certificate_validation == "false"
170
+ c[:ssl][:verify] = :disable
171
+ elsif @ssl_certificate_validation == "browser"
172
+ c[:ssl][:verify] = :browser
173
+ end
174
+
175
+ if @cacert
176
+ c[:ssl][:ca_file] = @cacert
177
+ end
178
+
179
+ if @truststore
180
+ c[:ssl].merge!(
181
+ :truststore => @truststore,
182
+ :truststore_type => @truststore_type,
183
+ :truststore_password => @truststore_password.value
184
+ )
185
+
186
+ if c[:ssl][:truststore_password].nil?
187
+ raise LogStash::ConfigurationError, "Truststore declared without a password! This is not valid, please set the 'truststore_password' option"
188
+ end
189
+ end
190
+
191
+ if @keystore
192
+ c[:ssl].merge!(
193
+ :keystore => @keystore,
194
+ :keystore_type => @keystore_type,
195
+ :keystore_password => @keystore_password.value
196
+ )
197
+
198
+ if c[:ssl][:keystore_password].nil?
199
+ raise LogStash::ConfigurationError, "Keystore declared without a password! This is not valid, please set the 'keystore_password' option"
200
+ end
201
+ end
202
+
203
+ if @client_cert && @client_key
204
+ c[:ssl][:client_cert] = @client_cert
205
+ c[:ssl][:client_key] = @client_key
206
+ elsif !!@client_cert ^ !!@client_key
207
+ raise InvalidHTTPConfigError, "You must specify both client_cert and client_key for an HTTP client, or neither!"
208
+ end
209
+
210
+ c
211
+ end
212
+
213
+ private
214
+ def make_client
215
+ puts client_config
216
+ Manticore::Client.new(client_config)
217
+ end
218
+
219
+ public
220
+ def client
221
+ @client ||= make_client
222
+ end
223
+
224
+ public
225
+ def close
226
+ @client.close
227
+ end
228
+
229
+ def flatten(data, prefix)
230
+ ret = {}
231
+ if data.is_a? Hash
232
+ data.each { |key, value|
233
+ if prefix.to_s.empty?
234
+ ret.merge! flatten(value, "#{key.to_s}")
235
+ else
236
+ ret.merge! flatten(value, "#{prefix}__#{key.to_s}")
237
+ end
238
+ }
239
+ elsif data.is_a? Array
240
+ data.each_with_index {|val,index | ret.merge! flatten(val, "#{prefix}__#{index}")}
241
+ else
242
+ return {prefix => data.to_s}
243
+ end
244
+
245
+ ret
246
+ end
247
+
248
+ def send_batch(events)
249
+ url = @host + @api_path
250
+ body = events.to_json
251
+
252
+ request = client.post(url, {
253
+ :body => body,
254
+ :headers => {"Content-Type"=>"application/json"}
255
+ })
256
+
257
+ request.on_success do |response|
258
+ if response.code == 200
259
+ @logger.debug("Successfully sent ",
260
+ :response_code => response.code,
261
+ :total => @total,
262
+ :time => Time::now.utc)
263
+ else
264
+ @total_failed += 1
265
+ log_failure(
266
+ "Encountered non-200 HTTP code #{response.code}",
267
+ :response_code => response.code,
268
+ :url => url,
269
+ :response_body => response.body,
270
+ :total_failed => @total_failed)
271
+ end
272
+ end
273
+
274
+ request.on_failure do |exception|
275
+ @total_failed += 1
276
+ log_failure("Could not access URL",
277
+ :url => url,
278
+ :method => @http_method,
279
+ :body => body,
280
+ :message => exception.message,
281
+ :class => exception.class.name,
282
+ :backtrace => exception.backtrace,
283
+ :total_failed => @total_failed
284
+ )
285
+ end
286
+
287
+ @logger.debug("Sending Unomaly Event",
288
+ :total => @total,
289
+ :time => Time::now.utc)
290
+ request.call
291
+
292
+ rescue Exception => e
293
+ @logger.error("[Exception=] #{e.message} #{e.backtrace}")
294
+ end
295
+
296
+
297
+ public
298
+ def multi_receive(events)
299
+ if debug
300
+ puts events.to_json
301
+ end
302
+
303
+ events.each_slice(@batch_size) do |chunk|
304
+ documents = []
305
+ chunk.each do |event|
306
+ unomaly_event = {
307
+ message: event.get(@message_key),
308
+ source: event.get(@source_key),
309
+ }
310
+
311
+ metadata = event.to_hash
312
+
313
+ if @keep_timestamp
314
+ unomaly_event["timestamp"] = event.get("@timestamp")
315
+ metadata.delete("@timestamp")
316
+ end
317
+
318
+ metadata.delete(@source_key)
319
+ metadata.delete(@message_key)
320
+
321
+ unomaly_event["metadata"]=flatten(metadata,"")
322
+
323
+
324
+ documents.push(unomaly_event)
325
+ end
326
+ send_batch(documents)
327
+ end
328
+ end
329
+
330
+ def log_failure(message, opts)
331
+ @logger.error("[HTTP Output Failure] #{message}", opts)
332
+ end
333
+ end # class LogStash::Outputs::Unomaly
@@ -0,0 +1,28 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'logstash-output-unomaly'
3
+ s.version = '0.1.3'
4
+ s.licenses = ['Apache License (2.0)']
5
+ s.summary = "Logstash output plugin for Unomaly"
6
+ s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
7
+ s.authors = ["Unomaly"]
8
+ s.email = "support@unomaly.com"
9
+ s.homepage = "https://unomaly.com"
10
+ s.require_paths = ["lib"]
11
+
12
+ # Files
13
+ s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','Gemfile']
14
+ # Tests
15
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
16
+
17
+ # Special flag to let us know this is actually a logstash plugin
18
+ s.metadata = { "logstash_plugin" => "true", "logstash_group" => "output" }
19
+
20
+ # Gem dependencies
21
+ #
22
+
23
+ s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
24
+ s.add_runtime_dependency "logstash-codec-plain"
25
+ s.add_runtime_dependency 'manticore', '>= 0.5.2', '< 1.0.0'
26
+
27
+ s.add_development_dependency 'logstash-devutils'
28
+ end
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+ require "logstash/outputs/unomaly"
4
+ require "logstash/event"
5
+
6
+ describe LogStash::Outputs::Unomaly do
7
+ let(:sample_event) { LogStash::Event.new("message" => "hello this is log") }
8
+ let(:client) { @unomaly.client }
9
+
10
+ before do
11
+ @unomaly = LogStash::Outputs::Unomaly.new("host" => "localhost", "batch_size" => 3)
12
+ @unomaly.register
13
+ allow(@unomaly).to receive(:client).and_return(client)
14
+ allow(client).to receive(:post).and_call_original
15
+ end
16
+
17
+ before do
18
+ allow(@unomaly).to receive(:client).and_return(client)
19
+ end
20
+
21
+
22
+ it "Forwards an event" do
23
+ expect(client).to receive(:post).once.and_call_original
24
+ @unomaly.multi_receive([sample_event])
25
+ end
26
+
27
+ it "Batches multiple events and extracts metadata" do
28
+ event1 = LogStash::Event.new("message" => "hello this is log 1", "host" => "host1")
29
+ event2 = LogStash::Event.new("message" => "hello this is log 2", "host" => "host2")
30
+ event3 = LogStash::Event.new("message" => "hello this is log 3", "host" => "host3")
31
+ expect(client).to receive(:post).once.with("localhost/v1/batch",hash_including(:body => LogStash::Json.dump(
32
+ [{"message" => "hello this is log 1", "source" => "host1", "timestamp" => event1.timestamp.to_s, "metadata" => {"@version" => "1"}},
33
+ {"message" => "hello this is log 2", "source" => "host2", "timestamp" => event2.timestamp.to_s, "metadata" => {"@version" => "1"}},
34
+ {"message" => "hello this is log 3", "source" => "host3", "timestamp" => event3.timestamp.to_s, "metadata" => {"@version" => "1"}}
35
+ ]
36
+ ))).and_call_original
37
+ @unomaly.multi_receive([event1, event2, event3])
38
+ end
39
+
40
+ it "Batches data of size batch_size" do
41
+ expect(client).to receive(:post).exactly(2).times.and_call_original
42
+ @unomaly.multi_receive([sample_event, sample_event, sample_event, sample_event])
43
+ end
44
+
45
+
46
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-output-unomaly
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Unomaly
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-06-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logstash-core-plugin-api
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.60'
20
+ - - "<="
21
+ - !ruby/object:Gem::Version
22
+ version: '2.99'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.60'
30
+ - - "<="
31
+ - !ruby/object:Gem::Version
32
+ version: '2.99'
33
+ - !ruby/object:Gem::Dependency
34
+ name: logstash-codec-plain
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: manticore
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 0.5.2
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: 1.0.0
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 0.5.2
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: 1.0.0
67
+ - !ruby/object:Gem::Dependency
68
+ name: logstash-devutils
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ description: This gem is a Logstash plugin required to be installed on top of the
82
+ Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This
83
+ gem is not a stand-alone program
84
+ email: support@unomaly.com
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - CHANGELOG.md
90
+ - Gemfile
91
+ - README.md
92
+ - lib/logstash/outputs/unomaly.rb
93
+ - logstash-output-unomaly.gemspec
94
+ - spec/outputs/unomaly_spec.rb
95
+ homepage: https://unomaly.com
96
+ licenses:
97
+ - Apache License (2.0)
98
+ metadata:
99
+ logstash_plugin: 'true'
100
+ logstash_group: output
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.7.7
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: Logstash output plugin for Unomaly
121
+ test_files:
122
+ - spec/outputs/unomaly_spec.rb