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