logstash-output-dynatrace 0.4.0 → 0.5.0.rc1
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/Gemfile +1 -1
- data/README.md +2 -6
- data/{version.rb → lib/dynatrace/version.rb} +4 -3
- data/lib/logstash/outputs/dynatrace.rb +228 -67
- data/logstash-output-dynatrace.gemspec +8 -4
- data/spec/outputs/dynatrace_spec.rb +433 -90
- data/spec/spec_helper.rb +141 -0
- data/version.yaml +1 -0
- metadata +54 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f19d849000371b66a0e843352045a31bef644981f413bd81b22553bc9775ad3f
|
4
|
+
data.tar.gz: 2086305ff6250a75137771c2999c040f33d880156f3afd6e96534eefe416fa76
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc6a3d695ffb5ed72f9313b262275b2b002dcf60f4659afb42dac8d8a13c402b0a314f85dc7e6cb15a7ee0421697d35d3675bf002404324665dcdb353370d7db
|
7
|
+
data.tar.gz: 2324e4a662915858622b80fb8adbaba7613a6015f59f60092ec51f0225092cbaa51cb6c38d3800e245404f5541d9bd562eddfbd33b80e5e578f237e2d555dd40
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -103,12 +103,8 @@ It is recommended to leave this optional configuration set to `false` unless abs
|
|
103
103
|
Setting `ssl_verify_none` to `true` causes the output plugin to skip certificate verification when sending log ingest requests to SSL and TLS protected HTTPS endpoints.
|
104
104
|
This option may be required if you are using a self-signed certificate, an expired certificate, or a certificate which was generated for a different domain than the one in use.
|
105
105
|
|
106
|
-
|
107
|
-
|
108
|
-
* Value type is codec
|
109
|
-
* Default value is "plain"
|
110
|
-
|
111
|
-
The codec used for output data. Output codecs are a convenient method for encoding your data before it leaves the output without needing a separate filter in your Logstash pipeline.
|
106
|
+
> NOTE: Starting in plugin version `0.5.0`, this option has no effect in versions of Logstash older than `8.1.0`.
|
107
|
+
> If this functionality is required, it is recommended to update Logstash or stay at plugin version `0.4.x` or older.
|
112
108
|
|
113
109
|
### `enable_metric`
|
114
110
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright
|
3
|
+
# Copyright 2023 Dynatrace LLC
|
4
4
|
#
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
6
|
# you may not use this file except in compliance with the License.
|
@@ -15,6 +15,7 @@
|
|
15
15
|
# limitations under the License.
|
16
16
|
|
17
17
|
module DynatraceConstants
|
18
|
-
|
19
|
-
VERSION =
|
18
|
+
require 'yaml'
|
19
|
+
VERSION = YAML.load_file(File.expand_path('../../version.yaml',
|
20
|
+
File.dirname(__FILE__))).fetch('logstash-output-dynatrace')
|
20
21
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright
|
3
|
+
# Copyright 2023 Dynatrace LLC
|
4
4
|
#
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
6
|
# you may not use this file except in compliance with the License.
|
@@ -14,24 +14,48 @@
|
|
14
14
|
# See the License for the specific language governing permissions and
|
15
15
|
# limitations under the License.
|
16
16
|
|
17
|
-
require 'logstash/namespace'
|
18
17
|
require 'logstash/outputs/base'
|
18
|
+
require 'logstash/namespace'
|
19
19
|
require 'logstash/json'
|
20
|
-
require '
|
20
|
+
require 'logstash/version'
|
21
|
+
require 'dynatrace/version'
|
22
|
+
require 'uri'
|
23
|
+
require 'logstash/plugin_mixins/http_client'
|
21
24
|
|
22
|
-
|
23
|
-
|
25
|
+
# These constants came from the http plugin config but we don't want them configurable
|
26
|
+
# If encountered as response codes this plugin will retry these requests
|
27
|
+
RETRYABLE_CODES = [429, 500, 502, 503, 504].freeze
|
28
|
+
RETRY_FAILED = true
|
24
29
|
|
25
30
|
module LogStash
|
26
31
|
module Outputs
|
27
|
-
class RetryableError < StandardError;
|
28
|
-
end
|
29
|
-
|
30
|
-
# An output which sends logs to the Dynatrace log ingest v2 endpoint formatted as JSON
|
31
32
|
class Dynatrace < LogStash::Outputs::Base
|
32
|
-
|
33
|
+
include LogStash::PluginMixins::HttpClient
|
34
|
+
|
35
|
+
concurrency :shared
|
36
|
+
|
37
|
+
RETRYABLE_MANTICORE_EXCEPTIONS = [
|
38
|
+
::Manticore::Timeout,
|
39
|
+
::Manticore::SocketException,
|
40
|
+
::Manticore::ClientProtocolException,
|
41
|
+
::Manticore::ResolutionFailure,
|
42
|
+
::Manticore::SocketTimeout
|
43
|
+
].freeze
|
44
|
+
|
45
|
+
RETRYABLE_UNKNOWN_EXCEPTION_STRINGS = [
|
46
|
+
/Connection reset by peer/i,
|
47
|
+
/Read Timed out/i
|
48
|
+
].freeze
|
33
49
|
|
34
|
-
|
50
|
+
class PluginInternalQueueLeftoverError < StandardError; end
|
51
|
+
|
52
|
+
# This output will execute up to 'pool_max' requests in parallel for performance.
|
53
|
+
# Consider this when tuning this plugin for performance.
|
54
|
+
#
|
55
|
+
# Additionally, note that when parallel execution is used strict ordering of events is not
|
56
|
+
# guaranteed!
|
57
|
+
|
58
|
+
config_name 'dynatrace'
|
35
59
|
|
36
60
|
# The full URL of the Dynatrace log ingestion endpoint:
|
37
61
|
# - on SaaS: https://{your-environment-id}.live.dynatrace.com/api/v2/logs/ingest
|
@@ -44,80 +68,217 @@ module LogStash
|
|
44
68
|
# Disable SSL validation by setting :verify_mode OpenSSL::SSL::VERIFY_NONE
|
45
69
|
config :ssl_verify_none, validate: :boolean, default: false
|
46
70
|
|
47
|
-
|
71
|
+
# Include headers in debug logs when HTTP errors occur. Headers include sensitive data such as API tokens.
|
72
|
+
config :debug_include_headers, validate: :boolean, default: false
|
48
73
|
|
49
|
-
|
74
|
+
# Include body in debug logs when HTTP errors occur. Body may be large and include sensitive data.
|
75
|
+
config :debug_include_body, validate: :boolean, default: false
|
50
76
|
|
51
77
|
def register
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
@
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
78
|
+
# ssl_verification_mode config is from mixin but ssl_verify_none is our documented config
|
79
|
+
@ssl_verification_mode = 'none' if @ssl_verify_none
|
80
|
+
|
81
|
+
@ingest_endpoint_url = @ingest_endpoint_url.to_s
|
82
|
+
|
83
|
+
# Run named Timer as daemon thread
|
84
|
+
@timer = java.util.Timer.new("HTTP Output #{params['id']}", true)
|
85
|
+
end
|
86
|
+
|
87
|
+
def multi_receive(events)
|
88
|
+
return if events.empty?
|
89
|
+
|
90
|
+
send_events(events)
|
91
|
+
end
|
92
|
+
|
93
|
+
class RetryTimerTask < java.util.TimerTask
|
94
|
+
def initialize(pending, event, attempt)
|
95
|
+
@pending = pending
|
96
|
+
@event = event
|
97
|
+
@attempt = attempt
|
98
|
+
super()
|
99
|
+
end
|
100
|
+
|
101
|
+
def run
|
102
|
+
@pending << [@event, @attempt]
|
61
103
|
end
|
62
|
-
@logger.info('Client', client: @client.inspect)
|
63
104
|
end
|
64
105
|
|
65
|
-
def
|
106
|
+
def make_headers
|
66
107
|
{
|
67
|
-
'User-Agent' => "logstash-output-dynatrace/#{
|
108
|
+
'User-Agent' => "logstash-output-dynatrace/#{DynatraceConstants::VERSION} logstash/#{LOGSTASH_VERSION}",
|
68
109
|
'Content-Type' => 'application/json; charset=utf-8',
|
69
110
|
'Authorization' => "Api-Token #{@api_key.value}"
|
70
111
|
}
|
71
112
|
end
|
72
113
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
114
|
+
def log_retryable_response(response)
|
115
|
+
retry_msg = RETRY_FAILED ? 'will retry' : "won't retry"
|
116
|
+
if response.code == 429
|
117
|
+
@logger.debug? && @logger.debug("Encountered a 429 response, #{retry_msg}. This is not serious, just flow control via HTTP")
|
118
|
+
else
|
119
|
+
@logger.warn("Encountered a retryable HTTP request in HTTP output, #{retry_msg}", code: response.code,
|
120
|
+
body: response.body)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def log_error_response(response, ingest_endpoint_url, event)
|
125
|
+
log_failure(
|
126
|
+
"Encountered non-2xx HTTP code #{response.code}",
|
127
|
+
response_code: response.code,
|
128
|
+
ingest_endpoint_url: ingest_endpoint_url,
|
129
|
+
event: event
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
133
|
+
def send_events(events)
|
134
|
+
successes = java.util.concurrent.atomic.AtomicInteger.new(0)
|
135
|
+
failures = java.util.concurrent.atomic.AtomicInteger.new(0)
|
136
|
+
|
137
|
+
pending = Queue.new
|
138
|
+
pending << [events, 0]
|
139
|
+
|
140
|
+
while popped = pending.pop
|
141
|
+
break if popped == :done
|
142
|
+
|
143
|
+
event, attempt = popped
|
144
|
+
|
145
|
+
if attempt > 2 && pipeline_shutdown_requested?
|
146
|
+
raise PluginInternalQueueLeftoverError, 'Received pipeline shutdown request but http output has unfinished events. ' \
|
147
|
+
'If persistent queue is enabled, events will be retried.'
|
148
|
+
end
|
149
|
+
|
150
|
+
action, event, attempt = send_event(event, attempt)
|
151
|
+
begin
|
152
|
+
action = :failure if action == :retry && !RETRY_FAILED
|
153
|
+
|
154
|
+
case action
|
155
|
+
when :success
|
156
|
+
successes.incrementAndGet
|
157
|
+
when :retry
|
158
|
+
next_attempt = attempt + 1
|
159
|
+
sleep_for = sleep_for_attempt(next_attempt)
|
160
|
+
@logger.info("Retrying http request, will sleep for #{sleep_for} seconds")
|
161
|
+
timer_task = RetryTimerTask.new(pending, event, next_attempt)
|
162
|
+
@timer.schedule(timer_task, sleep_for * 1000)
|
163
|
+
when :failure
|
164
|
+
failures.incrementAndGet
|
165
|
+
else
|
166
|
+
# this should never happen. It means send_event returned a symbol we didn't recognize
|
167
|
+
raise "Unknown action #{action}"
|
168
|
+
end
|
169
|
+
|
170
|
+
pending << :done if %i[success failure].include?(action) && (successes.get + failures.get == 1)
|
171
|
+
rescue StandardError => e
|
172
|
+
# This should never happen unless there's a flat out bug in the code
|
173
|
+
@logger.error('Error sending HTTP Request',
|
174
|
+
class: e.class.name,
|
175
|
+
message: e.message,
|
176
|
+
backtrace: e.backtrace)
|
177
|
+
failures.incrementAndGet
|
178
|
+
raise e
|
95
179
|
end
|
180
|
+
end
|
181
|
+
rescue StandardError => e
|
182
|
+
@logger.error('Error in http output loop',
|
183
|
+
class: e.class.name,
|
184
|
+
message: e.message,
|
185
|
+
backtrace: e.backtrace)
|
186
|
+
raise e
|
187
|
+
end
|
188
|
+
|
189
|
+
def pipeline_shutdown_requested?
|
190
|
+
return super if defined?(super) # since LS 8.1.0
|
191
|
+
|
192
|
+
nil
|
193
|
+
end
|
96
194
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
195
|
+
def sleep_for_attempt(attempt)
|
196
|
+
sleep_for = attempt**2
|
197
|
+
sleep_for = sleep_for <= 60 ? sleep_for : 60
|
198
|
+
(sleep_for / 2) + (rand(0..sleep_for) / 2)
|
199
|
+
end
|
200
|
+
|
201
|
+
def send_event(event, attempt)
|
202
|
+
body = event_body(event)
|
203
|
+
headers = make_headers
|
204
|
+
|
205
|
+
# Create an async request
|
206
|
+
response = client.post(ingest_endpoint_url, body: body, headers: headers)
|
207
|
+
|
208
|
+
if response_success?(response)
|
209
|
+
[:success, event, attempt]
|
210
|
+
elsif retryable_response?(response)
|
211
|
+
log_retryable_response(response)
|
212
|
+
[:retry, event, attempt]
|
213
|
+
else
|
214
|
+
log_error_response(response, ingest_endpoint_url, event)
|
215
|
+
[:failure, event, attempt]
|
216
|
+
end
|
217
|
+
rescue StandardError => e
|
218
|
+
will_retry = retryable_exception?(e)
|
219
|
+
log_params = {
|
220
|
+
ingest_endpoint_url: ingest_endpoint_url,
|
221
|
+
message: e.message,
|
222
|
+
class: e.class,
|
223
|
+
will_retry: will_retry
|
224
|
+
}
|
225
|
+
if @logger.debug?
|
226
|
+
# backtraces are big
|
227
|
+
log_params[:backtrace] = e.backtrace
|
228
|
+
if @debug_include_headers
|
229
|
+
# headers can have sensitive data
|
230
|
+
log_params[:headers] = headers
|
112
231
|
end
|
113
|
-
|
114
|
-
|
115
|
-
|
232
|
+
if @debug_include_body
|
233
|
+
# body can be big and may have sensitive data
|
234
|
+
log_params[:body] = body
|
235
|
+
end
|
236
|
+
end
|
237
|
+
log_failure('Could not fetch URL', log_params)
|
238
|
+
|
239
|
+
if will_retry
|
240
|
+
[:retry, event, attempt]
|
241
|
+
else
|
242
|
+
[:failure, event, attempt]
|
116
243
|
end
|
117
244
|
end
|
118
245
|
|
119
|
-
def
|
120
|
-
|
246
|
+
def close
|
247
|
+
@timer.cancel
|
248
|
+
client.close
|
249
|
+
end
|
250
|
+
|
251
|
+
private
|
252
|
+
|
253
|
+
def response_success?(response)
|
254
|
+
response.code >= 200 && response.code <= 299
|
255
|
+
end
|
256
|
+
|
257
|
+
def retryable_response?(response)
|
258
|
+
RETRYABLE_CODES.include?(response.code)
|
259
|
+
end
|
260
|
+
|
261
|
+
def retryable_exception?(exception)
|
262
|
+
retryable_manticore_exception?(exception) || retryable_unknown_exception?(exception)
|
263
|
+
end
|
264
|
+
|
265
|
+
def retryable_manticore_exception?(exception)
|
266
|
+
RETRYABLE_MANTICORE_EXCEPTIONS.any? { |me| exception.is_a?(me) }
|
267
|
+
end
|
268
|
+
|
269
|
+
def retryable_unknown_exception?(exception)
|
270
|
+
exception.is_a?(::Manticore::UnknownException) &&
|
271
|
+
RETRYABLE_UNKNOWN_EXCEPTION_STRINGS.any? { |snippet| exception.message =~ snippet }
|
272
|
+
end
|
273
|
+
|
274
|
+
# This is split into a separate method mostly to help testing
|
275
|
+
def log_failure(message, opts)
|
276
|
+
@logger.error(message, opts)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Format the HTTP body
|
280
|
+
def event_body(event)
|
281
|
+
"#{LogStash::Json.dump(event.map(&:to_hash)).chomp}\n"
|
121
282
|
end
|
122
283
|
end
|
123
284
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright
|
3
|
+
# Copyright 2023 Dynatrace LLC
|
4
4
|
#
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
6
|
# you may not use this file except in compliance with the License.
|
@@ -14,11 +14,12 @@
|
|
14
14
|
# See the License for the specific language governing permissions and
|
15
15
|
# limitations under the License.
|
16
16
|
|
17
|
-
|
17
|
+
require 'yaml'
|
18
18
|
|
19
19
|
Gem::Specification.new do |s|
|
20
20
|
s.name = 'logstash-output-dynatrace'
|
21
|
-
s.version =
|
21
|
+
s.version = YAML.load_file(File.expand_path('./version.yaml',
|
22
|
+
File.dirname(__FILE__))).fetch('logstash-output-dynatrace')
|
22
23
|
s.summary = 'A logstash output plugin for sending logs to the Dynatrace Generic log ingest API v2'
|
23
24
|
s.description = <<-EOF
|
24
25
|
This gem is a Logstash plugin required to be installed on top of the Logstash
|
@@ -32,7 +33,7 @@ Gem::Specification.new do |s|
|
|
32
33
|
s.require_paths = ['lib']
|
33
34
|
|
34
35
|
# Files
|
35
|
-
s.files = Dir['lib/**/*', 'spec/**/*', '*.gemspec', '*.md', 'Gemfile', 'LICENSE','version.
|
36
|
+
s.files = Dir['lib/**/*', 'spec/**/*', '*.gemspec', '*.md', 'Gemfile', 'LICENSE', 'version.yaml']
|
36
37
|
# Tests
|
37
38
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
38
39
|
|
@@ -42,9 +43,12 @@ Gem::Specification.new do |s|
|
|
42
43
|
# Gem dependencies
|
43
44
|
s.add_runtime_dependency 'logstash-codec-json'
|
44
45
|
s.add_runtime_dependency 'logstash-core-plugin-api', '>= 2.0.0', '< 3'
|
46
|
+
s.add_runtime_dependency 'logstash-mixin-http_client', '>= 6.0.0', '< 8.0.0'
|
45
47
|
|
46
48
|
s.add_development_dependency 'logstash-devutils'
|
47
49
|
s.add_development_dependency 'logstash-input-generator'
|
50
|
+
s.add_development_dependency 'sinatra'
|
51
|
+
s.add_development_dependency 'webrick'
|
48
52
|
|
49
53
|
s.add_development_dependency 'rubocop', '1.9.1'
|
50
54
|
s.add_development_dependency 'rubocop-rake', '0.5.1'
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright
|
3
|
+
# Copyright 2023 Dynatrace LLC
|
4
4
|
#
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
6
|
# you may not use this file except in compliance with the License.
|
@@ -14,138 +14,481 @@
|
|
14
14
|
# See the License for the specific language governing permissions and
|
15
15
|
# limitations under the License.
|
16
16
|
|
17
|
-
|
18
|
-
require_relative '../../version'
|
19
|
-
require 'logstash/codecs/plain'
|
20
|
-
require 'logstash/event'
|
21
|
-
require 'net/http'
|
22
|
-
require 'json'
|
17
|
+
require File.expand_path('../spec_helper.rb', File.dirname(__FILE__))
|
23
18
|
|
24
19
|
describe LogStash::Outputs::Dynatrace do
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
20
|
+
# Wait for the async request to finish in this spinlock
|
21
|
+
# Requires pool_max to be 1
|
22
|
+
|
23
|
+
before(:all) do
|
24
|
+
@server = start_app_and_wait(TestApp)
|
25
|
+
end
|
26
|
+
|
27
|
+
after(:all) do
|
28
|
+
@server.shutdown # WEBrick::HTTPServer
|
29
|
+
begin
|
30
|
+
TestApp.stop!
|
31
|
+
rescue StandardError
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:port) { PORT }
|
37
|
+
let(:event) do
|
38
|
+
LogStash::Event.new({ 'message' => 'hi' })
|
30
39
|
end
|
31
|
-
let(:
|
32
|
-
let(:
|
40
|
+
let(:ingest_endpoint_url) { "http://localhost:#{port}/good" }
|
41
|
+
let(:api_key) { 'placeholder-key' }
|
42
|
+
|
43
|
+
shared_examples('failure log behaviour') do
|
44
|
+
it 'logs failure' do
|
45
|
+
expect(subject).to have_received(:log_failure).with(any_args)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'does not log headers' do
|
49
|
+
expect(subject).to have_received(:log_failure).with(anything, hash_not_including(:headers))
|
50
|
+
end
|
33
51
|
|
34
|
-
|
35
|
-
|
52
|
+
it 'does not log the message body' do
|
53
|
+
expect(subject).to have_received(:log_failure).with(anything, hash_not_including(:body))
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'with debug log level' do
|
57
|
+
before :all do
|
58
|
+
@current_log_level = LogStash::Logging::Logger.get_logging_context.get_root_logger.get_level.to_s.downcase
|
59
|
+
LogStash::Logging::Logger.configure_logging 'debug'
|
60
|
+
end
|
61
|
+
after :all do
|
62
|
+
LogStash::Logging::Logger.configure_logging @current_log_level
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'logs a failure' do
|
66
|
+
expect(subject).to have_received(:log_failure).with(anything, hash_including(:backtrace))
|
67
|
+
end
|
36
68
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
69
|
+
context 'with debug_include_headers false (default)' do
|
70
|
+
it 'does not log headers' do
|
71
|
+
expect(subject).to have_received(:log_failure).with(anything, hash_not_including(:headers))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'with debug_include_body false (default)' do
|
76
|
+
it 'does not log body' do
|
77
|
+
expect(subject).to have_received(:log_failure).with(anything, hash_not_including(:body))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'with debug_include_headers true' do
|
82
|
+
let(:config) { super().merge 'debug_include_headers' => true }
|
83
|
+
|
84
|
+
it 'logs headers' do
|
85
|
+
expect(subject).to have_received(:log_failure).with(anything, hash_including(:headers))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'with debug_include_body true' do
|
90
|
+
let(:config) { super().merge 'debug_include_body' => true }
|
91
|
+
|
92
|
+
it 'logs body' do
|
93
|
+
expect(subject).to have_received(:log_failure).with(anything, hash_including(:body))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'with debug_include_headers false' do
|
98
|
+
let(:config) { super().merge 'debug_include_headers' => false }
|
99
|
+
|
100
|
+
it 'logs headers' do
|
101
|
+
expect(subject).to have_received(:log_failure).with(anything, hash_not_including(:headers))
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context 'with debug_include_body false' do
|
106
|
+
let(:config) { super().merge 'debug_include_body' => false }
|
107
|
+
|
108
|
+
it 'logs body' do
|
109
|
+
expect(subject).to have_received(:log_failure).with(anything, hash_not_including(:body))
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
41
114
|
|
42
|
-
let(:
|
115
|
+
let(:config) { { 'ingest_endpoint_url' => ingest_endpoint_url, 'api_key' => api_key, 'pool_max' => 1 } }
|
116
|
+
subject { LogStash::Outputs::Dynatrace.new(config) }
|
117
|
+
|
118
|
+
let(:client) { subject.client }
|
43
119
|
|
44
120
|
before do
|
45
121
|
subject.register
|
122
|
+
allow(client).to receive(:post)
|
123
|
+
.with(ingest_endpoint_url, hash_including(:body, :headers))
|
124
|
+
.and_call_original
|
125
|
+
allow(subject).to receive(:log_failure).with(any_args)
|
126
|
+
allow(subject).to receive(:log_retryable_response).with(any_args)
|
46
127
|
end
|
47
128
|
|
48
|
-
|
49
|
-
|
50
|
-
|
129
|
+
context 'sending no events' do
|
130
|
+
it 'should not block the pipeline' do
|
131
|
+
subject.multi_receive([])
|
132
|
+
end
|
51
133
|
end
|
52
134
|
|
53
|
-
context '
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
expect(
|
61
|
-
|
62
|
-
ok
|
135
|
+
context 'performing a get' do
|
136
|
+
describe 'invoking the request' do
|
137
|
+
before do
|
138
|
+
subject.multi_receive([event])
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'should execute the request' do
|
142
|
+
expect(client).to have_received(:post)
|
143
|
+
.with(ingest_endpoint_url, hash_including(:body, :headers))
|
63
144
|
end
|
64
|
-
subject.multi_receive(events)
|
65
145
|
end
|
66
146
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
147
|
+
context 'with passing requests' do
|
148
|
+
before do
|
149
|
+
subject.multi_receive([event])
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'should not log a failure' do
|
153
|
+
expect(subject).not_to have_received(:log_failure).with(any_args)
|
71
154
|
end
|
72
|
-
subject.multi_receive(events)
|
73
155
|
end
|
74
156
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
157
|
+
context 'with failing requests' do
|
158
|
+
let(:ingest_endpoint_url) { "http://localhost:#{port}/bad" }
|
159
|
+
let(:api_key) { 'placeholder-key' }
|
160
|
+
|
161
|
+
before do
|
162
|
+
subject.multi_receive([event])
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'should log a failure' do
|
166
|
+
expect(subject).to have_received(:log_failure).with(any_args)
|
79
167
|
end
|
80
|
-
subject.multi_receive(events)
|
81
168
|
end
|
82
169
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
170
|
+
context 'with retryable failing requests' do
|
171
|
+
let(:ingest_endpoint_url) { "http://localhost:#{port}/retry" }
|
172
|
+
let(:api_key) { 'placeholder-key' }
|
173
|
+
|
174
|
+
before do
|
175
|
+
TestApp.retry_fail_count = 2
|
176
|
+
allow(subject).to receive(:send_event).and_call_original
|
177
|
+
allow(subject).to receive(:sleep_for_attempt) { 0 }
|
178
|
+
subject.multi_receive([event])
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'should log a retryable response 2 times' do
|
182
|
+
expect(subject).to have_received(:log_retryable_response).with(any_args).twice
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'should make three total requests' do
|
186
|
+
expect(subject).to have_received(:send_event).exactly(3).times
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
context 'on retryable unknown exception' do
|
192
|
+
before :each do
|
193
|
+
raised = false
|
194
|
+
original_method = subject.client.method(:post)
|
195
|
+
allow(subject).to receive(:send_event).and_call_original
|
196
|
+
expect(subject.client).to receive(:post) do |*args|
|
197
|
+
unless raised
|
198
|
+
raised = true
|
199
|
+
raise ::Manticore::UnknownException, 'Read timed out'
|
200
|
+
end
|
201
|
+
original_method.call(args)
|
87
202
|
end
|
88
|
-
subject.multi_receive(
|
203
|
+
subject.multi_receive([event])
|
89
204
|
end
|
90
205
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
expect(subject
|
95
|
-
expect(subject.logger).to_not receive(:warn)
|
96
|
-
expect(client).to receive(:request) { ok }
|
97
|
-
subject.multi_receive(events)
|
206
|
+
include_examples('failure log behaviour')
|
207
|
+
|
208
|
+
it 'retries' do
|
209
|
+
expect(subject).to have_received(:send_event).exactly(2).times
|
98
210
|
end
|
99
211
|
end
|
100
212
|
|
101
|
-
context '
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
expect(client).to receive(:
|
213
|
+
context 'on non-retryable unknown exception' do
|
214
|
+
before :each do
|
215
|
+
raised = false
|
216
|
+
original_method = subject.client.method(:post)
|
217
|
+
allow(subject).to receive(:send_event).and_call_original
|
218
|
+
expect(subject.client).to receive(:post) do |*args|
|
219
|
+
unless raised
|
220
|
+
raised = true
|
221
|
+
raise ::Manticore::UnknownException, 'broken'
|
222
|
+
end
|
223
|
+
original_method.call(args)
|
224
|
+
end
|
225
|
+
subject.multi_receive([event])
|
226
|
+
end
|
107
227
|
|
228
|
+
include_examples('failure log behaviour')
|
108
229
|
|
109
|
-
|
110
|
-
expect(subject).to
|
111
|
-
|
112
|
-
|
113
|
-
expect(subject).to receive(:sleep).with(16).ordered
|
230
|
+
it 'does not retry' do
|
231
|
+
expect(subject).to have_received(:send_event).exactly(1).times
|
232
|
+
end
|
233
|
+
end
|
114
234
|
|
115
|
-
|
116
|
-
|
235
|
+
context 'on non-retryable exception' do
|
236
|
+
before :each do
|
237
|
+
raised = false
|
238
|
+
original_method = subject.client.method(:post)
|
239
|
+
allow(subject).to receive(:send_event).and_call_original
|
240
|
+
expect(subject.client).to receive(:post) do |*args|
|
241
|
+
unless raised
|
242
|
+
raised = true
|
243
|
+
raise 'broken'
|
244
|
+
end
|
245
|
+
original_method.call(args)
|
246
|
+
end
|
247
|
+
subject.multi_receive([event])
|
248
|
+
end
|
249
|
+
|
250
|
+
include_examples('failure log behaviour')
|
251
|
+
|
252
|
+
it 'does not retry' do
|
253
|
+
expect(subject).to have_received(:send_event).exactly(1).times
|
117
254
|
end
|
118
255
|
end
|
119
256
|
|
120
|
-
context '
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
subject.
|
257
|
+
context 'on retryable exception' do
|
258
|
+
before :each do
|
259
|
+
raised = false
|
260
|
+
original_method = subject.client.method(:post)
|
261
|
+
allow(subject).to receive(:send_event).and_call_original
|
262
|
+
expect(subject.client).to receive(:post) do |*args|
|
263
|
+
unless raised
|
264
|
+
raised = true
|
265
|
+
raise ::Manticore::Timeout, 'broken'
|
266
|
+
end
|
267
|
+
original_method.call(args)
|
268
|
+
end
|
269
|
+
subject.multi_receive([event])
|
125
270
|
end
|
126
271
|
|
127
|
-
it '
|
128
|
-
expect(
|
129
|
-
|
130
|
-
expect(client_error).to receive(:body) { body }
|
272
|
+
it 'retries' do
|
273
|
+
expect(subject).to have_received(:send_event).exactly(2).times
|
274
|
+
end
|
131
275
|
|
132
|
-
|
133
|
-
|
276
|
+
include_examples('failure log behaviour')
|
277
|
+
end
|
134
278
|
|
135
|
-
|
279
|
+
shared_examples('a received event') do
|
280
|
+
before do
|
281
|
+
TestApp.last_request = nil
|
282
|
+
end
|
283
|
+
|
284
|
+
let(:events) { [event] }
|
285
|
+
|
286
|
+
describe 'with a good code' do
|
287
|
+
before do
|
288
|
+
subject.multi_receive(events)
|
289
|
+
end
|
290
|
+
|
291
|
+
let(:last_request) { TestApp.last_request }
|
292
|
+
let(:body) { last_request.body.read }
|
293
|
+
let(:content_type) { last_request.env['CONTENT_TYPE'] }
|
294
|
+
|
295
|
+
it 'should receive the request' do
|
296
|
+
expect(last_request).to be_truthy
|
297
|
+
end
|
298
|
+
|
299
|
+
it 'should receive the event as a hash' do
|
300
|
+
expect(body).to eql(expected_body)
|
301
|
+
end
|
302
|
+
|
303
|
+
it 'should have the correct content type' do
|
304
|
+
expect(content_type).to eql(expected_content_type)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
describe 'a retryable code' do
|
309
|
+
let(:ingest_endpoint_url) { "http://localhost:#{port}/retry" }
|
310
|
+
let(:api_key) { 'placeholder-key' }
|
311
|
+
|
312
|
+
before do
|
313
|
+
TestApp.retry_fail_count = 2
|
314
|
+
allow(subject).to receive(:send_event).and_call_original
|
315
|
+
allow(subject).to receive(:log_retryable_response)
|
316
|
+
subject.multi_receive(events)
|
317
|
+
end
|
318
|
+
|
319
|
+
it 'should retry' do
|
320
|
+
expect(subject).to have_received(:log_retryable_response).with(any_args).twice
|
321
|
+
end
|
136
322
|
end
|
137
323
|
end
|
138
324
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
325
|
+
shared_examples 'integration tests' do
|
326
|
+
let(:base_config) { {} }
|
327
|
+
let(:ingest_endpoint_url) { "http://localhost:#{port}/good" }
|
328
|
+
let(:api_key) { 'placeholder-key' }
|
329
|
+
let(:event) do
|
330
|
+
LogStash::Event.new('foo' => 'bar', 'baz' => 'bot', 'user' => 'McBest')
|
331
|
+
end
|
332
|
+
|
333
|
+
subject { LogStash::Outputs::Dynatrace.new(config) }
|
334
|
+
|
335
|
+
before do
|
336
|
+
subject.register
|
337
|
+
end
|
338
|
+
|
339
|
+
describe 'sending with the default (JSON) config' do
|
340
|
+
let(:config) do
|
341
|
+
base_config.merge({ 'ingest_endpoint_url' => ingest_endpoint_url, 'api_key' => api_key, 'pool_max' => 1 })
|
342
|
+
end
|
343
|
+
let(:expected_body) { "#{LogStash::Json.dump([event].map(&:to_hash)).chomp}\n" }
|
344
|
+
let(:expected_content_type) { 'application/json; charset=utf-8' }
|
345
|
+
|
346
|
+
include_examples('a received event')
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
describe 'integration test without gzip compression' do
|
351
|
+
include_examples('integration tests')
|
352
|
+
end
|
353
|
+
|
354
|
+
# describe "integration test with gzip compression" do
|
355
|
+
# include_examples("integration tests") do
|
356
|
+
# let(:base_config) { { "http_compression" => true } }
|
357
|
+
# end
|
358
|
+
# end
|
359
|
+
|
360
|
+
describe 'retryable error in termination' do
|
361
|
+
let(:ingest_endpoint_url) { "http://localhost:#{port - 1}/invalid" }
|
362
|
+
let(:api_key) { 'placeholder-key' }
|
363
|
+
let(:events) { [event] }
|
364
|
+
let(:config) { { 'ingest_endpoint_url' => ingest_endpoint_url, 'api_key' => api_key, 'pool_max' => 1 } }
|
365
|
+
|
366
|
+
subject { LogStash::Outputs::Dynatrace.new(config) }
|
367
|
+
|
368
|
+
before do
|
369
|
+
subject.register
|
370
|
+
allow(subject).to receive(:pipeline_shutdown_requested?).and_return(true)
|
371
|
+
end
|
372
|
+
|
373
|
+
it 'raise exception to exit indefinitely retry' do
|
374
|
+
expect do
|
375
|
+
subject.multi_receive(events)
|
376
|
+
end.to raise_error(LogStash::Outputs::Dynatrace::PluginInternalQueueLeftoverError)
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
RSpec.describe LogStash::Outputs::Dynatrace do # different block as we're starting web server with TLS
|
382
|
+
@@default_server_settings = TestApp.server_settings.dup
|
383
|
+
|
384
|
+
before do
|
385
|
+
TestApp.server_settings = @@default_server_settings.merge(webrick_config)
|
386
|
+
|
387
|
+
TestApp.last_request = nil
|
388
|
+
|
389
|
+
@server = start_app_and_wait(TestApp)
|
390
|
+
end
|
391
|
+
|
392
|
+
let(:webrick_config) do
|
393
|
+
cert, key = WEBrick::Utils.create_self_signed_cert 2048, [['CN', ssl_cert_host]], 'Logstash testing'
|
394
|
+
{
|
395
|
+
SSLEnable: true,
|
396
|
+
SSLVerifyClient: OpenSSL::SSL::VERIFY_NONE,
|
397
|
+
SSLCertificate: cert,
|
398
|
+
SSLPrivateKey: key
|
399
|
+
}
|
400
|
+
end
|
401
|
+
|
402
|
+
after do
|
403
|
+
@server.shutdown # WEBrick::HTTPServer
|
404
|
+
|
405
|
+
begin
|
406
|
+
TestApp.stop!
|
407
|
+
rescue StandardError
|
408
|
+
nil
|
409
|
+
end
|
410
|
+
TestApp.server_settings = @@default_server_settings
|
411
|
+
end
|
412
|
+
|
413
|
+
let(:ssl_cert_host) { 'localhost' }
|
414
|
+
|
415
|
+
let(:port) { PORT }
|
416
|
+
let(:ingest_endpoint_url) { "https://localhost:#{port}/good" }
|
417
|
+
let(:api_key) { 'placeholder-key' }
|
418
|
+
let(:method) { 'post' }
|
419
|
+
|
420
|
+
let(:config) { { 'ingest_endpoint_url' => ingest_endpoint_url, 'api_key' => api_key } }
|
421
|
+
|
422
|
+
subject { LogStash::Outputs::Dynatrace.new(config) }
|
423
|
+
|
424
|
+
before { subject.register }
|
425
|
+
after { subject.close }
|
426
|
+
|
427
|
+
let(:last_request) { TestApp.last_request }
|
428
|
+
let(:last_request_body) { last_request.body.read }
|
429
|
+
|
430
|
+
let(:event) { LogStash::Event.new('message' => 'hello!') }
|
431
|
+
|
432
|
+
context 'with default (full) verification' do
|
433
|
+
let(:config) { super() } # 'ssl_verification_mode' => 'full'
|
434
|
+
|
435
|
+
it 'does NOT process the request (due client protocol exception)' do
|
436
|
+
# Manticore's default verification does not accept self-signed certificates!
|
437
|
+
Thread.start do
|
438
|
+
subject.multi_receive [event]
|
439
|
+
end
|
440
|
+
sleep 1.5
|
441
|
+
|
442
|
+
expect(last_request).to be nil
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
context 'with verification disabled' do
|
447
|
+
let(:config) { super().merge 'ssl_verification_mode' => 'none' }
|
448
|
+
|
449
|
+
it 'should process the request' do
|
450
|
+
subject.multi_receive [event]
|
451
|
+
expect(last_request_body).to include '"message":"hello!"'
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
unless tls_version_enabled_by_default?('TLSv1.1')
|
456
|
+
context 'with supported_protocols set to (disabled) 1.1' do
|
457
|
+
let(:config) { super().merge 'ssl_supported_protocols' => ['TLSv1.1'], 'ssl_verification_mode' => 'none' }
|
458
|
+
|
459
|
+
it 'keeps retrying due a protocol exception' do # TLSv1.1 not enabled by default
|
460
|
+
expect(subject).to receive(:log_failure)
|
461
|
+
.with('Could not fetch URL', hash_including(message: 'No appropriate protocol (protocol is disabled or cipher suites are inappropriate)'))
|
462
|
+
.at_least(:once)
|
463
|
+
Thread.start { subject.multi_receive [event] }
|
464
|
+
sleep 1.0
|
145
465
|
end
|
466
|
+
end
|
467
|
+
end
|
146
468
|
|
147
|
-
|
148
|
-
|
469
|
+
context 'with supported_protocols set to 1.2/1.3' do
|
470
|
+
let(:config) do
|
471
|
+
super().merge 'ssl_supported_protocols' => ['TLSv1.2', 'TLSv1.3'], 'ssl_verification_mode' => 'none'
|
472
|
+
end
|
473
|
+
|
474
|
+
let(:webrick_config) { super().merge SSLVersion: 'TLSv1.2' }
|
475
|
+
|
476
|
+
it 'should process the request' do
|
477
|
+
subject.multi_receive [event]
|
478
|
+
expect(last_request_body).to include '"message":"hello!"'
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
if tls_version_enabled_by_default?('TLSv1.3') && JOpenSSL::VERSION > '0.12'
|
483
|
+
context 'with supported_protocols set to 1.3' do
|
484
|
+
let(:config) { super().merge 'ssl_supported_protocols' => ['TLSv1.3'], 'ssl_verification_mode' => 'none' }
|
485
|
+
|
486
|
+
let(:webrick_config) { super().merge SSLVersion: 'TLSv1.3' }
|
487
|
+
|
488
|
+
it 'should process the request' do
|
489
|
+
subject.multi_receive [event]
|
490
|
+
expect(last_request_body).to include '"message":"hello!"'
|
491
|
+
end
|
149
492
|
end
|
150
493
|
end
|
151
494
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,4 +1,145 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Copyright 2023 Dynatrace LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
3
17
|
require 'logstash/devutils/rspec/spec_helper'
|
4
18
|
require 'logstash/outputs/dynatrace'
|
19
|
+
require 'logstash/codecs/plain'
|
20
|
+
|
21
|
+
require 'sinatra'
|
22
|
+
require 'webrick'
|
23
|
+
require 'webrick/https'
|
24
|
+
require 'openssl'
|
25
|
+
|
26
|
+
PORT = rand(65_535 - 1024) + 1025
|
27
|
+
|
28
|
+
module LogStash
|
29
|
+
module Outputs
|
30
|
+
class Dynatrace
|
31
|
+
attr_writer :agent
|
32
|
+
attr_reader :request_tokens
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# NOTE: extend WEBrick with support for config[:SSLVersion]
|
38
|
+
WEBrick::GenericServer.class_eval do
|
39
|
+
alias_method :__setup_ssl_context, :setup_ssl_context
|
40
|
+
|
41
|
+
def setup_ssl_context(config)
|
42
|
+
ctx = __setup_ssl_context(config)
|
43
|
+
ctx.ssl_version = config[:SSLVersion] if config[:SSLVersion]
|
44
|
+
ctx
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# NOTE: that Sinatra startup and shutdown messages are directly logged to stderr so
|
49
|
+
# it is not really possible to disable them without reopening stderr which is not advisable.
|
50
|
+
#
|
51
|
+
# == Sinatra (v1.4.6) has taken the stage on 51572 for development with backup from WEBrick
|
52
|
+
# == Sinatra has ended his set (crowd applauds)
|
53
|
+
#
|
54
|
+
class TestApp < Sinatra::Base
|
55
|
+
set :environment, :production
|
56
|
+
set :sessions, false
|
57
|
+
|
58
|
+
@@server_settings = {
|
59
|
+
AccessLog: [], # disable WEBrick logging
|
60
|
+
Logger: WEBrick::BasicLog.new(nil, WEBrick::BasicLog::FATAL)
|
61
|
+
}
|
62
|
+
|
63
|
+
def self.server_settings
|
64
|
+
@@server_settings
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.server_settings=(settings)
|
68
|
+
@@server_settings = settings
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.multiroute(methods, path, &block)
|
72
|
+
methods.each do |method|
|
73
|
+
method.to_sym
|
74
|
+
send method, path, &block
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class << self
|
79
|
+
attr_writer :last_request
|
80
|
+
end
|
81
|
+
|
82
|
+
class << self
|
83
|
+
attr_reader :last_request
|
84
|
+
end
|
85
|
+
|
86
|
+
class << self
|
87
|
+
attr_writer :retry_fail_count
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.retry_fail_count
|
91
|
+
@retry_fail_count || 2
|
92
|
+
end
|
93
|
+
|
94
|
+
multiroute(%w[get post put patch delete], '/good') do
|
95
|
+
self.class.last_request = request
|
96
|
+
[200, 'YUP']
|
97
|
+
end
|
98
|
+
|
99
|
+
multiroute(%w[get post put patch delete], '/bad') do
|
100
|
+
self.class.last_request = request
|
101
|
+
[400, 'YUP']
|
102
|
+
end
|
103
|
+
|
104
|
+
multiroute(%w[get post put patch delete], '/retry') do
|
105
|
+
self.class.last_request = request
|
106
|
+
|
107
|
+
if self.class.retry_fail_count > 0
|
108
|
+
self.class.retry_fail_count -= 1
|
109
|
+
[429, "Will succeed in #{self.class.retry_fail_count}"]
|
110
|
+
else
|
111
|
+
[200, 'Done Retrying']
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
RSpec.configure do |config|
|
117
|
+
# http://stackoverflow.com/questions/6557079/start-and-call-ruby-http-server-in-the-same-script
|
118
|
+
def start_app_and_wait(app, opts = {})
|
119
|
+
queue = Queue.new
|
120
|
+
|
121
|
+
Thread.start do
|
122
|
+
begin
|
123
|
+
app.start!({ server: 'WEBrick', port: PORT }.merge(opts)) do |server|
|
124
|
+
yield(server) if block_given?
|
125
|
+
queue.push(server)
|
126
|
+
end
|
127
|
+
rescue StandardError => e
|
128
|
+
warn "Error starting app: #{e.inspect}" # ignore
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
queue.pop # blocks until the start! callback runs
|
133
|
+
end
|
134
|
+
|
135
|
+
config.extend(Module.new do
|
136
|
+
def tls_version_enabled_by_default?(tls_version)
|
137
|
+
context = javax.net.ssl.SSLContext.getInstance('TLS')
|
138
|
+
context.init nil, nil, nil
|
139
|
+
context.getDefaultSSLParameters.getProtocols.include? tls_version.to_s
|
140
|
+
rescue StandardError => e
|
141
|
+
warn "#{__method__} failed : #{e.inspect}"
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
end)
|
145
|
+
end
|
data/version.yaml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
logstash-output-dynatrace: '0.5.0.rc1'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-output-dynatrace
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dynatrace Open Source Engineering
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-07-
|
11
|
+
date: 2023-07-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: logstash-codec-json
|
@@ -44,6 +44,26 @@ dependencies:
|
|
44
44
|
- - "<"
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '3'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: logstash-mixin-http_client
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 6.0.0
|
54
|
+
- - "<"
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 8.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: 6.0.0
|
64
|
+
- - "<"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 8.0.0
|
47
67
|
- !ruby/object:Gem::Dependency
|
48
68
|
name: logstash-devutils
|
49
69
|
requirement: !ruby/object:Gem::Requirement
|
@@ -72,6 +92,34 @@ dependencies:
|
|
72
92
|
- - ">="
|
73
93
|
- !ruby/object:Gem::Version
|
74
94
|
version: '0'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: sinatra
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
- !ruby/object:Gem::Dependency
|
110
|
+
name: webrick
|
111
|
+
requirement: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
type: :development
|
117
|
+
prerelease: false
|
118
|
+
version_requirements: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
75
123
|
- !ruby/object:Gem::Dependency
|
76
124
|
name: rubocop
|
77
125
|
requirement: !ruby/object:Gem::Requirement
|
@@ -115,11 +163,12 @@ files:
|
|
115
163
|
- Gemfile
|
116
164
|
- LICENSE
|
117
165
|
- README.md
|
166
|
+
- lib/dynatrace/version.rb
|
118
167
|
- lib/logstash/outputs/dynatrace.rb
|
119
168
|
- logstash-output-dynatrace.gemspec
|
120
169
|
- spec/outputs/dynatrace_spec.rb
|
121
170
|
- spec/spec_helper.rb
|
122
|
-
- version.
|
171
|
+
- version.yaml
|
123
172
|
homepage: https://github.com/dynatrace-oss/logstash-output-dynatrace
|
124
173
|
licenses:
|
125
174
|
- Apache-2.0
|
@@ -137,9 +186,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
137
186
|
version: '0'
|
138
187
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
188
|
requirements:
|
140
|
-
- - "
|
189
|
+
- - ">"
|
141
190
|
- !ruby/object:Gem::Version
|
142
|
-
version:
|
191
|
+
version: 1.3.1
|
143
192
|
requirements: []
|
144
193
|
rubygems_version: 3.1.6
|
145
194
|
signing_key:
|