scout_apm_logging 1.0.3 → 1.1.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: b0d9d5323e07f8b2fe838a030d93123d00a5fb6e289fe7bc78d7ac0bcc2ebc79
4
- data.tar.gz: e00d25c837617410f02ec4d6523fd5afdbb37efa633b3ecaf625f5929a1b960f
3
+ metadata.gz: ae315d1586cbc29835019dd459208978f033398a7427dedd7dd36b49c7fd6296
4
+ data.tar.gz: 734acfcbaae4e9a424710889d30a2cde7cc6904d4e56d304ac173a327e8c1fd0
5
5
  SHA512:
6
- metadata.gz: 0d126cf0149770bb461251442e7cf2fdb40c536076d3d09131ad3d0df3763e10fb5ec4bb44ceb780b11851ab4c2276934b476033d77bcbb7fa8db7e89eed0339
7
- data.tar.gz: 2c441b2bf745ca77aa154d44a536509de405051b8c363d9d8ccb95a6828fa77110b0bc3e196cf3044e7d0e1b837b295de855b428c64e8e6c7fa39d910fcb3d15
6
+ metadata.gz: 36adc77a85764f440b3bf8170528c28d6957e82c8e729bc03dd74a533f0c9d9c4a6231e5aae35dbad5d92a2cb77f446f004770c5998cb4559220899c147cd239
7
+ data.tar.gz: d89fdad7d8d5efe7678055180fe28e3217b5adbd8cdcbfb70cee68fd93ee9e9c8b77ed56c6e19088df8919c55dbbd8a60a69e6dac5f4b597047271399d9f4f9b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ ## 1.1.0
2
+ * Bump vendored SDK version to 0.2.0.
3
+
1
4
  ## 1.0.3
2
5
  * Add capturing of queue for background jobs.
3
6
  * Fix entrypoint name capturing for namespaced controllers.
@@ -36,7 +36,7 @@ module ScoutApm
36
36
  ).on_emit(
37
37
  severity_text: severity,
38
38
  severity_number: ::Logger::Severity.const_get(severity),
39
- attributes: attributes_to_log,
39
+ attributes: attributes_to_log.transform_keys(&:to_s),
40
40
  timestamp: time,
41
41
  body: msg,
42
42
  context: ::OpenTelemetry::Context.current
@@ -27,52 +27,53 @@ module ScoutApm
27
27
  SUCCESS = OpenTelemetry::SDK::Logs::Export::SUCCESS
28
28
  FAILURE = OpenTelemetry::SDK::Logs::Export::FAILURE
29
29
  private_constant(:SUCCESS, :FAILURE)
30
-
30
+
31
31
  # Default timeouts in seconds.
32
32
  KEEP_ALIVE_TIMEOUT = 30
33
33
  RETRY_COUNT = 5
34
- WRITE_TIMEOUT_SUPPORTED = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.6')
35
- private_constant(:KEEP_ALIVE_TIMEOUT, :RETRY_COUNT, :WRITE_TIMEOUT_SUPPORTED)
36
-
34
+ private_constant(:KEEP_ALIVE_TIMEOUT, :RETRY_COUNT)
35
+
37
36
  ERROR_MESSAGE_INVALID_HEADERS = 'headers must be a String with comma-separated URL Encoded UTF-8 k=v pairs or a Hash'
38
37
  private_constant(:ERROR_MESSAGE_INVALID_HEADERS)
39
-
38
+
40
39
  DEFAULT_USER_AGENT = "OTel-OTLP-Exporter-Ruby/#{OpenTelemetry::Exporter::OTLP::VERSION} Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM}; #{RUBY_ENGINE}/#{RUBY_ENGINE_VERSION})".freeze
41
-
40
+
42
41
  def self.ssl_verify_mode
43
- if ENV.key?('OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER')
42
+ if ENV['OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER'] == 'true'
44
43
  OpenSSL::SSL::VERIFY_PEER
45
- elsif ENV.key?('OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE')
44
+ elsif ENV['OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE'] == 'true'
46
45
  OpenSSL::SSL::VERIFY_NONE
47
46
  else
48
47
  OpenSSL::SSL::VERIFY_PEER
49
48
  end
50
49
  end
51
-
50
+
52
51
  def initialize(endpoint: ::OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT', 'OTEL_EXPORTER_OTLP_ENDPOINT', default: 'http://localhost:4318/v1/logs'),
53
- certificate_file: ::OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CERTIFICATE'),
54
- ssl_verify_mode: LogsExporter.ssl_verify_mode,
55
- headers: ::OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_HEADERS', 'OTEL_EXPORTER_OTLP_HEADERS', default: {}),
56
- compression: ::OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_COMPRESSION', 'OTEL_EXPORTER_OTLP_COMPRESSION', default: 'gzip'),
57
- timeout: ::OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_TIMEOUT', 'OTEL_EXPORTER_OTLP_TIMEOUT', default: 10))
58
- raise ArgumentError, "invalid url for OTLP::Exporter #{endpoint}" unless ::OpenTelemetry::Common::Utilities.valid_url?(endpoint)
52
+ certificate_file: ::OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CERTIFICATE'),
53
+ client_certificate_file: ::OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE'),
54
+ client_key_file: ::OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY', 'OTEL_EXPORTER_OTLP_CLIENT_KEY'),
55
+ ssl_verify_mode: LogsExporter.ssl_verify_mode,
56
+ headers: ::OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_HEADERS', 'OTEL_EXPORTER_OTLP_HEADERS', default: {}),
57
+ compression: ::OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_COMPRESSION', 'OTEL_EXPORTER_OTLP_COMPRESSION', default: 'gzip'),
58
+ timeout: ::OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_TIMEOUT', 'OTEL_EXPORTER_OTLP_TIMEOUT', default: 10))
59
+ raise ArgumentError, "invalid url for OTLP::Logs::LogsExporter #{endpoint}" unless ::OpenTelemetry::Common::Utilities.valid_url?(endpoint)
59
60
  raise ArgumentError, "unsupported compression key #{compression}" unless compression.nil? || %w[gzip none].include?(compression)
60
-
61
+
61
62
  @uri = if endpoint == ENV['OTEL_EXPORTER_OTLP_ENDPOINT']
62
- URI.join(endpoint, 'v1/logs')
63
- else
64
- URI(endpoint)
65
- end
66
-
67
- @http = http_connection(@uri, ssl_verify_mode, certificate_file)
68
-
63
+ URI.join(endpoint, 'v1/logs')
64
+ else
65
+ URI(endpoint)
66
+ end
67
+
68
+ @http = http_connection(@uri, ssl_verify_mode, certificate_file, client_certificate_file, client_key_file)
69
+
69
70
  @path = @uri.path
70
71
  @headers = prepare_headers(headers)
71
72
  @timeout = timeout.to_f
72
73
  @compression = compression
73
74
  @shutdown = false
74
75
  end
75
-
76
+
76
77
  # Called to export sampled {OpenTelemetry::SDK::Logs::LogRecordData} structs.
77
78
  #
78
79
  # @param [Enumerable<OpenTelemetry::SDK::Logs::LogRecordData>] log_record_data the
@@ -83,10 +84,10 @@ module ScoutApm
83
84
  def export(log_record_data, timeout: nil)
84
85
  OpenTelemetry.logger.error('Logs Exporter tried to export, but it has already shut down') if @shutdown
85
86
  return FAILURE if @shutdown
86
-
87
+
87
88
  send_bytes(encode(log_record_data), timeout: timeout)
88
89
  end
89
-
90
+
90
91
  # Called when {OpenTelemetry::SDK::Logs::LoggerProvider#force_flush} is called, if
91
92
  # this exporter is registered to a {OpenTelemetry::SDK::Logs::LoggerProvider}
92
93
  # object.
@@ -95,7 +96,7 @@ module ScoutApm
95
96
  def force_flush(timeout: nil)
96
97
  SUCCESS
97
98
  end
98
-
99
+
99
100
  # Called when {OpenTelemetry::SDK::Logs::LoggerProvider#shutdown} is called, if
100
101
  # this exporter is registered to a {OpenTelemetry::SDK::Logs::LoggerProvider}
101
102
  # object.
@@ -106,18 +107,24 @@ module ScoutApm
106
107
  @http.finish if @http.started?
107
108
  SUCCESS
108
109
  end
109
-
110
+
110
111
  private
111
-
112
- def http_connection(uri, ssl_verify_mode, certificate_file)
112
+
113
+ def handle_http_error(response)
114
+ OpenTelemetry.handle_error(message: "OTLP logs exporter received #{response.class.name}, http.code=#{response.code}, for uri: '#{@path}'")
115
+ end
116
+
117
+ def http_connection(uri, ssl_verify_mode, certificate_file, client_certificate_file, client_key_file)
113
118
  http = Net::HTTP.new(uri.host, uri.port)
114
119
  http.use_ssl = uri.scheme == 'https'
115
120
  http.verify_mode = ssl_verify_mode
116
121
  http.ca_file = certificate_file unless certificate_file.nil?
122
+ http.cert = OpenSSL::X509::Certificate.new(File.read(client_certificate_file)) unless client_certificate_file.nil?
123
+ http.key = OpenSSL::PKey::RSA.new(File.read(client_key_file)) unless client_key_file.nil?
117
124
  http.keep_alive_timeout = KEEP_ALIVE_TIMEOUT
118
125
  http
119
126
  end
120
-
127
+
121
128
  # The around_request is a private method that provides an extension
122
129
  # point for the exporters network calls. The default behaviour
123
130
  # is to not record these operations.
@@ -128,10 +135,10 @@ module ScoutApm
128
135
  def around_request
129
136
  ::OpenTelemetry::Common::Utilities.untraced { yield } # rubocop:disable Style/ExplicitBlockArgument
130
137
  end
131
-
138
+
132
139
  def send_bytes(bytes, timeout:) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
133
140
  return FAILURE if bytes.nil?
134
-
141
+
135
142
  request = Net::HTTP::Post.new(@path)
136
143
  if @compression == 'gzip'
137
144
  request.add_field('Content-Encoding', 'gzip')
@@ -139,39 +146,41 @@ module ScoutApm
139
146
  else
140
147
  body = bytes
141
148
  end
142
-
149
+
143
150
  request.body = body
144
151
  request.add_field('Content-Type', 'application/x-protobuf')
145
152
  @headers.each { |key, value| request.add_field(key, value) }
146
-
153
+
147
154
  retry_count = 0
148
155
  timeout ||= @timeout
149
156
  start_time = ::OpenTelemetry::Common::Utilities.timeout_timestamp
150
-
157
+
151
158
  around_request do
152
159
  remaining_timeout = ::OpenTelemetry::Common::Utilities.maybe_timeout(timeout, start_time)
153
160
  return FAILURE if remaining_timeout.zero?
154
-
161
+
155
162
  @http.open_timeout = remaining_timeout
156
163
  @http.read_timeout = remaining_timeout
157
- @http.write_timeout = remaining_timeout if WRITE_TIMEOUT_SUPPORTED
164
+ @http.write_timeout = remaining_timeout
158
165
  @http.start unless @http.started?
159
- response = measure_request_duration { @http.request(request) }
160
-
166
+ response = @http.request(request)
167
+
161
168
  case response
162
169
  when Net::HTTPOK
163
170
  response.body # Read and discard body
164
171
  SUCCESS
165
172
  when Net::HTTPServiceUnavailable, Net::HTTPTooManyRequests
166
173
  response.body # Read and discard body
167
- redo if backoff?(retry_after: response['Retry-After'], retry_count: retry_count += 1, reason: response.code)
174
+ handle_http_error(response)
175
+ redo if backoff?(retry_after: response['Retry-After'], retry_count: retry_count += 1)
168
176
  FAILURE
169
177
  when Net::HTTPRequestTimeOut, Net::HTTPGatewayTimeOut, Net::HTTPBadGateway
170
178
  response.body # Read and discard body
171
- redo if backoff?(retry_count: retry_count += 1, reason: response.code)
179
+ handle_http_error(response)
180
+ redo if backoff?(retry_count: retry_count += 1)
172
181
  FAILURE
173
182
  when Net::HTTPNotFound
174
- OpenTelemetry.handle_error(message: "OTLP exporter received http.code=404 for uri: '#{@path}'")
183
+ handle_http_error(response)
175
184
  FAILURE
176
185
  when Net::HTTPBadRequest, Net::HTTPClientError, Net::HTTPServerError
177
186
  log_status(response.body)
@@ -179,28 +188,35 @@ module ScoutApm
179
188
  when Net::HTTPRedirection
180
189
  @http.finish
181
190
  handle_redirect(response['location'])
182
- redo if backoff?(retry_after: 0, retry_count: retry_count += 1, reason: response.code)
191
+ redo if backoff?(retry_after: 0, retry_count: retry_count += 1)
183
192
  else
184
193
  @http.finish
194
+ handle_http_error(response)
185
195
  FAILURE
186
196
  end
187
- rescue Net::OpenTimeout, Net::ReadTimeout
188
- retry if backoff?(retry_count: retry_count += 1, reason: 'timeout')
197
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
198
+ OpenTelemetry.handle_error(exception: e)
199
+ retry if backoff?(retry_count: retry_count += 1)
189
200
  return FAILURE
190
- rescue OpenSSL::SSL::SSLError
191
- retry if backoff?(retry_count: retry_count += 1, reason: 'openssl_error')
201
+ rescue OpenSSL::SSL::SSLError => e
202
+ OpenTelemetry.handle_error(exception: e)
203
+ retry if backoff?(retry_count: retry_count += 1)
192
204
  return FAILURE
193
- rescue SocketError
194
- retry if backoff?(retry_count: retry_count += 1, reason: 'socket_error')
205
+ rescue SocketError => e
206
+ OpenTelemetry.handle_error(exception: e)
207
+ retry if backoff?(retry_count: retry_count += 1)
195
208
  return FAILURE
196
209
  rescue SystemCallError => e
197
- retry if backoff?(retry_count: retry_count += 1, reason: e.class.name)
210
+ retry if backoff?(retry_count: retry_count += 1)
211
+ OpenTelemetry.handle_error(exception: e)
198
212
  return FAILURE
199
- rescue EOFError
200
- retry if backoff?(retry_count: retry_count += 1, reason: 'eof_error')
213
+ rescue EOFError => e
214
+ OpenTelemetry.handle_error(exception: e)
215
+ retry if backoff?(retry_count: retry_count += 1)
201
216
  return FAILURE
202
- rescue Zlib::DataError
203
- retry if backoff?(retry_count: retry_count += 1, reason: 'zlib_error')
217
+ rescue Zlib::DataError => e
218
+ OpenTelemetry.handle_error(exception: e)
219
+ retry if backoff?(retry_count: retry_count += 1)
204
220
  return FAILURE
205
221
  rescue StandardError => e
206
222
  OpenTelemetry.handle_error(exception: e, message: 'unexpected error in OTLP::Exporter#send_bytes')
@@ -210,37 +226,27 @@ module ScoutApm
210
226
  # Reset timeouts to defaults for the next call.
211
227
  @http.open_timeout = @timeout
212
228
  @http.read_timeout = @timeout
213
- @http.write_timeout = @timeout if WRITE_TIMEOUT_SUPPORTED
229
+ @http.write_timeout = @timeout
214
230
  end
215
-
231
+
216
232
  def handle_redirect(location)
217
233
  # TODO: figure out destination and reinitialize @http and @path
218
234
  end
219
-
235
+
220
236
  def log_status(body)
221
237
  status = Google::Rpc::Status.decode(body)
222
238
  details = status.details.map do |detail|
223
239
  klass_or_nil = ::Google::Protobuf::DescriptorPool.generated_pool.lookup(detail.type_name).msgclass
224
240
  detail.unpack(klass_or_nil) if klass_or_nil
225
241
  end.compact
226
- OpenTelemetry.handle_error(message: "OTLP exporter received rpc.Status{message=#{status.message}, details=#{details}}")
242
+ OpenTelemetry.handle_error(message: "OTLP logs exporter received rpc.Status{message=#{status.message}, details=#{details}}")
227
243
  rescue StandardError => e
228
244
  OpenTelemetry.handle_error(exception: e, message: 'unexpected error decoding rpc.Status in OTLP::Exporter#log_status')
229
245
  end
230
-
231
- def measure_request_duration
232
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
233
- begin
234
- yield
235
- ensure
236
- stop = Process.clock_gettime(Process::CLOCK_MONOTONIC)
237
- 1000.0 * (stop - start)
238
- end
239
- end
240
-
241
- def backoff?(retry_count:, reason:, retry_after: nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
246
+
247
+ def backoff?(retry_count:, retry_after: nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
242
248
  return false if retry_count > RETRY_COUNT
243
-
249
+
244
250
  sleep_interval = nil
245
251
  unless retry_after.nil?
246
252
  sleep_interval =
@@ -258,11 +264,11 @@ module ScoutApm
258
264
  sleep_interval = nil unless sleep_interval&.positive?
259
265
  end
260
266
  sleep_interval ||= rand(2**retry_count)
261
-
267
+
262
268
  sleep(sleep_interval)
263
269
  true
264
270
  end
265
-
271
+
266
272
  def encode(log_record_data) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
267
273
  Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.encode(
268
274
  Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.new(
@@ -292,7 +298,7 @@ module ScoutApm
292
298
  OpenTelemetry.handle_error(exception: e, message: 'unexpected error in OTLP::Exporter#encode')
293
299
  nil
294
300
  end
295
-
301
+
296
302
  def as_otlp_log_record(log_record_data)
297
303
  Opentelemetry::Proto::Logs::V1::LogRecord.new(
298
304
  time_unix_nano: log_record_data.timestamp,
@@ -307,7 +313,7 @@ module ScoutApm
307
313
  span_id: log_record_data.span_id
308
314
  )
309
315
  end
310
-
316
+
311
317
  def as_otlp_key_value(key, value)
312
318
  Opentelemetry::Proto::Common::V1::KeyValue.new(key: key, value: as_otlp_any_value(value))
313
319
  rescue Encoding::UndefinedConversionError => e
@@ -315,7 +321,7 @@ module ScoutApm
315
321
  OpenTelemetry.handle_error(exception: e, message: "encoding error for key #{key} and value #{encoded_value}")
316
322
  Opentelemetry::Proto::Common::V1::KeyValue.new(key: key, value: as_otlp_any_value('Encoding Error'))
317
323
  end
318
-
324
+
319
325
  def as_otlp_any_value(value)
320
326
  result = Opentelemetry::Proto::Common::V1::AnyValue.new
321
327
  case value
@@ -333,6 +339,22 @@ module ScoutApm
333
339
  end
334
340
  result
335
341
  end
342
+
343
+ def prepare_headers(config_headers)
344
+ headers = case config_headers
345
+ when String then parse_headers(config_headers)
346
+ when Hash then config_headers.dup
347
+ else
348
+ raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS
349
+ end
350
+
351
+ headers['User-Agent'] = "#{headers.fetch('User-Agent', '')} #{DEFAULT_USER_AGENT}".strip
352
+
353
+ headers
354
+ end
355
+
356
+ # NOTE: This has been removed in newer versions of the OpenTelemetry Ruby Logs SDK,
357
+ # but we need it for backward compatibility with the protobuf definitions.
336
358
 
337
359
  # TODO: maybe don't translate the severity number, but translate the severity text into
338
360
  # the number if the number is nil? Poss. change to allow for adding your own
@@ -347,24 +369,11 @@ module ScoutApm
347
369
  when 5 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_UNSPECIFIED
348
370
  end
349
371
  end
350
-
351
- def prepare_headers(config_headers)
352
- headers = case config_headers
353
- when String then parse_headers(config_headers)
354
- when Hash then config_headers.dup
355
- else
356
- raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS
357
- end
358
-
359
- headers['User-Agent'] = "#{headers.fetch('User-Agent', '')} #{DEFAULT_USER_AGENT}".strip
360
-
361
- headers
362
- end
363
-
372
+
364
373
  def parse_headers(raw)
365
374
  entries = raw.split(',')
366
375
  raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS if entries.empty?
367
-
376
+
368
377
  entries.each_with_object({}) do |entry, headers|
369
378
  k, v = entry.split('=', 2).map(&CGI.method(:unescape))
370
379
  begin
@@ -376,7 +385,7 @@ module ScoutApm
376
385
  raise e, ERROR_MESSAGE_INVALID_HEADERS
377
386
  end
378
387
  raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS if k.empty? || v.empty?
379
-
388
+
380
389
  headers[k] = v
381
390
  end
382
391
  end
@@ -3,7 +3,6 @@
3
3
  # Copyright The OpenTelemetry Authors
4
4
  #
5
5
  # SPDX-License-Identifier: Apache-2.0
6
-
7
6
  module ScoutApm
8
7
  module Logging
9
8
  module Loggers
@@ -41,9 +40,8 @@ module ScoutApm
41
40
  exporter_timeout: Float(ENV.fetch('OTEL_BLRP_EXPORT_TIMEOUT', 30_000)),
42
41
  schedule_delay: Float(ENV.fetch('OTEL_BLRP_SCHEDULE_DELAY', 1000)),
43
42
  max_queue_size: Integer(ENV.fetch('OTEL_BLRP_MAX_QUEUE_SIZE', 2048)),
44
- max_export_batch_size: Integer(ENV.fetch('OTEL_BLRP_MAX_EXPORT_BATCH_SIZE', 256)),
43
+ max_export_batch_size: Integer(ENV.fetch('OTEL_BLRP_MAX_EXPORT_BATCH_SIZE', 512)),
45
44
  start_thread_on_boot: String(ENV['OTEL_RUBY_BLRP_START_THREAD_ON_BOOT']) !~ /false/i)
46
-
47
45
  unless max_export_batch_size <= max_queue_size
48
46
  raise ArgumentError,
49
47
  'max_export_batch_size much be less than or equal to max_queue_size'
@@ -12,6 +12,10 @@ module ScoutApm
12
12
  module Logs
13
13
  # Implementation of OpenTelemetry::Logs::LogRecord that records log events.
14
14
  class LogRecord < OpenTelemetry::Logs::LogRecord
15
+ EMPTY_ATTRIBUTES = {}.freeze
16
+
17
+ private_constant :EMPTY_ATTRIBUTES
18
+
15
19
  attr_accessor :timestamp,
16
20
  :observed_timestamp,
17
21
  :severity_text,
@@ -52,6 +56,8 @@ module ScoutApm
52
56
  # source of the log, desrived from the LoggerProvider.
53
57
  # @param [optional OpenTelemetry::SDK::InstrumentationScope] instrumentation_scope
54
58
  # The instrumentation scope, derived from the emitting Logger
59
+ # @param [optional] OpenTelemetry::SDK::LogRecordLimits] log_record_limits
60
+ # Attribute limits
55
61
  #
56
62
  #
57
63
  # @return [LogRecord]
@@ -66,7 +72,8 @@ module ScoutApm
66
72
  span_id: nil,
67
73
  trace_flags: nil,
68
74
  resource: nil,
69
- instrumentation_scope: nil
75
+ instrumentation_scope: nil,
76
+ log_record_limits: nil
70
77
  )
71
78
  @timestamp = timestamp
72
79
  @observed_timestamp = observed_timestamp || timestamp || Time.now
@@ -79,7 +86,10 @@ module ScoutApm
79
86
  @trace_flags = trace_flags
80
87
  @resource = resource
81
88
  @instrumentation_scope = instrumentation_scope
89
+ @log_record_limits = log_record_limits || LogRecordLimits::DEFAULT
82
90
  @total_recorded_attributes = @attributes&.size || 0
91
+
92
+ trim_attributes(@attributes)
83
93
  end
84
94
 
85
95
  def to_log_record_data
@@ -106,6 +116,51 @@ module ScoutApm
106
116
 
107
117
  (timestamp.to_r * 10**9).to_i
108
118
  end
119
+
120
+ def trim_attributes(attributes)
121
+ return if attributes.nil?
122
+
123
+ # truncate total attributes
124
+ truncate_attributes(attributes, @log_record_limits.attribute_count_limit)
125
+
126
+ # truncate attribute values
127
+ truncate_attribute_values(attributes, @log_record_limits.attribute_length_limit)
128
+
129
+ # validate attributes
130
+ validate_attributes(attributes)
131
+
132
+ nil
133
+ end
134
+
135
+ def truncate_attributes(attributes, attribute_limit)
136
+ excess = attributes.size - attribute_limit
137
+ excess.times { attributes.shift } if excess.positive?
138
+ end
139
+
140
+ def validate_attributes(attrs)
141
+ # Similar to Internal.valid_attributes?, but with different messages
142
+ # Future refactor opportunity: https://github.com/open-telemetry/opentelemetry-ruby/issues/1739
143
+ attrs.keep_if do |k, v|
144
+ if !::OpenTelemetry::SDK::Internal.valid_key?(k)
145
+ OpenTelemetry.handle_error(message: "invalid log record attribute key type #{k.class}, #{k} on record: '#{body}'")
146
+ return false
147
+ elsif !::OpenTelemetry::SDK::Internal.valid_value?(v)
148
+ OpenTelemetry.handle_error(message: "invalid log record attribute value type #{v.class} for key '#{k}' on record: '#{body}'")
149
+ return false
150
+ end
151
+
152
+ true
153
+ end
154
+ end
155
+
156
+ def truncate_attribute_values(attributes, attribute_length_limit)
157
+ return EMPTY_ATTRIBUTES if attributes.nil?
158
+ return attributes if attribute_length_limit.nil?
159
+
160
+ attributes.transform_values! { |value| ::OpenTelemetry::Common::Utilities.truncate_attribute_value(value, attribute_length_limit) }
161
+
162
+ attributes
163
+ end
109
164
  end
110
165
  end
111
166
  end
@@ -16,7 +16,7 @@ module ScoutApm
16
16
  :severity_text, # optional String
17
17
  :severity_number, # optional Integer
18
18
  :body, # optional String, Numeric, Boolean, Array<String, Numeric, Boolean>, Hash{String => String, Numeric, Boolean, Array<String, Numeric, Boolean>}
19
- :attributes, # optional Hash{String => String, Numeric, Boolean, Array<String, Numeric, Boolean>}
19
+ :attributes, # optional Hash{String => String, Numeric, Boolean, Array<String, Numeric, Boolean>}
20
20
  :trace_id, # optional String (16-byte binary)
21
21
  :span_id, # optional String (8-byte binary)
22
22
  :trace_flags, # optional Integer (8-bit byte of bit flags)
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright The OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ module ScoutApm
8
+ module Logging
9
+ module Loggers
10
+ module OpenTelemetry
11
+ module SDK
12
+ module Logs
13
+ # Class that holds log record attribute limit parameters.
14
+ class LogRecordLimits
15
+ # The global default max number of attributes per {LogRecord}.
16
+ attr_reader :attribute_count_limit
17
+
18
+ # The global default max length of attribute value per {LogRecord}.
19
+ attr_reader :attribute_length_limit
20
+
21
+ # Returns a {LogRecordLimits} with the desired values.
22
+ #
23
+ # @return [LogRecordLimits] with the desired values.
24
+ # @raise [ArgumentError] if any of the max numbers are not positive.
25
+ def initialize(attribute_count_limit: Integer(::OpenTelemetry::Common::Utilities.config_opt(
26
+ 'OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT',
27
+ 'OTEL_ATTRIBUTE_COUNT_LIMIT',
28
+ default: 128
29
+ )),
30
+ attribute_length_limit: ::OpenTelemetry::Common::Utilities.config_opt(
31
+ 'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT',
32
+ 'OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT'
33
+ ))
34
+ raise ArgumentError, 'attribute_count_limit must be positive' unless attribute_count_limit.positive?
35
+ raise ArgumentError, 'attribute_length_limit must not be less than 32' unless attribute_length_limit.nil? || Integer(attribute_length_limit) >= 32
36
+
37
+ @attribute_count_limit = attribute_count_limit
38
+ @attribute_length_limit = attribute_length_limit.nil? ? nil : Integer(attribute_length_limit)
39
+ end
40
+
41
+ # The default {LogRecordLimits}.
42
+ DEFAULT = new
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -3,7 +3,6 @@
3
3
  # Copyright The OpenTelemetry Authors
4
4
  #
5
5
  # SPDX-License-Identifier: Apache-2.0
6
-
7
6
  module ScoutApm
8
7
  module Logging
9
8
  module Loggers
@@ -29,7 +28,7 @@ module ScoutApm
29
28
  # the process after an invocation, but before the `Processor` exports
30
29
  # the completed spans.
31
30
  #
32
- # @param [Numeric] timeout An optional timeout in seconds.
31
+ # @param [optional Numeric] timeout An optional timeout in seconds.
33
32
  # @return [Integer] Export::SUCCESS if no error occurred, Export::FAILURE if
34
33
  # a non-specific failure occurred, Export::TIMEOUT if a timeout occurred.
35
34
  def force_flush(timeout: nil)
@@ -38,7 +37,7 @@ module ScoutApm
38
37
 
39
38
  # Called when {LoggerProvider#shutdown} is called.
40
39
  #
41
- # @param [Numeric] timeout An optional timeout in seconds.
40
+ # @param [optional Numeric] timeout An optional timeout in seconds.
42
41
  # @return [Integer] Export::SUCCESS if no error occurred, Export::FAILURE if
43
42
  # a non-specific failure occurred, Export::TIMEOUT if a timeout occurred.
44
43
  def shutdown(timeout: nil)
@@ -37,6 +37,8 @@ module ScoutApm
37
37
  # @param [optional OpenTelemetry::Trace::SpanContext] span_context The
38
38
  # OpenTelemetry::Trace::SpanContext to associate with the
39
39
  # {LogRecord}.
40
+ # @param [optional String] severity_text Original string representation of
41
+ # the severity as it is known at the source. Also known as log level.
40
42
  # @param severity_number [optional Integer] Numerical value of the
41
43
  # severity. Smaller numerical values correspond to less severe events
42
44
  # (such as debug events), larger numerical values correspond to more
@@ -72,6 +74,8 @@ module ScoutApm
72
74
  span_id: nil,
73
75
  trace_flags: nil,
74
76
  context: ::OpenTelemetry::Context.current)
77
+ current_span = ::OpenTelemetry::Trace.current_span(context)
78
+ span_context = current_span.context unless current_span == ::OpenTelemetry::Trace::Span::INVALID
75
79
 
76
80
  @logger_provider.on_emit(timestamp: timestamp,
77
81
  observed_timestamp: observed_timestamp,
@@ -79,9 +83,9 @@ module ScoutApm
79
83
  severity_number: severity_number,
80
84
  body: body,
81
85
  attributes: attributes,
82
- trace_id: nil,
83
- span_id: nil,
84
- trace_flags: nil,
86
+ trace_id: trace_id || span_context&.trace_id,
87
+ span_id: span_id || span_context&.span_id,
88
+ trace_flags: trace_flags || span_context&.trace_flags,
85
89
  instrumentation_scope: @instrumentation_scope,
86
90
  context: context)
87
91
  end
@@ -12,6 +12,9 @@ module ScoutApm
12
12
  module Logs
13
13
  # The SDK implementation of OpenTelemetry::Logs::LoggerProvider.
14
14
  class LoggerProvider < OpenTelemetry::Logs::LoggerProvider
15
+ Key = Struct.new(:name, :version)
16
+ private_constant(:Key)
17
+
15
18
  UNEXPECTED_ERROR_MESSAGE = 'unexpected error in ' \
16
19
  'OpenTelemetry::SDK::Logs::LoggerProvider#%s'
17
20
 
@@ -21,13 +24,18 @@ module ScoutApm
21
24
  #
22
25
  # @param [optional Resource] resource The resource to associate with
23
26
  # new LogRecords created by {Logger}s created by this LoggerProvider.
27
+ # @param [optional LogRecordLimits] log_record_limits The limits for
28
+ # attributes count and attribute length for LogRecords.
24
29
  #
25
30
  # @return [OpenTelemetry::SDK::Logs::LoggerProvider]
26
- def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create)
31
+ def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create, log_record_limits: LogRecordLimits::DEFAULT)
27
32
  @log_record_processors = []
33
+ @log_record_limits = log_record_limits
28
34
  @mutex = Mutex.new
29
35
  @resource = resource
30
36
  @stopped = false
37
+ @registry = {}
38
+ @registry_mutex = Mutex.new
31
39
  end
32
40
 
33
41
  # Returns an {OpenTelemetry::SDK::Logs::Logger} instance.
@@ -44,7 +52,9 @@ module ScoutApm
44
52
  "invalid name. Name provided: #{name.inspect}")
45
53
  end
46
54
 
47
- Logger.new(name, version, self)
55
+ @registry_mutex.synchronize do
56
+ @registry[Key.new(name, version)] ||= Logger.new(name, version, self)
57
+ end
48
58
  end
49
59
 
50
60
  # Adds a new log record processor to this LoggerProvider's
@@ -134,6 +144,7 @@ module ScoutApm
134
144
  trace_flags: nil,
135
145
  instrumentation_scope: nil,
136
146
  context: nil)
147
+ return if @stopped
137
148
 
138
149
  log_record = LogRecord.new(timestamp: timestamp,
139
150
  observed_timestamp: observed_timestamp,
@@ -145,7 +156,8 @@ module ScoutApm
145
156
  span_id: span_id,
146
157
  trace_flags: trace_flags,
147
158
  resource: @resource,
148
- instrumentation_scope: instrumentation_scope)
159
+ instrumentation_scope: instrumentation_scope,
160
+ log_record_limits: @log_record_limits)
149
161
 
150
162
  @log_record_processors.each { |processor| processor.on_emit(log_record, context) }
151
163
  end
@@ -11,10 +11,10 @@ module ScoutApm
11
11
  module SDK
12
12
  module Logs
13
13
  # Current OpenTelemetry logs sdk version
14
- VERSION = '0.1.0'
14
+ VERSION = '0.2.0'
15
15
  end
16
16
  end
17
17
  end
18
18
  end
19
19
  end
20
- end
20
+ end
@@ -11,6 +11,7 @@ require_relative 'logs/log_record'
11
11
  require_relative 'logs/log_record_data'
12
12
  require_relative 'logs/log_record_processor'
13
13
  require_relative 'logs/export'
14
+ require_relative 'logs/log_record_limits'
14
15
 
15
16
  module ScoutApm
16
17
  module Logging
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ScoutApm
4
4
  module Logging
5
- VERSION = '1.0.3'
5
+ VERSION = '1.1.0'
6
6
  end
7
7
  end
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
7
7
  s.authors = 'Scout APM'
8
8
  s.email = ['support@scoutapp.com']
9
9
  s.homepage = 'https://github.com/scoutapp/scout_apm_ruby_logging'
10
- s.summary = 'Ruby Logging Support'
10
+ s.summary = 'Managed log monitoring for Ruby applications.'
11
11
  s.description = 'Sets up log monitoring for Scout APM Ruby clients.'
12
12
  s.license = 'MIT'
13
13
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_apm_logging
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scout APM
@@ -208,6 +208,7 @@ files:
208
208
  - lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/export/log_record_exporter.rb
209
209
  - lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/log_record.rb
210
210
  - lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/log_record_data.rb
211
+ - lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/log_record_limits.rb
211
212
  - lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/log_record_processor.rb
212
213
  - lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/logger.rb
213
214
  - lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/logger_provider.rb
@@ -251,7 +252,7 @@ requirements: []
251
252
  rubygems_version: 3.3.26
252
253
  signing_key:
253
254
  specification_version: 4
254
- summary: Ruby Logging Support
255
+ summary: Managed log monitoring for Ruby applications.
255
256
  test_files:
256
257
  - spec/data/config_test_1.yml
257
258
  - spec/data/mock_config.yml