logstash-output-stackify 1.0.3 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/logstash/outputs/stackify.rb +127 -27
- data/lib/logstash/outputs/stackify/http_client.rb +131 -0
- data/logstash-output-stackify.gemspec +2 -1
- metadata +23 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: adaf7be7736925de7b4a9d6af06d3a0e83079c22b4b966999560eae67ec62e7d
|
4
|
+
data.tar.gz: a7b120bd7de7f818acbcb9452adc4b9d2b9ef33f336a145e547f5dfe362462fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
7
|
-
|
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
|
-
|
74
|
+
def multi_receive(events)
|
75
|
+
send_events(events)
|
76
|
+
end
|
45
77
|
|
46
|
-
|
78
|
+
def close
|
79
|
+
http_client.close
|
80
|
+
end
|
47
81
|
|
48
|
-
|
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
|
-
|
147
|
+
@logger.warn('Error parsing @timestamp: ' + e.to_s)
|
113
148
|
end
|
114
149
|
if timestamp.nil?
|
115
|
-
|
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
|
-
|
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
|
-
|
189
|
+
@logger.warn(e.to_s)
|
170
190
|
end
|
171
191
|
|
172
192
|
end
|
173
193
|
|
174
194
|
rescue StandardError => e
|
175
|
-
|
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
|
+
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.
|
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-
|
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
|