logstash-output-dynatrace 0.2.1 → 0.3.1

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: 0a8f5b5423d993317f8f73d5416c7c17b2961c833ca95da455f2f4742bcbe81b
4
- data.tar.gz: 249eaf43d553a14a30ee84328514d6771acba98c8ef4da3bee738169b73008ec
3
+ metadata.gz: 06c22415ca3c16b7214c6aca478edd757db1302dff08fd706e80402bd3627392
4
+ data.tar.gz: af603917cd20c72d12d98add8409e1d171e7ca2edc632d42378f8a54f5a018a5
5
5
  SHA512:
6
- metadata.gz: bdb3df6d43d9e2361958b925830af0ec1f686fa02d2d9adbd6488e063e890885aae42589cf5d60e4a2b53c89b6d4a121e9dff1b3500fe5773c1a1cf8203d415a
7
- data.tar.gz: 20525e9f0cea6acb310656de596404e086ac755508dfa6d603a0d7b6f1dfb030c36ec32db60cf89fbe7942732c14dd2c0fbea375df5874c981d5805c16ce67a3
6
+ metadata.gz: 0c5b60e14c175f9e7937c79131f872768034e191df4c12e9c58b88abcb190208148fb77d76c2d93e42aff8a2599e1b89621f165740910b0ee0561273f529fcc4
7
+ data.tar.gz: ad04bc45337935f619619bef6765dee1335285fc7fa705147bfdce7888f88158c829f62ad1a84f4a26c729710292143ffd8efa217954ce5c1bc9f4c458a64abe
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 0.3.1
2
+ - Log and re-raise unknown errors
3
+ - Use [password](https://www.elastic.co/guide/en/logstash/current/configuration-file-structure.html#password) type for `api_key` configuration
4
+
5
+ ## 0.3.0
6
+ - Log response bodies on client errors
7
+
1
8
  ## 0.2.0
2
9
  - Add retries with exponential backoff (#8)
3
10
 
@@ -19,7 +19,7 @@ require 'logstash/outputs/base'
19
19
  require 'logstash/json'
20
20
 
21
21
  MAX_RETRIES = 5
22
- PLUGIN_VERSION = '0.2.1'
22
+ PLUGIN_VERSION = '0.3.1'
23
23
 
24
24
  module LogStash
25
25
  module Outputs
@@ -36,7 +36,7 @@ module LogStash
36
36
  config :ingest_endpoint_url, validate: :uri, required: true
37
37
 
38
38
  # The API token to use to authenticate requests to the log ingestion endpoint. Must have `logs.ingest` (Ingest Logs) scope.
39
- config :api_key, validate: :string, required: true
39
+ config :api_key, validate: :password, required: true
40
40
 
41
41
  # Disable SSL validation by setting :verify_mode OpenSSL::SSL::VERIFY_NONE
42
42
  config :ssl_verify_none, validate: :boolean, default: false
@@ -61,9 +61,9 @@ module LogStash
61
61
 
62
62
  def headers
63
63
  {
64
- 'User-Agent' => "logstash-output-dynatrace v#{PLUGIN_VERSION}",
64
+ 'User-Agent' => "logstash-output-dynatrace/#{PLUGIN_VERSION}",
65
65
  'Content-Type' => 'application/json; charset=utf-8',
66
- 'Authorization' => "Api-Token #{@api_key}"
66
+ 'Authorization' => "Api-Token #{@api_key.value}"
67
67
  }
68
68
  end
69
69
 
@@ -76,29 +76,26 @@ module LogStash
76
76
  begin
77
77
  request = Net::HTTP::Post.new(uri, headers)
78
78
  request.body = "#{LogStash::Json.dump(events.map(&:to_hash)).chomp}\n"
79
- response = send(request)
80
- return if response.is_a? Net::HTTPSuccess
81
-
82
- failure_message = "Dynatrace returned #{response.code} #{response.message}."
83
-
84
- if response.is_a? Net::HTTPServerError
85
- raise RetryableError.new failure_message
86
- end
87
-
88
- if response.is_a? Net::HTTPNotFound
89
- @logger.error("#{failure_message} Please check that log ingest is enabled and your API token has the `logs.ingest` (Ingest Logs) scope.")
90
- return
79
+ response = @client.request(request)
80
+
81
+ case response
82
+ when Net::HTTPSuccess
83
+ @logger.debug("successfully sent #{events.length} events#{" with #{retries} retries" if retries > 0}")
84
+ when Net::HTTPServerError
85
+ @logger.error("Encountered an HTTP server error", :message => response.message, :code => response.code, :body => response.body) if retries == 0
86
+ when Net::HTTPNotFound
87
+ @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)
88
+ when Net::HTTPClientError
89
+ @logger.error("Encountered an HTTP client error", :message => response.message, :code => response.code, :body => response.body)
90
+ else
91
+ @logger.error("Encountered an unexpected response code", :message => response.message, :code => response.code)
91
92
  end
92
93
 
93
- if response.is_a? Net::HTTPClientError
94
- @logger.error(failure_message)
95
- return
96
- end
94
+ raise RetryableError.new "code #{response.code}" if retryable(response)
97
95
 
98
- @logger.debug("successfully sent #{events.length} events")
99
96
  rescue Net::HTTPBadResponse, RetryableError => e
100
97
  # indicates a protocol error
101
- if retries < MAX_RETRIES
98
+ if retries < MAX_RETRIES
102
99
  sleep_seconds = 2 ** retries
103
100
  @logger.warn("Failed to contact dynatrace: #{e.message}. Trying again after #{sleep_seconds} seconds.")
104
101
  sleep sleep_seconds
@@ -108,13 +105,14 @@ module LogStash
108
105
  @logger.error("Failed to export logs to Dynatrace.")
109
106
  return
110
107
  end
108
+ rescue StandardError => e
109
+ @logger.error("Unknown error raised", :error => e.inspect)
110
+ raise e
111
111
  end
112
-
113
- @logger.debug("Successfully exported #{events.length} events with #{retries} retries")
114
112
  end
115
113
 
116
- def send(request)
117
- @client.request(request)
114
+ def retryable(response)
115
+ return response.is_a? Net::HTTPServerError
118
116
  end
119
117
  end
120
118
  end
@@ -43,11 +43,8 @@ Gem::Specification.new do |s|
43
43
  s.add_runtime_dependency 'logstash-codec-json'
44
44
  s.add_runtime_dependency 'logstash-core-plugin-api', '>= 2.0.0', '< 3'
45
45
 
46
- s.add_development_dependency 'insist'
47
46
  s.add_development_dependency 'logstash-devutils'
48
47
  s.add_development_dependency 'logstash-input-generator'
49
- s.add_development_dependency 'sinatra'
50
- s.add_development_dependency 'webrick'
51
48
 
52
49
  s.add_development_dependency 'rubocop', '1.9.1'
53
50
  s.add_development_dependency 'rubocop-rake', '0.5.1'
@@ -18,8 +18,6 @@ require_relative '../spec_helper'
18
18
  require_relative '../../version'
19
19
  require 'logstash/codecs/plain'
20
20
  require 'logstash/event'
21
- require 'sinatra'
22
- require 'insist'
23
21
  require 'net/http'
24
22
  require 'json'
25
23
 
@@ -32,96 +30,122 @@ describe LogStash::Outputs::Dynatrace do
32
30
  end
33
31
  let(:url) { "http://localhost/good" }
34
32
  let(:key) { 'api.key' }
33
+
35
34
  let(:subject) { LogStash::Outputs::Dynatrace.new({ 'api_key' => key, 'ingest_endpoint_url' => url }) }
35
+ let(:client) { subject.instance_variable_get(:@client) }
36
+
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" }
41
+
42
+ let(:body) { "this is a failure" }
36
43
 
37
44
  before do
38
45
  subject.register
39
46
  end
40
47
 
41
48
  it 'does not send empty events' do
42
- allow(subject).to receive(:send)
49
+ expect(client).to_not receive(:request)
43
50
  subject.multi_receive([])
44
- expect(subject).to_not have_received(:send)
45
51
  end
46
52
 
47
53
  context 'server response success' do
48
54
  it 'sends events' do
49
- allow(subject).to receive(:send) do |req|
55
+ expect(client).to receive(:request) do |req|
50
56
  body = JSON.parse(req.body)
51
57
  expect(body.length).to eql(2)
52
58
  expect(body[0]['message']).to eql('message 1')
53
59
  expect(body[0]['@timestamp']).to eql('2021-06-25T15:46:45.693Z')
54
60
  expect(body[1]['message']).to eql('message 2')
55
61
  expect(body[1]['@timestamp']).to eql('2021-06-25T15:46:46.693Z')
56
- Net::HTTPOK.new "1.1", "200", "OK"
62
+ ok
57
63
  end
58
64
  subject.multi_receive(events)
59
- expect(subject).to have_received(:send)
60
65
  end
61
66
 
62
67
  it 'includes authorization header' do
63
- allow(subject).to receive(:send) do |req|
68
+ expect(client).to receive(:request) do |req|
64
69
  expect(req['Authorization']).to eql("Api-Token #{key}")
65
- Net::HTTPOK.new "1.1", "200", "OK"
70
+ ok
66
71
  end
67
72
  subject.multi_receive(events)
68
- expect(subject).to have_received(:send)
69
73
  end
70
74
 
71
75
  it 'includes content type header' do
72
- allow(subject).to receive(:send) do |req|
76
+ expect(client).to receive(:request) do |req|
73
77
  expect(req['Content-Type']).to eql('application/json; charset=utf-8')
74
- Net::HTTPOK.new "1.1", "200", "OK"
78
+ ok
75
79
  end
76
80
  subject.multi_receive(events)
77
- expect(subject).to have_received(:send)
78
81
  end
79
82
 
80
83
  it 'includes user agent' do
81
- allow(subject).to receive(:send) do |req|
82
- expect(req['User-Agent']).to eql("logstash-output-dynatrace v#{::DynatraceConstants::VERSION}")
83
- Net::HTTPOK.new "1.1", "200", "OK"
84
+ expect(client).to receive(:request) do |req|
85
+ expect(req['User-Agent']).to eql("logstash-output-dynatrace/#{::DynatraceConstants::VERSION}")
86
+ ok
84
87
  end
85
88
  subject.multi_receive(events)
86
- expect(subject).to have_received(:send)
87
89
  end
88
90
 
89
91
  it 'does not log on success' do
90
92
  allow(subject.logger).to receive(:debug)
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
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
97
  subject.multi_receive(events)
98
- expect(subject).to have_received(:send)
99
98
  end
100
99
  end
101
100
 
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" }
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
107
+
108
+
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
114
+
115
+ expect(subject.logger).to receive(:error).with("Failed to export logs to Dynatrace.")
105
116
  subject.multi_receive(events)
106
- expect(subject).to have_received(:send).once
107
117
  end
108
118
  end
109
119
 
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" }
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)
125
+ end
126
+
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 }
131
+
132
+ expect(subject.logger).to receive(:error).with("Encountered an HTTP client error",
133
+ {:body=>body, :code=>"400", :message=> "Client error"})
114
134
 
115
135
  subject.multi_receive(events)
136
+ end
137
+ end
116
138
 
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
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
145
+ end
122
146
 
123
- expect(subject).to have_received(:sleep).exactly(5).times
124
- expect(subject).to have_received(:send).exactly(6).times
147
+ expect(subject.logger).to receive(:error)
148
+ expect { subject.multi_receive(BadEvents.new) }.to raise_error(StandardError)
125
149
  end
126
150
  end
127
151
  end
data/version.rb CHANGED
@@ -16,5 +16,5 @@
16
16
 
17
17
  module DynatraceConstants
18
18
  # Also required to change the version in lib/logstash/outputs/dynatrace.rb
19
- VERSION = '0.2.1'
19
+ VERSION = '0.3.1'
20
20
  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.2.1
4
+ version: 0.3.1
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: 2022-01-14 00:00:00.000000000 Z
11
+ date: 2022-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logstash-codec-json
@@ -44,20 +44,6 @@ dependencies:
44
44
  - - "<"
45
45
  - !ruby/object:Gem::Version
46
46
  version: '3'
47
- - !ruby/object:Gem::Dependency
48
- name: insist
49
- requirement: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- type: :development
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0'
61
47
  - !ruby/object:Gem::Dependency
62
48
  name: logstash-devutils
63
49
  requirement: !ruby/object:Gem::Requirement
@@ -86,34 +72,6 @@ dependencies:
86
72
  - - ">="
87
73
  - !ruby/object:Gem::Version
88
74
  version: '0'
89
- - !ruby/object:Gem::Dependency
90
- name: sinatra
91
- requirement: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - ">="
94
- - !ruby/object:Gem::Version
95
- version: '0'
96
- type: :development
97
- prerelease: false
98
- version_requirements: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - ">="
101
- - !ruby/object:Gem::Version
102
- version: '0'
103
- - !ruby/object:Gem::Dependency
104
- name: webrick
105
- requirement: !ruby/object:Gem::Requirement
106
- requirements:
107
- - - ">="
108
- - !ruby/object:Gem::Version
109
- version: '0'
110
- type: :development
111
- prerelease: false
112
- version_requirements: !ruby/object:Gem::Requirement
113
- requirements:
114
- - - ">="
115
- - !ruby/object:Gem::Version
116
- version: '0'
117
75
  - !ruby/object:Gem::Dependency
118
76
  name: rubocop
119
77
  requirement: !ruby/object:Gem::Requirement