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 +4 -4
- data/CHANGELOG.md +5 -0
- data/VERSION +1 -1
- data/lib/logstash/outputs/dynatrace.rb +43 -13
- data/spec/outputs/dynatrace_spec.rb +76 -100
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 74790f2358ebc0f61dbc88da2c1af0beed1ac35a3a0f021a18e1d365e7751e3a
|
4
|
+
data.tar.gz: 4d8cbb3c59a4bd6224457f49c25f01932686662551c7642f6fc65047663413e7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f970e3aa82cb27219224b241c3b30d3e0c5e3a6a70cec220ce3d7952507ab9a7a1d5c207f22602385871baee310ec3fe3b291c1f8e486462656d5c75b60cbf1
|
7
|
+
data.tar.gz: dd03de995d2038dfcf533fa050b6ceb16dd627a5e3258c95528e96eaf4b345efacb92f3cc81e922c3af604732f4ed532342e245bc0e1d74d4a3592394746ec50
|
data/CHANGELOG.md
ADDED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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
|
-
|
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(:
|
86
|
-
|
87
|
-
|
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
|
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
|
-
|
38
|
+
subject.plugin_version = "1.2.3"
|
96
39
|
end
|
97
40
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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 '
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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 '
|
124
|
-
|
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 '
|
128
|
-
|
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 '
|
132
|
-
|
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
|
-
|
136
|
-
|
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
|
141
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
148
|
-
expect(subject).to have_received(:
|
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.
|
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-
|
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
|