logstash-output-dynatrace 0.1.0 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fa581d24c7d6dea1eed2199562913047b8712dc6f1c0c1259c7621880605552
4
- data.tar.gz: 6ade6b59b77d436cbc9a537ce3fd87ca39345c243bd61bf927514c12d50f3246
3
+ metadata.gz: 74790f2358ebc0f61dbc88da2c1af0beed1ac35a3a0f021a18e1d365e7751e3a
4
+ data.tar.gz: 4d8cbb3c59a4bd6224457f49c25f01932686662551c7642f6fc65047663413e7
5
5
  SHA512:
6
- metadata.gz: 3b03743e665c6af4ae1bf6be5031884aa71cc5dc1d27744d7229ce8bf5ccedd6e56097816bf73548492196a2bf484af9bbd5be41bcaca982c915a634a60af161
7
- data.tar.gz: f7621c4ca998dca1166f73927ac58dc585c71036dd70af2d59ae8640c7aa4661eaf9ba64a4632127449d3d4e494c35321f25f5fdab110d6bbcbb9ac0395fcc33
6
+ metadata.gz: 2f970e3aa82cb27219224b241c3b30d3e0c5e3a6a70cec220ce3d7952507ab9a7a1d5c207f22602385871baee310ec3fe3b291c1f8e486462656d5c75b60cbf1
7
+ data.tar.gz: dd03de995d2038dfcf533fa050b6ceb16dd627a5e3258c95528e96eaf4b345efacb92f3cc81e922c3af604732f4ed532342e245bc0e1d74d4a3592394746ec50
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## 0.2.0
2
+ - Add retries with exponential backoff (#8)
3
+
4
+ ## 0.1.0
5
+ - Initial release
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
@@ -18,16 +18,19 @@ require 'logstash/namespace'
18
18
  require 'logstash/outputs/base'
19
19
  require 'logstash/json'
20
20
 
21
+ MAX_RETRIES = 5
22
+
21
23
  module LogStash
22
24
  module Outputs
25
+ class RetryableError < StandardError;
26
+ end
27
+
23
28
  # An output which sends logs to the Dynatrace log ingest v2 endpoint formatted as JSON
24
29
  class Dynatrace < LogStash::Outputs::Base
25
30
  @plugin_version = ::File.read(::File.expand_path('../../../VERSION', __dir__)).strip
26
31
 
27
32
  config_name 'dynatrace'
28
33
 
29
- concurrency :single
30
-
31
34
  # The full URL of the Dynatrace log ingestion endpoint:
32
35
  # - on SaaS: https://{your-environment-id}.live.dynatrace.com/api/v2/logs/ingest
33
36
  # - on Managed: https://{your-domain}/e/{your-environment-id}/api/v2/logs/ingest
@@ -41,7 +44,7 @@ module LogStash
41
44
 
42
45
  default :codec, 'json'
43
46
 
44
- attr_accessor :uri
47
+ attr_accessor :uri, :plugin_version
45
48
 
46
49
  def register
47
50
  require 'net/https'
@@ -56,11 +59,6 @@ module LogStash
56
59
  @logger.info('Client', client: @client.inspect)
57
60
  end
58
61
 
59
- # This is split into a separate method mostly to help testing
60
- def log_failure(message, opts)
61
- @logger.error(message, opts)
62
- end
63
-
64
62
  def headers
65
63
  {
66
64
  'User-Agent' => "logstash-output-dynatrace v#{@plugin_version}",
@@ -73,12 +71,44 @@ module LogStash
73
71
  def multi_receive(events)
74
72
  return if events.length.zero?
75
73
 
76
- request = Net::HTTP::Post.new(uri, headers)
77
- request.body = "#{LogStash::Json.dump(events.map(&:to_hash)).chomp}\n"
78
- response = @client.request(request)
79
- return if response.is_a? Net::HTTPSuccess
74
+ retries = 0
75
+ begin
76
+ request = Net::HTTP::Post.new(uri, headers)
77
+ request.body = "#{LogStash::Json.dump(events.map(&:to_hash)).chomp}\n"
78
+ response = send(request)
79
+ return if response.is_a? Net::HTTPSuccess
80
+
81
+ failure_message = "Dynatrace returned #{response.code} #{response.message}."
82
+
83
+ if response.is_a? Net::HTTPServerError
84
+ raise RetryableError.new failure_message
85
+ end
86
+
87
+ if response.is_a? Net::HTTPNotFound
88
+ @logger.error("#{failure_message} Please check that log ingest is enabled and your API token has the `logs.ingest` (Ingest Logs) scope.")
89
+ return
90
+ end
91
+
92
+ if response.is_a? Net::HTTPClientError
93
+ @logger.error(failure_message)
94
+ return
95
+ end
96
+ rescue Net::HTTPBadResponse, RetryableError => e
97
+ # indicates a protocol error
98
+ if retries < MAX_RETRIES
99
+ sleep_seconds = 2 ** retries
100
+ @logger.warn("Failed to contact dynatrace: #{e.message}. Trying again after #{sleep_seconds} seconds.")
101
+ sleep sleep_seconds
102
+ retries += 1
103
+ retry
104
+ else
105
+ @logger.error("Failed to export logs to Dynatrace.")
106
+ end
107
+ end
108
+ end
80
109
 
81
- log_failure('Bad Response', request: request.inspect, response: response.inspect)
110
+ def send(request)
111
+ @client.request(request)
82
112
  end
83
113
  end
84
114
  end
@@ -19,133 +19,109 @@ require 'logstash/codecs/plain'
19
19
  require 'logstash/event'
20
20
  require 'sinatra'
21
21
  require 'insist'
22
-
23
- PORT = rand(65_535 - 1024) + 1025
24
-
25
- # NOTE: that Sinatra startup and shutdown messages are directly logged to stderr so
26
- # it is not really possible to disable them without reopening stderr which is not advisable.
27
- #
28
- # == Sinatra (v1.4.6) has taken the stage on 51572 for development with backup from WEBrick
29
- # == Sinatra has ended his set (crowd applauds)
30
- #
31
- class TestApp < Sinatra::Base
32
- # disable WEBrick logging
33
- def self.server_settings
34
- { AccessLog: [], Logger: WEBrick::BasicLog.new(nil, WEBrick::BasicLog::FATAL) }
35
- end
36
-
37
- class << self
38
- attr_accessor :last_request
39
-
40
- def clear
41
- self.last_request = nil
42
- end
43
- end
44
-
45
- post '/good' do
46
- self.class.last_request = request
47
- [204, '']
48
- end
49
-
50
- post '/bad' do
51
- self.class.last_request = request
52
- [400, 'Bad']
53
- end
54
- end
55
-
56
- RSpec.configure do |config|
57
- # http://stackoverflow.com/questions/6557079/start-and-call-ruby-http-server-in-the-same-script
58
- def sinatra_run_wait(app, opts)
59
- queue = Queue.new
60
-
61
- t = java.lang.Thread.new(
62
- proc do
63
- begin
64
- app.run!(opts) do |_server|
65
- queue.push('started')
66
- end
67
- rescue StandardError => e
68
- puts "Error in webserver thread #{e}"
69
- # ignore
70
- end
71
- end
72
- )
73
- t.daemon = true
74
- t.start
75
- queue.pop # blocks until the run! callback runs
76
- end
77
-
78
- config.before(:suite) do
79
- sinatra_run_wait(TestApp, port: PORT, server: 'webrick')
80
- puts "Test webserver on port #{PORT}"
81
- end
82
- end
22
+ require 'net/http'
23
+ require 'json'
83
24
 
84
25
  describe LogStash::Outputs::Dynatrace do
85
- let(:port) { PORT }
86
- let(:event) do
87
- LogStash::Event.new({ 'message' => 'hi' })
26
+ let(:events) do
27
+ [
28
+ LogStash::Event.new({ 'message' => 'message 1', '@timestamp' => "2021-06-25T15:46:45.693Z" }),
29
+ LogStash::Event.new({ 'message' => 'message 2', '@timestamp' => "2021-06-25T15:46:46.693Z" }),
30
+ ]
88
31
  end
89
- let(:url) { "http://localhost:#{port}/good" }
32
+ let(:url) { "http://localhost/good" }
90
33
  let(:key) { 'api.key' }
91
34
  let(:subject) { LogStash::Outputs::Dynatrace.new({ 'api_key' => key, 'ingest_endpoint_url' => url }) }
92
35
 
93
36
  before do
94
37
  subject.register
95
- allow(subject).to receive(:log_failure).with(any_args)
38
+ subject.plugin_version = "1.2.3"
96
39
  end
97
40
 
98
- context 'sending no events' do
99
- it 'does not crash on empty events' do
100
- subject.multi_receive([])
101
- end
41
+ it 'does not send empty events' do
42
+ allow(subject).to receive(:send)
43
+ subject.multi_receive([])
44
+ expect(subject).to_not have_received(:send)
102
45
  end
103
46
 
104
- context 'with passing requests' do
105
- before do
106
- TestApp.clear
107
- subject.multi_receive([event])
47
+ context 'server response success' do
48
+ it 'sends events' do
49
+ allow(subject).to receive(:send) do |req|
50
+ body = JSON.parse(req.body)
51
+ expect(body.length).to eql(2)
52
+ expect(body[0]['message']).to eql('message 1')
53
+ expect(body[0]['@timestamp']).to eql('2021-06-25T15:46:45.693Z')
54
+ expect(body[1]['message']).to eql('message 2')
55
+ expect(body[1]['@timestamp']).to eql('2021-06-25T15:46:46.693Z')
56
+ Net::HTTPOK.new "1.1", "200", "OK"
57
+ end
58
+ subject.multi_receive(events)
59
+ expect(subject).to have_received(:send)
108
60
  end
109
61
 
110
- let(:last_request) { TestApp.last_request }
111
- let(:body) { last_request.body.read }
112
- let(:content_type) { last_request.env['CONTENT_TYPE'] }
113
- let(:authorization) { last_request.env['HTTP_AUTHORIZATION'] }
114
-
115
- let(:expected_body) { "#{LogStash::Json.dump([event])}\n" }
116
- let(:expected_content_type) { 'application/json; charset=utf-8' }
117
- let(:expected_authorization) { "Api-Token #{key}" }
118
-
119
- it 'should not log a failure' do
120
- expect(subject).not_to have_received(:log_failure).with(any_args)
62
+ it 'includes authorization header' do
63
+ allow(subject).to receive(:send) do |req|
64
+ expect(req['Authorization']).to eql("Api-Token #{key}")
65
+ Net::HTTPOK.new "1.1", "200", "OK"
66
+ end
67
+ subject.multi_receive(events)
68
+ expect(subject).to have_received(:send)
121
69
  end
122
70
 
123
- it 'should receive the request' do
124
- expect(last_request).to be_truthy
71
+ it 'includes content type header' do
72
+ allow(subject).to receive(:send) do |req|
73
+ expect(req['Content-Type']).to eql('application/json; charset=utf-8')
74
+ Net::HTTPOK.new "1.1", "200", "OK"
75
+ end
76
+ subject.multi_receive(events)
77
+ expect(subject).to have_received(:send)
125
78
  end
126
79
 
127
- it 'should receive the event as a hash' do
128
- expect(body).to eql(expected_body)
80
+ it 'includes user agent' do
81
+ allow(subject).to receive(:send) do |req|
82
+ expect(req['User-Agent']).to eql('logstash-output-dynatrace v1.2.3')
83
+ Net::HTTPOK.new "1.1", "200", "OK"
84
+ end
85
+ subject.multi_receive(events)
86
+ expect(subject).to have_received(:send)
129
87
  end
130
88
 
131
- it 'should have the correct content type' do
132
- expect(content_type).to eql(expected_content_type)
89
+ it 'does not log on success' do
90
+ allow(subject.logger).to receive(:debug) { raise "should not log" }
91
+ allow(subject.logger).to receive(:info) { raise "should not log" }
92
+ allow(subject.logger).to receive(:error) { raise "should not log" }
93
+ allow(subject.logger).to receive(:warn) { raise "should not log" }
94
+ allow(subject).to receive(:send) do |req|
95
+ Net::HTTPOK.new "1.1", "200", "OK"
96
+ end
97
+ subject.multi_receive(events)
98
+ expect(subject).to have_received(:send)
133
99
  end
100
+ end
134
101
 
135
- it 'should have the correct authorization' do
136
- expect(authorization).to eql(expected_authorization)
102
+ context 'with bad client request' do
103
+ it 'does not retry on 404' do
104
+ allow(subject).to receive(:send) { Net::HTTPNotFound.new "1.1", "404", "Not Found" }
105
+ subject.multi_receive(events)
106
+ expect(subject).to have_received(:send).once
137
107
  end
138
108
  end
139
109
 
140
- context 'with failing requests' do
141
- let(:url) { "http://localhost:#{port}/bad" }
110
+ context 'with server error' do
111
+ it 'retries 5 times with exponential backoff' do
112
+ allow(subject).to receive(:sleep)
113
+ allow(subject).to receive(:send) { Net::HTTPInternalServerError.new "1.1", "500", "Internal Server Error" }
142
114
 
143
- before do
144
- subject.multi_receive([event])
145
- end
115
+ subject.multi_receive(events)
116
+
117
+ expect(subject).to have_received(:sleep).with(1).ordered
118
+ expect(subject).to have_received(:sleep).with(2).ordered
119
+ expect(subject).to have_received(:sleep).with(4).ordered
120
+ expect(subject).to have_received(:sleep).with(8).ordered
121
+ expect(subject).to have_received(:sleep).with(16).ordered
146
122
 
147
- it 'should log a failure' do
148
- expect(subject).to have_received(:log_failure).with(any_args)
123
+ expect(subject).to have_received(:sleep).exactly(5).times
124
+ expect(subject).to have_received(:send).exactly(6).times
149
125
  end
150
126
  end
151
127
  end
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.1.0
4
+ version: 0.2.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: 2021-06-16 00:00:00.000000000 Z
11
+ date: 2021-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logstash-codec-json
@@ -152,6 +152,7 @@ executables: []
152
152
  extensions: []
153
153
  extra_rdoc_files: []
154
154
  files:
155
+ - CHANGELOG.md
155
156
  - Gemfile
156
157
  - LICENSE
157
158
  - README.md