logstash-output-dynatrace 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +52 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f41e7086fbd73db7471823a9bff842ad41783ba20b1f2ba112755f251af28c0a
|
4
|
+
data.tar.gz: 9b645d8c4f9eb4d416dc6c873cceb8779bae7c7745e63f009f2eaee4a5b29e52
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 043e6e396b6aa6e47e7dc6f2245440e83e295deeabc866a9bd96786f5d8c0c6b3298d936a231ec05d93f438424e92090f2f1cbff2edfd23fb6e4ebbbdf108471
|
7
|
+
data.tar.gz: 4940d3ab0bea54b9125da346c46157c7e16bf11e4fd529ce7d80fcbc07f7695131035b38ede87c13ca489b1f65815a1004e2666593983ce3fb8800227ad2265e
|
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'
|
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
|
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-
|
11
|
+
date: 2023-08-09 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
|