logstash-output-newrelic 2.0.0.pre.beta-java

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.
@@ -0,0 +1,276 @@
1
+ # encoding: utf-8
2
+ require "logstash/outputs/base"
3
+ require "logstash/outputs/newrelic_version/version"
4
+ require 'manticore'
5
+ require 'uri'
6
+ require 'zlib'
7
+ require 'json'
8
+ require 'java'
9
+ require 'set'
10
+ require 'stringio'
11
+ require_relative './config/bigdecimal_patch'
12
+ require_relative './exception/error'
13
+
14
+ class LogStash::Outputs::NewRelic < LogStash::Outputs::Base
15
+
16
+ RETRIABLE_CODES = Set[408, 429, 500, 502, 503, 504, 599]
17
+
18
+ MAX_PAYLOAD_SIZE_BYTES = 1_000_000
19
+
20
+ config_name "newrelic"
21
+
22
+ config :api_key, :validate => :password, :required => false
23
+ config :license_key, :validate => :password, :required => false
24
+ config :concurrent_requests, :validate => :number, :default => 1
25
+ config :base_uri, :validate => :string, :default => "https://log-api.newrelic.com/log/v1"
26
+ config :max_retries, :validate => :number, :default => 3
27
+ # Only used for E2E testing
28
+ config :custom_ca_cert, :validate => :string, :required => false
29
+
30
+ public
31
+
32
+ def register
33
+ @end_point = URI.parse(@base_uri)
34
+ if @api_key.nil? && @license_key.nil?
35
+ raise LogStash::ConfigurationError, "Must provide a license key or api key", caller
36
+ end
37
+ @logger.info("Registering logstash-output-newrelic", :version => LogStash::Outputs::NewRelicVersion::VERSION, :target => @base_uri)
38
+ auth = {
39
+ @api_key.nil? ? 'X-License-Key' : 'X-Insert-Key' =>
40
+ @api_key.nil? ? @license_key.value : @api_key.value
41
+ }
42
+ @header = {
43
+ 'X-Event-Source' => 'logs',
44
+ 'Content-Encoding' => 'gzip',
45
+ 'Content-Type' => 'application/json'
46
+ }.merge(auth).freeze
47
+
48
+ client_options = {
49
+ :pool_max => @concurrent_requests,
50
+ :pool_max_per_route => @concurrent_requests
51
+ }
52
+
53
+ # Only configure SSL if using HTTPS
54
+ if @end_point.scheme == 'https'
55
+ client_options[:ssl] = {
56
+ :verify => :default
57
+ }
58
+ # Set reasonable timeouts for the HTTP client
59
+ client_options[:connect_timeout] = 30
60
+ client_options[:socket_timeout] = 30
61
+
62
+ if !@custom_ca_cert.nil?
63
+ # Load the custom CA certificate
64
+ # For test environments with self-signed certs, disable verification
65
+ client_options[:ssl][:ca_file] = @custom_ca_cert
66
+ end
67
+ end
68
+
69
+ @client = Manticore::Client.new(client_options)
70
+
71
+ # We use a semaphore to ensure that at most there are @concurrent_requests inflight Logstash requests being processed
72
+ # by our plugin at the same time. Without this semaphore, given that @executor.submit() is an asynchronous method, it
73
+ # would cause that an unbounded amount of inflight requests may be processed by our plugin. Logstash then believes
74
+ # that our plugin has processed the request, and keeps reading more inflight requests in memory. This causes a memory
75
+ # leak and results in an OutOfMemoryError.
76
+ @executor = java.util.concurrent.Executors.newFixedThreadPool(@concurrent_requests)
77
+ @semaphore = java.util.concurrent.Semaphore.new(@concurrent_requests)
78
+ end
79
+
80
+ # Shutdown hook called by Logstash 5.x and 6.x versions during pipeline shutdown
81
+ def stop
82
+ shutdown
83
+ end
84
+
85
+ # Shutdown hook called by Logstash 7.x+ versions during pipeline shutdown
86
+ def close
87
+ shutdown
88
+ end
89
+
90
+ # Additional shutdown hook for cleanup, called by some Logstash versions
91
+ def teardown
92
+ shutdown
93
+ end
94
+
95
+ # Used by tests so that the test run can complete (background threads prevent JVM exit)
96
+ def shutdown
97
+ if @executor
98
+ @logger.info("Draining outstanding New Relic requests")
99
+ @executor.shutdown
100
+ # We want this long enough to not have threading issues
101
+ terminationWaitInSeconds = 10
102
+ terminatedInTime = @executor.awaitTermination(terminationWaitInSeconds, java.util.concurrent.TimeUnit::SECONDS)
103
+ if !terminatedInTime
104
+ raise "Did not shut down within #{terminationWaitInSeconds} seconds"
105
+ end
106
+ end
107
+
108
+ if defined?(@client) && @client
109
+ @logger.info("Closing New Relic HTTP client")
110
+ @client.close
111
+ end
112
+ end
113
+
114
+ def time_to_logstash_timestamp(time)
115
+ begin
116
+ LogStash::Timestamp.coerce(time)
117
+ rescue
118
+ nil
119
+ end
120
+ end
121
+
122
+ def to_nr_logs(logstash_events)
123
+ logstash_events.map do |logstash_event|
124
+ event_hash = logstash_event.to_hash
125
+
126
+ nr_log_message_hash = {
127
+ # non-intrinsic attributes get put into 'attributes'
128
+ :attributes => event_hash
129
+ }
130
+
131
+ # intrinsic attributes go at the top level
132
+ if event_hash['message']
133
+ nr_log_message_hash['message'] = event_hash['message']
134
+ nr_log_message_hash[:attributes].delete('message')
135
+ end
136
+ if event_hash['timestamp']
137
+ nr_log_message_hash['timestamp'] = event_hash['timestamp']
138
+ nr_log_message_hash[:attributes].delete('timestamp')
139
+ end
140
+
141
+ nr_log_message_hash
142
+ end
143
+ end
144
+
145
+ def multi_receive(logstash_events)
146
+ if logstash_events.empty?
147
+ return
148
+ end
149
+
150
+ nr_logs = to_nr_logs(logstash_events)
151
+
152
+ @logger.info("Submitting logs to New Relic", :event_count => nr_logs.length)
153
+
154
+ submit_logs_to_be_sent(nr_logs)
155
+ end
156
+
157
+ def submit_logs_to_be_sent(nr_logs)
158
+ @semaphore.acquire()
159
+ execute = @executor.java_method :submit, [java.lang.Runnable]
160
+ execute.call do
161
+ begin
162
+ package_and_send_recursively(nr_logs)
163
+ ensure
164
+ @semaphore.release()
165
+ end
166
+ end
167
+ end
168
+
169
+ def package_and_send_recursively(nr_logs)
170
+ payload = {
171
+ :common => {
172
+ :attributes => {
173
+ :plugin => {
174
+ :type => 'logstash',
175
+ :version => LogStash::Outputs::NewRelicVersion::VERSION,
176
+ }
177
+ }
178
+ },
179
+ :logs => nr_logs
180
+ }
181
+
182
+ payload_json = [payload].to_json
183
+ compressed_payload = gzip_compress(payload_json, Zlib::DEFAULT_COMPRESSION)
184
+ compressed_size = compressed_payload.bytesize
185
+ log_record_count = nr_logs.length
186
+
187
+ if compressed_size >= MAX_PAYLOAD_SIZE_BYTES && log_record_count == 1
188
+ @logger.error("Can't compress record below required maximum packet size and it will be discarded.")
189
+ elsif compressed_size >= MAX_PAYLOAD_SIZE_BYTES && log_record_count > 1
190
+ @logger.debug("Compressed payload size exceeds maximum packet size, splitting payload", :compressed_size => compressed_size)
191
+ split_index = log_record_count / 2
192
+ @logger.debug("Splitting payload", :split_index => split_index, :first_half => split_index, :second_half => log_record_count - split_index)
193
+ package_and_send_recursively(nr_logs[0...split_index])
194
+ package_and_send_recursively(nr_logs[split_index..-1])
195
+ else
196
+ nr_send(compressed_payload)
197
+ end
198
+ end
199
+
200
+ def handle_response(response)
201
+ if !(200 <= response.code && response.code < 300)
202
+ raise Error::BadResponseCodeError.new(response.code, @base_uri)
203
+ end
204
+ end
205
+
206
+ # Compresses a given payload string using GZIP.
207
+ #
208
+ # @param payload [String] The string payload to be compressed.
209
+ # @param compression_level [Integer] The GZIP compression level to use.
210
+ # @return [String] The GZIP-compressed binary string.
211
+ def gzip_compress(payload, compression_level)
212
+ string_io = StringIO.new
213
+ string_io.set_encoding("BINARY")
214
+ Zlib::GzipWriter.wrap(string_io, compression_level) do |gz|
215
+ gz.write(payload)
216
+ end
217
+ string_io.string
218
+ end
219
+
220
+ def nr_send(payload)
221
+ retries = 0
222
+ retry_duration = 1
223
+
224
+ begin
225
+ @logger.debug("Dispatching payload to New Relic", :endpoint => @base_uri, :payload_size => payload.bytesize)
226
+ response = @client.post(@base_uri, :body => payload, :headers => @header)
227
+ @logger.debug("Received response from New Relic", :code => response.code, :message => response.message)
228
+ handle_response(response)
229
+ if (retries > 0)
230
+ @logger.warn("Successfully sent logs at retry #{retries}")
231
+ else
232
+ @logger.debug("Successfully sent logs to New Relic", :response_code => response.code)
233
+ end
234
+ rescue Error::BadResponseCodeError => e
235
+ @logger.error(e.message)
236
+ if (should_retry(retries) && is_retryable_code(e))
237
+ retries += 1
238
+ sleep(retry_duration)
239
+ retry_duration *= 2
240
+ retry
241
+ end
242
+ rescue => e
243
+ # Stuff that should never happen
244
+ # For all other errors print out full issues
245
+ if (should_retry(retries))
246
+ retries += 1
247
+ @logger.warn(
248
+ "An unknown error occurred sending a bulk request to NewRelic. Retrying...",
249
+ :retries => "attempt #{retries} of #{@max_retries}",
250
+ :error_message => e.message,
251
+ :error_class => e.class.name,
252
+ :backtrace => e.backtrace
253
+ )
254
+ sleep(retry_duration)
255
+ retry_duration *= 2
256
+ retry
257
+ else
258
+ @logger.error(
259
+ "An unknown error occurred sending a bulk request to NewRelic. Maximum of attempts reached, dropping logs.",
260
+ :error_message => e.message,
261
+ :error_class => e.class.name,
262
+ :backtrace => e.backtrace
263
+ )
264
+ end
265
+ end
266
+ end
267
+
268
+ def should_retry(retries)
269
+ retries < @max_retries
270
+ end
271
+
272
+ def is_retryable_code(response_error)
273
+ error_code = response_error.response_code
274
+ RETRIABLE_CODES.include?(error_code)
275
+ end
276
+ end # class LogStash::Outputs::NewRelic
@@ -0,0 +1,7 @@
1
+ module LogStash
2
+ module Outputs
3
+ module NewRelicVersion
4
+ VERSION = "2.0.0-beta"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,36 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ require 'logstash/outputs/newrelic_version/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'logstash-output-newrelic'
8
+ s.version = LogStash::Outputs::NewRelicVersion::VERSION
9
+ s.licenses = ['Apache-2.0']
10
+ s.summary = "Sends Logstash events to New Relic"
11
+ s.homepage = 'https://github.com/newrelic/logstash-output-plugin'
12
+ s.authors = ['New Relic Logging Team']
13
+ s.email = 'logging-team@newrelic.com'
14
+ s.require_paths = ['lib']
15
+
16
+ # This is a Logstash plugin and requires JRuby
17
+ s.platform = 'java'
18
+
19
+ # Files
20
+ s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE','NOTICE.TXT']
21
+ # Tests
22
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
23
+
24
+ # Special flag to let us know this is actually a logstash plugin
25
+ s.metadata = { "logstash_plugin" => "true", "logstash_group" => "output" }
26
+
27
+ # Gem dependencies
28
+ s.add_runtime_dependency "logstash-core-plugin-api", "~> 2.0"
29
+ s.add_runtime_dependency "logstash-codec-plain"
30
+ s.add_runtime_dependency "manticore"
31
+ s.add_development_dependency "logstash-devutils"
32
+ s.add_development_dependency "webmock"
33
+ s.add_development_dependency "rspec"
34
+ s.add_development_dependency "rspec-wait"
35
+ s.add_development_dependency "rspec_junit_formatter"
36
+ end