logstash-output-dynatrace 0.4.0 → 0.5.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 657b814816f85318e7968d096c9d5c5de194002f39a1bdb3971442006001af8e
4
- data.tar.gz: 7a94f714d34078f792a648f3c468518099601e1c0e23614461aa0ed6a97dc50d
3
+ metadata.gz: f19d849000371b66a0e843352045a31bef644981f413bd81b22553bc9775ad3f
4
+ data.tar.gz: 2086305ff6250a75137771c2999c040f33d880156f3afd6e96534eefe416fa76
5
5
  SHA512:
6
- metadata.gz: 67579448d2e0034ba41b01cc5cdf5d6365d02e34c67c5b9698dc1af3638c885301610373fc774afba4c5b2a204aa42920d402a340520198057036c7362e00515
7
- data.tar.gz: a6ee38c6ba636b731e343e6e63c2d190a9d8b90d9e241f435af404e39f9b25e3b043ffa79ae35f87cb5f9be8013929afdaa819570e9b6257e04e23cbcc7c1281
6
+ metadata.gz: cc6a3d695ffb5ed72f9313b262275b2b002dcf60f4659afb42dac8d8a13c402b0a314f85dc7e6cb15a7ee0421697d35d3675bf002404324665dcdb353370d7db
7
+ data.tar.gz: 2324e4a662915858622b80fb8adbaba7613a6015f59f60092ec51f0225092cbaa51cb6c38d3800e245404f5541d9bd562eddfbd33b80e5e578f237e2d555dd40
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2021 Dynatrace LLC
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.
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
- ### `codec`
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 2021 Dynatrace LLC
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
- # Also required to change the version in lib/logstash/outputs/dynatrace.rb
19
- VERSION = '0.4.0'
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 2021 Dynatrace LLC
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 'openssl'
20
+ require 'logstash/version'
21
+ require 'dynatrace/version'
22
+ require 'uri'
23
+ require 'logstash/plugin_mixins/http_client'
21
24
 
22
- MAX_RETRIES = 5
23
- PLUGIN_VERSION = '0.4.0'
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
- config_name 'dynatrace'
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
- concurrency :single
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
- default :codec, 'json'
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
- attr_accessor :uri, :plugin_version
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
- @logger.debug("Registering plugin")
53
- require 'net/https'
54
- require 'uri'
55
- @uri = URI.parse(@ingest_endpoint_url.uri.to_s)
56
- @client = Net::HTTP.new(@uri.host, @uri.port)
57
-
58
- if uri.scheme == 'https'
59
- @client.use_ssl = true
60
- @client.verify_mode = OpenSSL::SSL::VERIFY_NONE if @ssl_verify_none
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 headers
106
+ def make_headers
66
107
  {
67
- 'User-Agent' => "logstash-output-dynatrace/#{PLUGIN_VERSION}",
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
- # Takes an array of events
74
- def multi_receive(events)
75
- @logger.debug("Received #{events.length} events")
76
- return if events.length.zero?
77
-
78
- retries = 0
79
- begin
80
- request = Net::HTTP::Post.new(uri, headers)
81
- request.body = "#{LogStash::Json.dump(events.map(&:to_hash)).chomp}\n"
82
- response = @client.request(request)
83
-
84
- case response
85
- when Net::HTTPSuccess
86
- @logger.debug("successfully sent #{events.length} events#{" with #{retries} retries" if retries > 0}")
87
- when Net::HTTPServerError
88
- @logger.error("Encountered an HTTP server error", :message => response.message, :code => response.code, :body => response.body) if retries == 0
89
- when Net::HTTPNotFound
90
- @logger.error("Encountered a 404 Not Found error. Please check that log ingest is enabled and your API token has the `logs.ingest` (Ingest Logs) scope.", :message => response.message, :code => response.code)
91
- when Net::HTTPClientError
92
- @logger.error("Encountered an HTTP client error", :message => response.message, :code => response.code, :body => response.body)
93
- else
94
- @logger.error("Encountered an unexpected response code", :message => response.message, :code => response.code)
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
- raise RetryableError.new "code #{response.code}" if retryable(response)
98
-
99
- rescue Net::OpenTimeout, Net::HTTPBadResponse, OpenSSL::SSL::SSLError, RetryableError => e
100
- # Net::OpenTimeout indicates a connection could not be established within the timeout period
101
- # Net::HTTPBadResponse indicates a protocol error
102
- # OpenSSL::SSL::SSLErrorWaitReadable indicates an error establishing the ssl connection
103
- if retries < MAX_RETRIES
104
- sleep_seconds = 2 ** retries
105
- @logger.warn("Failed to contact dynatrace: #{e.message}. Trying again after #{sleep_seconds} seconds.")
106
- sleep sleep_seconds
107
- retries += 1
108
- retry
109
- else
110
- @logger.error("Failed to export logs to Dynatrace.")
111
- return
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
- rescue StandardError => e
114
- @logger.error("Unknown error raised", :error => e.inspect)
115
- raise e
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 retryable(response)
120
- return response.is_a? Net::HTTPServerError
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 2021 Dynatrace LLC
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
- require_relative './version'
17
+ require 'yaml'
18
18
 
19
19
  Gem::Specification.new do |s|
20
20
  s.name = 'logstash-output-dynatrace'
21
- s.version = DynatraceConstants::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.rb']
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 2021 Dynatrace LLC
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
- require_relative '../spec_helper'
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
- let(:events) do
26
- [
27
- LogStash::Event.new({ 'message' => 'message 1', '@timestamp' => "2021-06-25T15:46:45.693Z" }),
28
- LogStash::Event.new({ 'message' => 'message 2', '@timestamp' => "2021-06-25T15:46:46.693Z" }),
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(:url) { "http://localhost/good" }
32
- let(:key) { 'api.key' }
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
- let(:subject) { LogStash::Outputs::Dynatrace.new({ 'api_key' => key, 'ingest_endpoint_url' => url }) }
35
- let(:client) { subject.instance_variable_get(:@client) }
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
- let(:ok) { Net::HTTPOK.new "1.1", "200", "OK" }
38
- let(:server_error) { Net::HTTPServerError.new "1.1", "500", "Internal Server Error" }
39
- let(:client_error) { Net::HTTPClientError.new("1.1", '400', 'Client error') }
40
- let(:not_found) { Net::HTTPNotFound.new "1.1", "404", "Not Found" }
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(:body) { "this is a failure" }
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
- it 'does not send empty events' do
49
- expect(client).to_not receive(:request)
50
- subject.multi_receive([])
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 'server response success' do
54
- it 'sends events' do
55
- expect(client).to receive(:request) do |req|
56
- body = JSON.parse(req.body)
57
- expect(body.length).to eql(2)
58
- expect(body[0]['message']).to eql('message 1')
59
- expect(body[0]['@timestamp']).to eql('2021-06-25T15:46:45.693Z')
60
- expect(body[1]['message']).to eql('message 2')
61
- expect(body[1]['@timestamp']).to eql('2021-06-25T15:46:46.693Z')
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
- it 'includes authorization header' do
68
- expect(client).to receive(:request) do |req|
69
- expect(req['Authorization']).to eql("Api-Token #{key}")
70
- ok
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
- it 'includes content type header' do
76
- expect(client).to receive(:request) do |req|
77
- expect(req['Content-Type']).to eql('application/json; charset=utf-8')
78
- ok
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
- it 'includes user agent' do
84
- expect(client).to receive(:request) do |req|
85
- expect(req['User-Agent']).to eql("logstash-output-dynatrace/#{::DynatraceConstants::VERSION}")
86
- ok
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(events)
203
+ subject.multi_receive([event])
89
204
  end
90
205
 
91
- it 'does not log on success' do
92
- allow(subject.logger).to receive(:debug)
93
- expect(subject.logger).to_not receive(:info)
94
- expect(subject.logger).to_not receive(:error)
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 'with server error' do
102
- it 'retries 5 times with exponential backoff' do
103
- # This prevents the elusive "undefined method `close' for nil:NilClass" error.
104
- expect(server_error).to receive(:body) { body }.once
105
- expect(subject.logger).to receive(:error).with("Encountered an HTTP server error", {:body=>body, :code=>"500", :message=> "Internal Server Error"}).once
106
- expect(client).to receive(:request) { server_error }.exactly(6).times
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
- expect(subject).to receive(:sleep).with(1).ordered
110
- expect(subject).to receive(:sleep).with(2).ordered
111
- expect(subject).to receive(:sleep).with(4).ordered
112
- expect(subject).to receive(:sleep).with(8).ordered
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
- expect(subject.logger).to receive(:error).with("Failed to export logs to Dynatrace.")
116
- subject.multi_receive(events)
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 'with client error' do
121
- it 'does not retry on 404' do
122
- allow(subject.logger).to receive(:error)
123
- expect(client).to receive(:request) { not_found }.once
124
- subject.multi_receive(events)
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 'logs the response body' do
128
- expect(client).to receive(:request) { client_error }
129
- # This prevents the elusive "undefined method `close' for nil:NilClass" error.
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
- expect(subject.logger).to receive(:error).with("Encountered an HTTP client error",
133
- {:body=>body, :code=>"400", :message=> "Client error"})
276
+ include_examples('failure log behaviour')
277
+ end
134
278
 
135
- subject.multi_receive(events)
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
- context 'when an unknown error occurs' do
140
- it 'logs and re-raises the error' do
141
- class BadEvents
142
- def length
143
- 1
144
- end
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
- expect(subject.logger).to receive(:error)
148
- expect { subject.multi_receive(BadEvents.new) }.to raise_error(StandardError)
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.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-10 00:00:00.000000000 Z
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.rb
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: '0'
191
+ version: 1.3.1
143
192
  requirements: []
144
193
  rubygems_version: 3.1.6
145
194
  signing_key: