logstash-output-stackify 1.0.3 → 1.0.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ee3bea3022070998d5caf289643c7b356eeb29751f34109b5bae9520d55aa65
4
- data.tar.gz: 28b8b344a668ad2072516a1f3030d713019aad36d84082aba567e2a33772efea
3
+ metadata.gz: adaf7be7736925de7b4a9d6af06d3a0e83079c22b4b966999560eae67ec62e7d
4
+ data.tar.gz: a7b120bd7de7f818acbcb9452adc4b9d2b9ef33f336a145e547f5dfe362462fa
5
5
  SHA512:
6
- metadata.gz: 8d09b810c70cefde834eec5bc36c88412fc4a354c017a895ff54a0fea5c2baee4701dc3573ae331be1e0c2779813b9b4a4e8eed6be1c87d1267aa13b8f27bdd5
7
- data.tar.gz: c09ab5ca029229d99f4d374c59034a801b98ab09656adcb0e8cdcab6150cb84d8136bb2106de319d313a59d6db654ad06ca2cbc80bc5dd419e773fa4c29d31b4
6
+ metadata.gz: 69d4a6bdca008bb34313dfcb9946c403eae443314d9001c650f9798325c43dff6458c6d679448ea197d30828006f04be73cbad19a84adffb8128464d69ace3a2
7
+ data.tar.gz: e9989d729f6a46c7e344b4d6d0bd60572fb051da8e25064e9dc4582ba7eacc25f4ecb4d8c67dc03403bd9e55a5d529a351ee61f2ee8d93903d0c7bcc0f951bae
@@ -1,15 +1,37 @@
1
1
  # encoding: utf-8
2
+
3
+ # Core classes
2
4
  require 'date'
3
5
  require 'socket'
4
6
  require 'json'
5
7
  require 'uri'
6
- require "net/http"
7
- require "net/https"
8
- require "logstash/outputs/base"
8
+
9
+ # Logstash classes
9
10
  require "logstash/namespace"
11
+ require "logstash/json"
12
+ require "logstash/outputs/base"
13
+ require "logstash/outputs/stackify/http_client"
14
+
10
15
 
11
16
  # Use logstash to ship logs to Stackify
12
17
  class LogStash::Outputs::Stackify < LogStash::Outputs::Base
18
+ include HttpClient
19
+
20
+ CONTENT_TYPE = "application/json".freeze
21
+ MAX_ATTEMPTS = 3
22
+ METHOD = :post.freeze
23
+ RETRYABLE_MANTICORE_EXCEPTIONS = [
24
+ ::Manticore::Timeout,
25
+ ::Manticore::SocketException,
26
+ ::Manticore::ClientProtocolException,
27
+ ::Manticore::ResolutionFailure,
28
+ ::Manticore::SocketTimeout
29
+ ].freeze
30
+ RETRYABLE_CODES = [429, 500, 502, 503, 504].freeze
31
+ URL = "https://api.stackify.com/Log/Save".freeze
32
+
33
+ concurrency :shared
34
+
13
35
  config_name "stackify"
14
36
 
15
37
  # Your Stackify Activation Key
@@ -39,14 +61,27 @@ class LogStash::Outputs::Stackify < LogStash::Outputs::Base
39
61
  public
40
62
 
41
63
  def register
64
+ @headers = {
65
+ "Content-Type" => CONTENT_TYPE,
66
+ "Accept" => CONTENT_TYPE,
67
+ "X-Stackify-PV" => "V1",
68
+ "X-Stackify-Key" => @key.to_s
69
+ }
70
+ @url = URL
71
+ @logger.info("Stackify Plugin Registered")
42
72
  end
43
73
 
44
- # def register
74
+ def multi_receive(events)
75
+ send_events(events)
76
+ end
45
77
 
46
- public
78
+ def close
79
+ http_client.close
80
+ end
47
81
 
48
- def multi_receive(events)
82
+ private
49
83
 
84
+ def send_events(events)
50
85
  begin
51
86
 
52
87
  default_level = 'info'
@@ -109,10 +144,10 @@ class LogStash::Outputs::Stackify < LogStash::Outputs::Base
109
144
  begin
110
145
  timestamp = (event.get('@timestamp').to_f * 1000).to_i
111
146
  rescue StandardError => e
112
- $stdout.write('Error parsing @timestamp: ' + e.to_s)
147
+ @logger.warn('Error parsing @timestamp: ' + e.to_s)
113
148
  end
114
149
  if timestamp.nil?
115
- timestamp = DateTime.now.strftime('%Q')
150
+ timestamp = DateTime.now.strftime('%Q')
116
151
  end
117
152
 
118
153
  # generate key
@@ -140,7 +175,7 @@ class LogStash::Outputs::Stackify < LogStash::Outputs::Base
140
175
  log_msg_groups[log_msg_group_key]['Msgs'].push(log_msg)
141
176
 
142
177
  rescue StandardError => e
143
- $stdout.write(e.to_s)
178
+ @logger.warn(e.to_s)
144
179
  end
145
180
 
146
181
  end
@@ -149,31 +184,96 @@ class LogStash::Outputs::Stackify < LogStash::Outputs::Base
149
184
  log_msg_groups.each do |key, log_msg_group|
150
185
 
151
186
  begin
152
-
153
- data = log_msg_group.to_json
154
- header = {
155
- 'Content-Type' => 'application/json',
156
- 'Accept' => 'application/json',
157
- 'X-Stackify-PV' => 'V1',
158
- 'X-Stackify-Key' => @key.to_s
159
- }
160
-
161
- uri = URI.parse('https://api.stackify.com/Log/Save')
162
- https = Net::HTTP.new(uri.host, uri.port)
163
- https.use_ssl = true
164
- req = Net::HTTP::Post.new(uri.path, initheader = header)
165
- req.body = data
166
- https.request(req)
167
-
187
+ post_log_msg_group(log_msg_group, 0)
168
188
  rescue StandardError => e
169
- $stdout.write(e.to_s)
189
+ @logger.warn(e.to_s)
170
190
  end
171
191
 
172
192
  end
173
193
 
174
194
  rescue StandardError => e
175
- $stdout.write(e.to_s)
195
+ @logger.warn(e.to_s)
196
+ end
197
+
198
+ end
199
+
200
+
201
+ def post_log_msg_group(log_msg_group, attempt)
202
+
203
+ @logger.debug("Stackify Request, Attempt " + attempt.to_s)
204
+
205
+ if attempt > MAX_ATTEMPTS
206
+ @logger.warn(
207
+ "Max attempts exceeded, dropping events",
208
+ :attempt => attempt
209
+ )
210
+ return false
211
+ end
212
+
213
+ response =
214
+ begin
215
+ body = log_msg_group.to_json
216
+ http_client.post(@url, :body => body, :headers => @headers)
217
+ rescue Exception => e
218
+ if retryable_exception?(e)
219
+ @logger.warn(
220
+ "Attempt #{attempt}, retryable exception when making request",
221
+ :attempt => attempt,
222
+ :class => e.class.name,
223
+ :message => e.message,
224
+ :backtrace => e.backtrace
225
+ )
226
+ return post_log_msg_group(log_msg_group, attempt + 1)
227
+ else
228
+ @logger.error(
229
+ "Attempt #{attempt}, fatal exception when making request",
230
+ :attempt => attempt,
231
+ :class => e.class.name,
232
+ :message => e.message,
233
+ :backtrace => e.backtrace
234
+ )
235
+ return false
236
+ end
237
+ end
238
+
239
+ @logger.debug("Stackify Response: " + response.code.to_s)
240
+
241
+ code = response.code
242
+
243
+ if code >= 200 && code <= 299
244
+ true
245
+ elsif RETRYABLE_CODES.include?(code)
246
+ @logger.warn(
247
+ "Bad retryable response from the Stackify API",
248
+ :attempt => attempt,
249
+ :code => code
250
+ )
251
+ sleep_time = sleep_for_attempt(attempt)
252
+ sleep(sleep_time)
253
+ post_log_msg_group(log_msg_group, attempt + 1)
254
+ else
255
+ @logger.error(
256
+ "Bad fatal response from the Stackify API",
257
+ :attempt => attempt,
258
+ :code => code
259
+ )
260
+ false
176
261
  end
177
262
 
178
263
  end
264
+
265
+
266
+ def retryable_exception?(e)
267
+ RETRYABLE_MANTICORE_EXCEPTIONS.any? do |exception_class|
268
+ e.is_a?(exception_class)
269
+ end
270
+ end
271
+
272
+ def sleep_for_attempt(attempt)
273
+ sleep_for = attempt ** 2
274
+ sleep_for = sleep_for <= 60 ? sleep_for : 60
275
+ (sleep_for / 2) + (rand(0..sleep_for) / 2)
276
+ end
277
+
278
+
179
279
  end # class LogStash::Outputs::Stackify
@@ -0,0 +1,131 @@
1
+ # encoding: utf-8
2
+ require "logstash/config/mixin"
3
+ require "manticore"
4
+
5
+ # This contains the HTTP client code within it's own namespace. It is based off
6
+ # of the logstash-mixin-http_client plugin.
7
+ class LogStash::Outputs::Stackify < LogStash::Outputs::Base
8
+ module HttpClient
9
+ class InvalidHTTPConfigError < StandardError; end
10
+
11
+ def self.included(base)
12
+ base.extend(self)
13
+ base.setup_http_client_config
14
+ end
15
+
16
+ def setup_http_client_config
17
+ # Timeout (in seconds) for the entire request
18
+ config :request_timeout, :validate => :number, :default => 60
19
+
20
+ # Timeout (in seconds) to wait for data on the socket. Default is `10s`
21
+ config :socket_timeout, :validate => :number, :default => 10
22
+
23
+ # Timeout (in seconds) to wait for a connection to be established. Default is `10s`
24
+ config :connect_timeout, :validate => :number, :default => 10
25
+
26
+ # Max number of concurrent connections. Defaults to `50`
27
+ config :pool_max, :validate => :number, :default => 50
28
+
29
+ # If you need to use a custom X.509 CA (.pem certs) specify the path to that here
30
+ config :cacert, :validate => :path
31
+
32
+ # If you'd like to use a client certificate (note, most people don't want this) set the path to the x509 cert here
33
+ config :client_cert, :validate => :path
34
+
35
+ # If you're using a client certificate specify the path to the encryption key here
36
+ config :client_key, :validate => :path
37
+
38
+ # If you need to use a custom keystore (`.jks`) specify that here. This does not work with .pem keys!
39
+ config :keystore, :validate => :path
40
+
41
+ # Specify the keystore password here.
42
+ # Note, most .jks files created with keytool require a password!
43
+ config :keystore_password, :validate => :password
44
+
45
+ # Specify the keystore type here. One of `JKS` or `PKCS12`. Default is `JKS`
46
+ config :keystore_type, :validate => :string, :default => "JKS"
47
+
48
+ # If you need to use a custom truststore (`.jks`) specify that here. This does not work with .pem certs!
49
+ config :truststore, :validate => :path
50
+
51
+ # Specify the truststore password here.
52
+ # Note, most .jks files created with keytool require a password!
53
+ config :truststore_password, :validate => :password
54
+
55
+ # Specify the truststore type here. One of `JKS` or `PKCS12`. Default is `JKS`
56
+ config :truststore_type, :validate => :string, :default => "JKS"
57
+
58
+ # If you'd like to use an HTTP proxy . This supports multiple configuration syntaxes:
59
+ #
60
+ # 1. Proxy host in form: `http://proxy.org:1234`
61
+ # 2. Proxy host in form: `{host => "proxy.org", port => 80, scheme => 'http', user => 'username@host', password => 'password'}`
62
+ # 3. Proxy host in form: `{url => 'http://proxy.org:1234', user => 'username@host', password => 'password'}`
63
+ config :proxy
64
+ end
65
+
66
+ def build_http_client_config
67
+ c = {
68
+ connect_timeout: @connect_timeout,
69
+ socket_timeout: @socket_timeout,
70
+ request_timeout: @request_timeout,
71
+ follow_redirects: true,
72
+ automatic_retries: 1,
73
+ retry_non_idempotent: true,
74
+ check_connection_timeout: 200,
75
+ pool_max: @pool_max,
76
+ pool_max_per_route: @pool_max,
77
+ cookies: false,
78
+ keepalive: true
79
+ }
80
+
81
+ if @proxy
82
+ # Symbolize keys if necessary
83
+ c[:proxy] = @proxy.is_a?(Hash) ?
84
+ @proxy.reduce({}) {|memo,(k,v)| memo[k.to_sym] = v; memo} :
85
+ @proxy
86
+ end
87
+
88
+ c[:ssl] = {}
89
+ if @cacert
90
+ c[:ssl][:ca_file] = @cacert
91
+ end
92
+
93
+ if @truststore
94
+ c[:ssl].merge!(
95
+ :truststore => @truststore,
96
+ :truststore_type => @truststore_type,
97
+ :truststore_password => @truststore_password.value
98
+ )
99
+
100
+ if c[:ssl][:truststore_password].nil?
101
+ raise LogStash::ConfigurationError, "Truststore declared without a password! This is not valid, please set the 'truststore_password' option"
102
+ end
103
+ end
104
+
105
+ if @keystore
106
+ c[:ssl].merge!(
107
+ :keystore => @keystore,
108
+ :keystore_type => @keystore_type,
109
+ :keystore_password => @keystore_password.value
110
+ )
111
+
112
+ if c[:ssl][:keystore_password].nil?
113
+ raise LogStash::ConfigurationError, "Keystore declared without a password! This is not valid, please set the 'keystore_password' option"
114
+ end
115
+ end
116
+
117
+ if @client_cert && @client_key
118
+ c[:ssl][:client_cert] = @client_cert
119
+ c[:ssl][:client_key] = @client_key
120
+ elsif !!@client_cert ^ !!@client_key
121
+ raise InvalidHTTPConfigError, "You must specify both client_cert and client_key for an HTTP client, or neither!"
122
+ end
123
+
124
+ c
125
+ end
126
+
127
+ def http_client
128
+ @http_client ||= Manticore::Client.new(build_http_client_config)
129
+ end
130
+ end
131
+ end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'logstash-output-stackify'
3
- s.version = '1.0.3'
3
+ s.version = '1.0.4'
4
4
  s.licenses = ['Apache License (2.0)']
5
5
  s.summary = "Sends logs to Stackify"
6
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"
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
20
20
 
21
21
  # Gem dependencies
22
22
  s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
23
+ s.add_runtime_dependency 'manticore', '>= 0.5.2', '< 1.0.0'
23
24
 
24
25
  s.add_development_dependency 'logstash-devutils'
25
26
  s.add_development_dependency 'logstash-codec-plain'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-output-stackify
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stackify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-05 00:00:00.000000000 Z
11
+ date: 2019-04-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
@@ -30,6 +30,26 @@ dependencies:
30
30
  - - "<="
31
31
  - !ruby/object:Gem::Version
32
32
  version: '2.99'
33
+ - !ruby/object:Gem::Dependency
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: 0.5.2
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: 1.0.0
42
+ name: manticore
43
+ prerelease: false
44
+ type: :runtime
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 0.5.2
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: 1.0.0
33
53
  - !ruby/object:Gem::Dependency
34
54
  requirement: !ruby/object:Gem::Requirement
35
55
  requirements:
@@ -85,6 +105,7 @@ files:
85
105
  - README.md
86
106
  - docs/DEVELOPMENT.md
87
107
  - lib/logstash/outputs/stackify.rb
108
+ - lib/logstash/outputs/stackify/http_client.rb
88
109
  - logstash-output-stackify.gemspec
89
110
  - spec/outputs/stackify_spec.rb
90
111
  homepage: https://www.stackify.com