logstash-output-dynatrace 0.1.0 → 0.2.0

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