scout_apm_logging 2.0.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7064d80f9892529f81a77b0e46f9e517e8ea96f8e9e53880ee2eefd6c5abbf2
4
- data.tar.gz: 3a3ec083c05435e99d23b719bbc31f38d7ce4a07ddcf25ef47fed544147ea21c
3
+ metadata.gz: e3e6660d99d9d31d6e78ba6ef801432d582a5a9fe78917d8b027a8cfae85e834
4
+ data.tar.gz: 37fd12719798598626ac9d97bfd3c13d186be2d54a0e6ac86b572ecc246c31b1
5
5
  SHA512:
6
- metadata.gz: d7ea291371355544a94b69d3ab7ad75ee7407c84d450bb3e9a44ce6aabfa6bb2c27ce5813276111fb6584268eaba59875d346bd6b36b83c4db0d153478f7ba9f
7
- data.tar.gz: ff9ee96494248654b146c2cc9d45cc7db960196781661cc94e3f79ebbabde83930b4c264e67c0270cd24e1510027242b62e307cda061b26e1b2789d1af5f7941
6
+ metadata.gz: 3ef2d0cb82f91cdfb640b4eed871e1b3505af4eead81317182bb53e53858ce009dccdc693227cafdc7ceffca21d9be3f95f089e34f4d1945a48599ca568e64bc
7
+ data.tar.gz: eadfd1cbd5ab6f204fbc5d8b087164c01ed7f532a33d5841b658143aeb0abda19c20d22dce4a2e827ebc4caca53c9c1830c83923185d8b103edc463805162cd3
@@ -0,0 +1,30 @@
1
+ name: Release Gem
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ # Directly from https://github.com/rubygems/release-gem/tree/v1
9
+ # Predicated on Tag Ruleset preventing arbitrary tag creation and
10
+ # requiring checks to pass for commit before tag creation
11
+ jobs:
12
+ push:
13
+ name: Push gem to RubyGems.org
14
+ runs-on: ubuntu-latest
15
+
16
+ permissions:
17
+ id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
18
+ contents: write # IMPORTANT: this permission is required for `rake release` to push the release tag
19
+
20
+ steps:
21
+ # Set up
22
+ - uses: actions/checkout@v4
23
+ - name: Set up Ruby
24
+ uses: ruby/setup-ruby@v1
25
+ with:
26
+ bundler-cache: true
27
+ ruby-version: ruby
28
+
29
+ # Release
30
+ - uses: rubygems/release-gem@v1
@@ -8,11 +8,11 @@ jobs:
8
8
  strategy:
9
9
  fail-fast: false
10
10
  matrix:
11
- ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4']
11
+ ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
12
12
  env:
13
13
  DOCKER_RUBY_VERSION: ${{ matrix.ruby }}
14
14
  BUNDLE_GEMFILE: gems/rails.gemfile
15
- runs-on: ubuntu-20.04
15
+ runs-on: ubuntu-latest
16
16
  steps:
17
17
  - uses: actions/checkout@v2
18
18
  - name: Extract Library Version
@@ -21,21 +21,26 @@ jobs:
21
21
  echo "LIBRARY_VERSION=$VERSION" >> $GITHUB_ENV
22
22
  - name: Set up Ruby
23
23
  uses: ruby/setup-ruby@v1
24
- if: ${{ (env.LIBRARY_VERSION == '1' && !contains(fromJson('["3.4"]'), matrix.ruby)) ||
25
- (env.LIBRARY_VERSION == '2' && !contains(fromJson('["2.6", "2.7", "3.0"]'), matrix.ruby)) }}
24
+ if: ${{ (env.LIBRARY_VERSION == '1' && !startsWith(matrix.ruby, '3.4') && !startsWith(matrix.ruby, '4.')) || (env.LIBRARY_VERSION == '2' && !startsWith(matrix.ruby, '2.6') && !startsWith(matrix.ruby, '2.7') && !startsWith(matrix.ruby, '3.0')) }}
26
25
  with:
27
26
  ruby-version: ${{ matrix.ruby }}
28
27
  bundler-cache: true
29
28
  - name: Run RSpec
30
29
  run: |
31
- if [[ "$LIBRARY_VERSION" == "1" && ("${{ matrix.ruby }}" == "3.4" ) ]]; then
32
- echo "Skipping Ruby ${{ matrix.ruby }} for Library v1.x"
30
+ RUBY="${{ matrix.ruby }}"
31
+
32
+ # Library v1: block Ruby >= 3.4 (3.4, 4.x, 5.x, ...)
33
+ if [[ "$LIBRARY_VERSION" == "1" && ( "$RUBY" == 3.4* || "$RUBY" == 4.* || "$RUBY" == 5.* ) ]]; then
34
+ echo "Skipping Ruby $RUBY for Library v1.x (>= 3.4 unsupported)"
33
35
  exit 0
34
36
  fi
35
- if [[ "$LIBRARY_VERSION" == "2" && ( "${{ matrix.ruby }}" == "2.6" || "${{ matrix.ruby }}" == "2.7" || "${{ matrix.ruby }}" == "3.0" ) ]]; then
36
- echo "Skipping Ruby ${{ matrix.ruby }} for Library v2.x"
37
+
38
+ # Library v2: block specific old Rubies
39
+ if [[ "$LIBRARY_VERSION" == "2" && ( "$RUBY" == 2.6* || "$RUBY" == 2.7* || "$RUBY" == 3.0* ) ]]; then
40
+ echo "Skipping Ruby $RUBY for Library v2.x"
37
41
  exit 0
38
42
  fi
43
+
39
44
  bundle exec rake test
40
45
 
41
46
  rubocop:
data/.rubocop.yml CHANGED
@@ -12,6 +12,7 @@ AllCops:
12
12
 
13
13
  Metrics/BlockLength:
14
14
  Exclude:
15
+ - "scout_apm_logging.gemspec"
15
16
  - "spec/**/*"
16
17
 
17
18
  Metrics/ClassLength:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ ## 2.2.0
2
+ * Fix severity numbers sent with `opentelemetry-exporter-otlp-logs` >= 0.7.0 (Ruby levels were sent instead of OpenTelemetry levels).
3
+ * See [#119](https://github.com/scoutapp/scout_apm_ruby_logging/pull/119).
4
+ * Remove the OTLP exporter monkey patch; severity is now mapped when the log is emitted.
5
+ * Replace the [#114](https://github.com/scoutapp/scout_apm_ruby_logging/pull/114) guard with a small `LogRecordData` shim, so mismatched `opentelemetry-exporter-otlp-logs` / `opentelemetry-logs-sdk` versions no longer drop logs.
6
+ * Declare `opentelemetry-logs-api` as a direct dependency.
7
+ * Fix `logger.unknown` raising `NameError`; it is now sent as FATAL.
8
+ * Fix logging text that isn't valid UTF-8 raising an error; invalid bytes are now replaced. Thanks @rpearce!
9
+ * See [#120](https://github.com/scoutapp/scout_apm_ruby_logging/pull/120).
10
+ * Add release workflow for publishing to RubyGems with trusted publishing.
11
+
12
+ ## 2.1.1
13
+ * Fix all logs being dropped with `opentelemetry-logs-sdk` >= 0.7.0. Thanks @rpearce!
14
+ * See [#114](https://github.com/scoutapp/scout_apm_ruby_logging/pull/114).
15
+ * Add Ruby 4.0 to test matrix.
16
+
17
+ ## 2.1.0
18
+ * Add ability to capture log line with logs.
19
+ * `logs_capture_log_line: true`
20
+ * Add ability to capture stack trace with logs.
21
+ * `logs_capture_call_stack: true`
22
+ * See [#98](https://github.com/scoutapp/scout_apm_ruby_logging/pull/98) for benchmarks.
23
+ * Add ability to disable warn message on method_missing.
24
+ * `logs_method_missing_warning: false`
25
+ * Add ability to log stack trace on method_missing.
26
+ * `logs_method_missing_call_stack: true`
27
+ * Add tests for ActionCable.
28
+
1
29
  ## 2.0.0
2
30
  * Remove vendored opentelmetry SDK.
3
31
  * Add support for Ruby 3.4.
data/README.md CHANGED
@@ -28,10 +28,14 @@ Update your [RAILS_ROOT/config/scout_apm.yml](https://scoutapm.com/apps/new_ruby
28
28
 
29
29
  logs_monitor: true
30
30
  logs_ingest_key: ...
31
+ logs_capture_level: debug
31
32
  ```
32
33
 
33
34
  Deploy :rocket:
34
35
 
36
+ ## Docs
37
+ For a complete list of configurations as well as additional information, visit our [docs](https://scoutapm.com/docs/ruby/log-management).
38
+
35
39
  ## Versions
36
40
  We currently maintain two versions: 2.x (Ruby 3.1+) and 1.x (2.6-3.3). These versions/branches have feature parity and a similar release cadence.
37
41
 
@@ -50,7 +54,7 @@ bundle exec rake test file=/path/to/spec/_spec.rb
50
54
 
51
55
  To run test(s) against a specific Ruby version:
52
56
  ```ruby
53
- DOCKER_RUBY_VERSION=3.4 bundle exec rake test
57
+ DOCKER_RUBY_VERSION=4.0 bundle exec rake test
54
58
  ```
55
59
 
56
60
  ## Local
@@ -19,19 +19,12 @@
19
19
  # logs_config - a hash of configuration options for merging into the collector's config
20
20
  # logs_reporting_endpoint - the endpoint to send logs to
21
21
  # logs_proxy_log_dir - the directory to store logs in for monitoring
22
- # manager_lock_file - the location for obtaining an exclusive lock for running monitor manager
23
- # monitor_pid_file - the location of the pid file for the monitor
24
- # monitor_state_file - the location of the state file for the monitor
25
- # monitor_interval - the interval to check the collector healtcheck and for new state logs
26
- # monitor_interval_delay - the delay to wait before running the first monitor interval
27
- # collector_log_level - the log level for the collector
28
- # collector_sending_queue_storage_dir - the directory to store queue files
29
- # collector_offset_storage_dir - the directory to store offset files
30
- # collector_pid_file - the location of the pid file for the collector
31
- # collector_download_dir - the directory to store downloaded collector files
32
- # collector_config_file - the location of the config file for the collector
33
- # collector_version - the version of the collector to download
34
- # health_check_port - the port to use for the collector health check. Default is dynamically derived based on port availability
22
+ # logs_capture_call_stack - true or false. If true, capture the call stack for each log message
23
+ # logs_capture_log_line - true or false. If true, capture the log line for each log message
24
+ # logs_call_stack_search_depth - the number of frames to search in the call stack
25
+ # logs_call_stack_capture_depth - the number of frames to capture in the call stack
26
+ # logs_method_missing_warning - true or false. If true, log a warning when method_missing is called
27
+ # logs_method_missing_call_stack - true or false. If true, capture the call stack when method_missing is called
35
28
  #
36
29
  # Any of these config settings can be set with an environment variable prefixed
37
30
  # by SCOUT_ and uppercasing the key: SCOUT_LOG_LEVEL for instance.
@@ -55,11 +48,23 @@ module ScoutApm
55
48
  logs_reporting_endpoint_http
56
49
  logs_proxy_log_dir
57
50
  logs_log_file_size
51
+ logs_capture_call_stack
52
+ logs_capture_log_line
53
+ logs_call_stack_search_depth
54
+ logs_call_stack_capture_depth
55
+ logs_method_missing_warning
56
+ logs_method_missing_call_stack
58
57
  ].freeze
59
58
 
60
59
  SETTING_COERCIONS = {
61
60
  'logs_monitor' => BooleanCoercion.new,
62
- 'logs_log_file_size' => IntegerCoercion.new
61
+ 'logs_capture_call_stack' => BooleanCoercion.new,
62
+ 'logs_capture_log_line' => BooleanCoercion.new,
63
+ 'logs_call_stack_search_depth' => IntegerCoercion.new,
64
+ 'logs_call_stack_capture_depth' => IntegerCoercion.new,
65
+ 'logs_log_file_size' => IntegerCoercion.new,
66
+ 'logs_method_missing_warning' => BooleanCoercion.new,
67
+ 'logs_method_missing_call_stack' => BooleanCoercion.new
63
68
  }.freeze
64
69
 
65
70
  # The bootstrapped, and initial config that we attach to the context. Will be swapped out by
@@ -117,7 +122,13 @@ module ScoutApm
117
122
  'logs_reporting_endpoint' => 'https://otlp.scoutotel.com:4317',
118
123
  'logs_reporting_endpoint_http' => 'https://otlp.scoutotel.com:4318/v1/logs',
119
124
  'logs_proxy_log_dir' => '/tmp/scout_apm/logs/',
120
- 'logs_log_file_size' => 1024 * 1024 * 10
125
+ 'logs_log_file_size' => 1024 * 1024 * 10,
126
+ 'logs_capture_call_stack' => false,
127
+ 'logs_capture_log_line' => false,
128
+ 'logs_call_stack_search_depth' => 15,
129
+ 'logs_call_stack_capture_depth' => 2,
130
+ 'logs_method_missing_warning' => true,
131
+ 'logs_method_missing_call_stack' => false
121
132
  }.freeze
122
133
 
123
134
  def value(key)
@@ -7,6 +7,14 @@ module ScoutApm
7
7
  # The root of the application.
8
8
  attr_accessor :application_root
9
9
 
10
+ # Use this as the entrypoint.
11
+ def self.instance
12
+ @@instance ||= new.tap do |instance|
13
+ instance.config = ScoutApm::Logging::Config.with_file(instance, instance.config.value('config_file'))
14
+ instance.config.log_settings(instance.logger)
15
+ end
16
+ end
17
+
10
18
  # Initially start up without attempting to load a configuration file. We
11
19
  # need to be able to lookup configuration options like "application_root"
12
20
  # which would then in turn influence where the yaml configuration file is
@@ -56,7 +56,7 @@ module ScoutApm
56
56
  # Re-extend TaggedLogging to verify the patch is be applied.
57
57
  # This appears to be an issue in Ruby 2.7 with the broadcast logger.
58
58
  ruby_version = Gem::Version.new(RUBY_VERSION)
59
- isruby27 = (ruby_version >= Gem::Version.new('2.7') && ruby_version < Gem::Version.new('3.0'))
59
+ isruby27 = ruby_version >= Gem::Version.new('2.7') && ruby_version < Gem::Version.new('3.0')
60
60
  return unless isruby27 && ::Rails.logger.respond_to?(:broadcasts)
61
61
 
62
62
  ::Rails.logger.broadcasts.each do |logger|
@@ -3,6 +3,7 @@
3
3
  require 'json'
4
4
  require 'logger'
5
5
 
6
+ require 'opentelemetry-logs-api'
6
7
  require 'scout_apm'
7
8
 
8
9
  module ScoutApm
@@ -12,40 +13,67 @@ module ScoutApm
12
13
  class Formatter < ::Logger::Formatter
13
14
  DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%LZ'
14
15
 
15
- def call(severity, time, progname, msg) # rubocop:disable Metrics/AbcSize
16
+ # Ruby's Logger::Severity integers (0..5) do not match OpenTelemetry's
17
+ # SeverityNumber scale. Emit the OTel value directly so the exporter can
18
+ # pass it straight through without a patch. Logger labels UNKNOWN as
19
+ # 'ANY'; it ranks above FATAL, so keep it at the top of the scale.
20
+ SEVERITY_NUMBER_MAP = {
21
+ 'DEBUG' => ::OpenTelemetry::Logs::SeverityNumber::SEVERITY_NUMBER_DEBUG,
22
+ 'INFO' => ::OpenTelemetry::Logs::SeverityNumber::SEVERITY_NUMBER_INFO,
23
+ 'WARN' => ::OpenTelemetry::Logs::SeverityNumber::SEVERITY_NUMBER_WARN,
24
+ 'ERROR' => ::OpenTelemetry::Logs::SeverityNumber::SEVERITY_NUMBER_ERROR,
25
+ 'FATAL' => ::OpenTelemetry::Logs::SeverityNumber::SEVERITY_NUMBER_FATAL,
26
+ 'ANY' => ::OpenTelemetry::Logs::SeverityNumber::SEVERITY_NUMBER_FATAL
27
+ }.freeze
28
+
29
+ def call(severity, time, progname, msg)
16
30
  attributes_to_log = {
17
31
  severity:,
18
32
  time: format_datetime(time),
19
33
  msg: msg2str(msg)
20
34
  }
21
35
 
36
+ log_location = Thread.current[:scout_log_location]
37
+
22
38
  attributes_to_log[:progname] = progname if progname
23
39
  attributes_to_log['service.name'] = service_name
40
+ attributes_to_log['log_location'] = log_location if log_location
24
41
 
25
42
  attributes_to_log.merge!(scout_transaction_id)
26
43
  attributes_to_log.merge!(scout_layer)
27
44
  attributes_to_log.merge!(scout_context)
28
- # Naive local benchmarks show this takes around 200 microseconds. As such, we only apply it to WARN and above.
29
- attributes_to_log.merge!(local_log_location) if ::Logger::Severity.const_get(severity) >= ::Logger::Severity::WARN
30
45
 
31
- message = "#{attributes_to_log.to_json}\n"
46
+ emit_log(msg, severity, time, attributes_to_log)
47
+
48
+ "#{attributes_to_log.to_json}\n"
49
+ end
50
+
51
+ private
52
+
53
+ # to_json raises on binary or invalid text, and the raise escapes the log call.
54
+ def msg2str(msg)
55
+ text = super
56
+ return text if text.valid_encoding? && text.encoding != Encoding::BINARY
57
+
58
+ text.dup.force_encoding(Encoding::UTF_8).scrub
59
+ end
32
60
 
61
+ def emit_log(msg, severity, time, attributes_to_log)
33
62
  ScoutApm::Logging::Loggers::OpenTelemetry.logger_provider.logger(
34
63
  name: 'scout_apm',
35
64
  version: '0.1.0'
36
65
  ).on_emit(
37
66
  severity_text: severity,
38
- severity_number: ::Logger::Severity.const_get(severity),
67
+ severity_number: SEVERITY_NUMBER_MAP.fetch(
68
+ severity, ::OpenTelemetry::Logs::SeverityNumber::SEVERITY_NUMBER_UNSPECIFIED
69
+ ),
39
70
  attributes: attributes_to_log.transform_keys(&:to_s),
40
71
  timestamp: time,
41
- body: msg,
72
+ body: msg.is_a?(String) ? attributes_to_log[:msg] : msg,
42
73
  context: ::OpenTelemetry::Context.current
43
74
  )
44
- message
45
75
  end
46
76
 
47
- private
48
-
49
77
  def format_datetime(time)
50
78
  time.utc.strftime(DATETIME_FORMAT)
51
79
  end
@@ -103,15 +131,6 @@ module ScoutApm
103
131
  { "scout_transaction_id": ScoutApm::RequestManager.lookup.transaction_id }
104
132
  end
105
133
 
106
- def local_log_location
107
- # Should give us the last local stack which called the log within just the last couple frames.
108
- last_local_location = caller[0..15].find { |path| path.include?(Rails.root.to_s) }
109
-
110
- return {} unless last_local_location
111
-
112
- { 'log_location' => last_local_location }
113
- end
114
-
115
134
  def context
116
135
  ScoutApm::Logging::Context.new.tap do |context|
117
136
  context.config = ScoutApm::Logging::Config.with_file(context, context.config.value('config_file'))
@@ -7,6 +7,70 @@ module ScoutApm
7
7
  class FileLogger < ::Logger
8
8
  include ::ActiveSupport::LoggerSilence if const_defined?('::ActiveSupport::LoggerSilence')
9
9
 
10
+ def initialize(*args, **kwargs, &block)
11
+ if ScoutApm::Logging::Context.instance.config.value('logs_capture_log_line')
12
+ self.class.send(:alias_method, :debug, :debug_patched)
13
+ self.class.send(:alias_method, :info, :info_patched)
14
+ self.class.send(:alias_method, :warn, :warn_patched)
15
+ self.class.send(:alias_method, :error, :error_patched)
16
+ self.class.send(:alias_method, :fatal, :fatal_patched)
17
+ self.class.send(:alias_method, :unknown, :unknown_patched)
18
+ end
19
+
20
+ super(*args, **kwargs, &block)
21
+ end
22
+
23
+ # Taken from ::Logger. Progname becomes message if no block is given.
24
+ def debug_patched(progname = nil, &block)
25
+ return true if level > DEBUG
26
+
27
+ # short circuit the block to update the message. #add would eventually call it.
28
+ # https://github.com/ruby/logger/blob/v1.7.0/lib/logger.rb#L675
29
+ progname = yield if block_given?
30
+ progname = add_log_file_and_line_to_message(progname) if progname
31
+ add(DEBUG, progname, nil, &block)
32
+ end
33
+
34
+ def info_patched(progname = nil, &block)
35
+ return true if level > INFO
36
+
37
+ progname = yield if block_given?
38
+ progname = add_log_file_and_line_to_message(progname) if progname
39
+ add(INFO, progname, nil, &block)
40
+ end
41
+
42
+ def warn_patched(progname = nil, &block)
43
+ return true if level > WARN
44
+
45
+ progname = yield if block_given?
46
+ progname = add_log_file_and_line_to_message(progname) if progname
47
+ add(WARN, progname, nil, &block)
48
+ end
49
+
50
+ def error_patched(progname = nil, &block)
51
+ return true if level > ERROR
52
+
53
+ progname = yield if block_given?
54
+ progname = add_log_file_and_line_to_message(progname) if progname
55
+ add(ERROR, progname, nil, &block)
56
+ end
57
+
58
+ def fatal_patched(progname = nil, &block)
59
+ return true if level > FATAL
60
+
61
+ progname = yield if block_given?
62
+ progname = add_log_file_and_line_to_message(progname) if progname
63
+ add(FATAL, progname, nil, &block)
64
+ end
65
+
66
+ def unknown_patched(progname = nil, &block)
67
+ return true if level > UNKNOWN
68
+
69
+ progname = yield if block_given?
70
+ progname = add_log_file_and_line_to_message(progname) if progname
71
+ add(UNKNOWN, progname, nil, &block)
72
+ end
73
+
10
74
  # Other loggers may be extended with additional methods that have not been applied to this file logger.
11
75
  # Most likely, these methods will still utilize the exiting logging methods to write to the IO device,
12
76
  # however, if this is not the case we may miss logs. With that being said, we shouldn't impact the original
@@ -14,13 +78,75 @@ module ScoutApm
14
78
  def method_missing(name, *_args)
15
79
  return unless defined?(::Rails)
16
80
 
17
- ::Rails.logger.warn("Method #{name} called on ScoutApm::Logging::Loggers::FileLogger, but it is not defined.")
81
+ return unless ScoutApm::Logging::Context.instance.config.value('logs_method_missing_warning')
82
+
83
+ if ScoutApm::Logging::Context.instance.config.value('logs_method_missing_call_stack')
84
+ cs = caller_locations(0, 20)
85
+ ::Rails.logger.warn("Method #{name} called on ScoutApm::Logging::Loggers::FileLogger, but it is not defined.")
86
+ ::Rails.logger.warn("Call stack: #{cs}")
87
+ else
88
+ ::Rails.logger.warn("Method #{name} called on ScoutApm::Logging::Loggers::FileLogger, but it is not defined. Try setting 'logs_method_missing_call_stack' to true in your Scout configuration to see the call stack.") # rubocop:disable Layout/LineLength
89
+ end
18
90
  end
19
91
 
20
92
  # More impactful for the broadcast logger.
21
93
  def respond_to_missing?(name, *_args)
22
94
  super
23
95
  end
96
+
97
+ private
98
+
99
+ # Useful for testing.
100
+ def filter_log_location(the_call_stack = caller_locations)
101
+ the_call_stack.find { |loc| loc.path.include?(Rails.root.to_s) }
102
+ end
103
+
104
+ # Cache call stack to reduce performance impact.
105
+ def call_stack
106
+ @call_stack ||= caller_locations(4, ScoutApm::Logging::Context.instance.config.value('logs_call_stack_search_depth'))
107
+ end
108
+
109
+ def find_log_location
110
+ filter_log_location(call_stack)
111
+ end
112
+
113
+ def get_call_stack_for_attribute
114
+ call_stack
115
+ .select { |loc| loc.path.include?(Rails.root.to_s) }
116
+ .map(&:to_s)
117
+ .slice(0, ScoutApm::Logging::Context.instance.config.value('logs_call_stack_capture_depth'))
118
+ .join("\n")
119
+ end
120
+
121
+ # Ideally, we would pass an additional argument to the formatter, but
122
+ # we run into issues with how tagged logging is implemented. As such,
123
+ # this is a work around for tagged logging and incorrect passed arguments.
124
+ # May need to move to fiber at some point.
125
+ def format_message(severity, datetime, progname, msg)
126
+ if ScoutApm::Logging::Context.instance.config.value('logs_capture_call_stack')
127
+ Thread.current[:scout_log_location] =
128
+ get_call_stack_for_attribute
129
+ end
130
+
131
+ super(severity, datetime, progname, msg)
132
+
133
+ # Reset for next logger call.
134
+ ensure
135
+ @call_stack = nil
136
+ Thread.current[:scout_log_location] = nil
137
+ end
138
+
139
+ def add_log_file_and_line_to_message(message)
140
+ return message unless message.is_a?(String)
141
+
142
+ file = find_log_location
143
+ return message unless file
144
+
145
+ file_path = file.path.split('/').last
146
+ line_number = file.lineno
147
+
148
+ "[#{file_path}:#{line_number}] #{message}"
149
+ end
24
150
  end
25
151
 
26
152
  # The newly created logger which we can configure, and will log to a filepath.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenTelemetry
4
+ module SDK
5
+ module Logs
6
+ # The OTLP logs exporter and the logs SDK are versioned independently, and every exporter
7
+ # release only requires `opentelemetry-logs-sdk ~> 0.1`, so Bundler can resolve pairs that
8
+ # disagree on LogRecordData's fields:
9
+ # * exporter <= 0.5.1 reads `total_recorded_attributes`, which logs-sdk >= 0.7.0 replaced
10
+ # with `dropped_attributes_count` (read by exporter >= 0.6.0). See #114.
11
+ # * exporter >= 0.5.0 reads `event_name`, which logs-sdk < 0.6.0 does not have.
12
+ # Define whichever of these is missing so any resolved pair can export.
13
+ class LogRecordData
14
+ unless method_defined?(:total_recorded_attributes)
15
+ def total_recorded_attributes
16
+ attributes&.size.to_i + dropped_attributes_count.to_i
17
+ end
18
+ end
19
+
20
+ unless method_defined?(:dropped_attributes_count)
21
+ def dropped_attributes_count
22
+ [total_recorded_attributes.to_i - attributes&.size.to_i, 0].max
23
+ end
24
+ end
25
+
26
+ unless method_defined?(:event_name)
27
+ def event_name
28
+ nil
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -4,7 +4,7 @@ require 'opentelemetry'
4
4
  require 'opentelemetry/sdk'
5
5
  require 'opentelemetry-logs-sdk'
6
6
  require 'opentelemetry/exporter/otlp_logs'
7
- require_relative './log_record_patch'
7
+ require_relative './log_record_data_compat'
8
8
 
9
9
  module ScoutApm
10
10
  module Logging
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ScoutApm
4
4
  module Logging
5
- VERSION = '2.0.0'
5
+ VERSION = '2.2.0'
6
6
  end
7
7
  end
@@ -15,9 +15,7 @@ module ScoutApm
15
15
  # If we are in a Rails environment, setup the monitor daemon manager.
16
16
  class RailTie < ::Rails::Railtie
17
17
  initializer 'scout_apm_logging.monitor', after: :initialize_logger, before: :initialize_cache do
18
- context = Context.new
19
- context.config = Config.with_file(context, context.config.value('config_file'))
20
- context.config.log_settings(context.logger)
18
+ context = ScoutApm::Logging::Context.instance
21
19
 
22
20
  Loggers::Capture.new(context).setup!
23
21
  end
@@ -23,6 +23,7 @@ Gem::Specification.new do |s|
23
23
  s.add_dependency 'opentelemetry-common'
24
24
  s.add_dependency 'opentelemetry-exporter-otlp-logs', '>= 0.2.0'
25
25
  s.add_dependency 'opentelemetry-instrumentation-base'
26
+ s.add_dependency 'opentelemetry-logs-api'
26
27
  s.add_dependency 'opentelemetry-logs-sdk', '>= 0.2.0'
27
28
  s.add_dependency 'opentelemetry-sdk', '>= 1.2'
28
29
  s.add_dependency 'scout_apm'
@@ -31,4 +32,7 @@ Gem::Specification.new do |s|
31
32
  s.add_development_dependency 'rubocop', '1.50.2'
32
33
  s.add_development_dependency 'rubocop-ast', '1.30.0'
33
34
  s.add_development_dependency 'webmock'
35
+ # Old, but works. It is a small wrapper library around websocket-eventmachine
36
+ # for ActionCable protocols.
37
+ s.add_development_dependency 'action_cable_client'
34
38
  end
@@ -1,10 +1,20 @@
1
1
  require 'webmock/rspec'
2
2
 
3
+ # Old, but still works.
4
+ require 'action_cable_client'
5
+ require 'eventmachine'
3
6
  require 'spec_helper'
4
7
  require 'zlib'
5
8
  require 'stringio'
9
+ require 'securerandom'
6
10
  require_relative '../../rails/app'
7
11
 
12
+ ScoutApm::Logging::Loggers::FileLogger.class_exec do
13
+ define_method(:filter_log_location) do |locations|
14
+ locations.find { |loc| loc.path.include?(Rails.root.to_s) && !loc.path.include?('scout_apm/logging') }
15
+ end
16
+ end
17
+
8
18
  describe ScoutApm::Logging do
9
19
  before do
10
20
  @file_path = '/app/response_body.txt'
@@ -14,8 +24,8 @@ describe ScoutApm::Logging do
14
24
  # We have to write to the file, as we are applying the patch in the fork, and
15
25
  # need to run the expectations in the main process.
16
26
  File.open(@file_path, 'a+') do |file|
17
- msgs = transform_body_to_msgs(request.body)
18
- file.puts(msgs)
27
+ records = transform_body_to_records(request.body)
28
+ records.each { |record| file.puts(record.to_json) }
19
29
  end
20
30
  { body: '', headers: {}, status: 200 }
21
31
  end
@@ -38,6 +48,27 @@ describe ScoutApm::Logging do
38
48
  # Call the app to generate the logs
39
49
  `curl localhost:9292`
40
50
 
51
+ channel_client = fork do
52
+ url = 'ws://localhost:9292/cable'
53
+ channel_name = 'TestChannel'
54
+ # Loops.
55
+ EventMachine.run do
56
+ client = ActionCableClient.new(url, channel_name)
57
+ # called whenever a welcome message is received from the server
58
+ client.connected { puts 'successfully connected.' }
59
+
60
+ client.subscribed do
61
+ puts 'client subscribed to the channel.'
62
+ client.perform('ding', { message: 'hello from client' })
63
+ end
64
+
65
+ # called whenever a message is received from the server
66
+ client.received do |message|
67
+ puts message
68
+ end
69
+ end
70
+ end
71
+
41
72
  sleep 5
42
73
 
43
74
  proxy_dir = context.config.value('logs_proxy_log_dir')
@@ -55,32 +86,52 @@ describe ScoutApm::Logging do
55
86
  end
56
87
 
57
88
  local_messages = lines.map { |item| item['msg'] }
89
+ puts local_messages
58
90
 
59
91
  # Verify we have all the logs in the local log file
60
92
  expect(local_messages.count('[TEST] Some log')).to eq(1)
61
93
  expect(local_messages.count('[YIELD] Yield Test')).to eq(1)
62
94
  expect(local_messages.count('Another Log')).to eq(1)
63
95
  expect(local_messages.count('Should not be captured')).to eq(0)
64
-
65
- log_locations = lines.map { |item| item['log_location'] }.compact
66
-
67
- # Verify that log attributes aren't persisted
68
- expect(log_locations.size).to eq(1)
96
+ expect(local_messages.count('Warn level log')).to eq(1)
97
+ expect(local_messages.count('Error level log')).to eq(1)
98
+ expect(local_messages.count('Fatal level log')).to eq(1)
69
99
 
70
100
  # Verify the logs are sent to the receiver
71
- receiver_contents = File.readlines(@file_path, chomp: true)
72
- expect(receiver_contents.count('[TEST] Some log')).to eq(1)
73
- expect(receiver_contents.count('[YIELD] Yield Test')).to eq(1)
74
- expect(receiver_contents.count('Another Log')).to eq(1)
75
- expect(receiver_contents.count('Should not be captured')).to eq(0)
101
+ receiver_records = File.readlines(@file_path, chomp: true).map { |line| JSON.parse(line) }
102
+ receiver_messages = receiver_records.map { |record| record['body'] }
103
+ expect(receiver_messages.count('[TEST] Some log')).to eq(1)
104
+ expect(receiver_messages.count('[YIELD] Yield Test')).to eq(1)
105
+ expect(receiver_messages.count('Another Log')).to eq(1)
106
+ expect(receiver_messages.count('Should not be captured')).to eq(0)
107
+ expect(receiver_messages.count('Warn level log')).to eq(1)
108
+ expect(receiver_messages.count('Error level log')).to eq(1)
109
+ expect(receiver_messages.count('Fatal level log')).to eq(1)
110
+
111
+ # Verify the severity number sent on the wire uses the OpenTelemetry scale,
112
+ # not Ruby's Logger::Severity scale. Guards against upstream exporter drift
113
+ # now that the severity-remapping patch has been removed.
114
+ expect(severity_for(receiver_records, 'Warn level log')).to eq(:SEVERITY_NUMBER_WARN)
115
+ expect(severity_for(receiver_records, 'Error level log')).to eq(:SEVERITY_NUMBER_ERROR)
116
+ expect(severity_for(receiver_records, 'Fatal level log')).to eq(:SEVERITY_NUMBER_FATAL)
117
+ expect(severity_for(receiver_records, '[TEST] Some log')).to eq(:SEVERITY_NUMBER_INFO)
118
+
119
+ # Verify we recorded server ActionCable messages
120
+ expect(receiver_messages.count { |msg| msg.include?('ActionCable Connected:') }).to eq(1)
121
+ expect(receiver_messages.count('Subscribed to test_channel')).to eq(1)
122
+ expect(receiver_messages.count { |msg| msg.include?('Ding received with data: {"message"') }).to eq(1)
123
+ expect(local_messages.count { |msg| msg.include?('ActionCable Connected:') }).to eq(1)
124
+ expect(local_messages.count('Subscribed to test_channel')).to eq(1)
125
+ expect(local_messages.count { |msg| msg.include?('Ding received with data: {"message"') }).to eq(1)
76
126
 
77
127
  # Kill the rails process. We use kill as using any other signal throws a long log line.
78
128
  Process.kill('KILL', rails_pid)
129
+ Process.kill('KILL', channel_client)
79
130
  end
80
131
 
81
132
  private
82
133
 
83
- def transform_body_to_msgs(body)
134
+ def transform_body_to_records(body) # rubocop:disable Metrics/AbcSize
84
135
  gz = Zlib::GzipReader.new(StringIO.new(body))
85
136
  uncompressed = gz.read
86
137
 
@@ -90,9 +141,18 @@ describe ScoutApm::Logging do
90
141
  value_hash[:resource_logs].map do |item|
91
142
  item[:scope_logs].map do |sl|
92
143
  sl[:log_records].map do |log|
93
- log[:body][:string_value]
144
+ {
145
+ 'body' => log[:body][:string_value],
146
+ 'severity_number' => log[:severity_number],
147
+ 'severity_text' => log[:severity_text]
148
+ }
94
149
  end
95
150
  end
96
151
  end.flatten
97
152
  end
153
+
154
+ def severity_for(records, body)
155
+ record = records.find { |item| item['body'] == body }
156
+ record && record['severity_number']&.to_sym
157
+ end
98
158
  end
data/spec/rails/app.rb CHANGED
@@ -5,6 +5,7 @@ rescue LoadError # rubocop:disable Lint/SuppressedException
5
5
  end
6
6
 
7
7
  require 'action_controller/railtie'
8
+ require 'action_cable/engine'
8
9
  require 'logger'
9
10
  require 'scout_apm_logging'
10
11
 
@@ -13,24 +14,58 @@ Rails.logger = ActiveSupport::TaggedLogging.new(Logger.new($stdout))
13
14
  class App < ::Rails::Application
14
15
  config.eager_load = false
15
16
  config.log_level = :info
17
+ config.action_cable.cable = { 'adapter' => 'async' }
18
+ config.action_cable.connection_class = -> { ApplicationCable::Connection }
19
+ config.action_cable.disable_request_forgery_protection = true
16
20
 
17
21
  routes.append do
22
+ mount ActionCable.server => '/cable'
18
23
  root to: 'root#index'
19
24
  end
20
25
  end
21
26
 
22
27
  class RootController < ActionController::Base
23
- def index
24
- Rails.logger.warn('Add location log attributes')
28
+ def index # rubocop:disable Metrics/AbcSize
25
29
  Rails.logger.tagged('TEST').info('Some log')
26
30
  Rails.logger.tagged('YIELD') { logger.info('Yield Test') }
27
31
  Rails.logger.info('Another Log')
28
32
  Rails.logger.debug('Should not be captured')
33
+ Rails.logger.warn('Warn level log')
34
+ Rails.logger.error('Error level log')
35
+ Rails.logger.fatal('Fatal level log')
29
36
 
30
37
  render plain: Rails.version
31
38
  end
32
39
  end
33
40
 
41
+ module ApplicationCable
42
+ class Channel < ActionCable::Channel::Base
43
+ end
44
+ end
45
+
46
+ module ApplicationCable
47
+ class Connection < ActionCable::Connection::Base
48
+ identified_by :id
49
+
50
+ def connect
51
+ self.id = SecureRandom.uuid
52
+ logger.info("ActionCable Connected: #{id}")
53
+ end
54
+ end
55
+ end
56
+
57
+ class TestChannel < ApplicationCable::Channel
58
+ def subscribed
59
+ stream_from 'test_channel'
60
+ logger.info 'Subscribed to test_channel'
61
+ end
62
+
63
+ def ding(data)
64
+ logger.info "Ding received with data: #{data.inspect}"
65
+ transmit({ dong: "Server response to: '#{data['message']}'" })
66
+ end
67
+ end
68
+
34
69
  def initialize_app
35
70
  App.initialize!
36
71
 
@@ -0,0 +1,86 @@
1
+ require 'json'
2
+ require 'logger'
3
+ require 'stringio'
4
+
5
+ require 'spec_helper'
6
+
7
+ require 'scout_apm_logging'
8
+
9
+ describe ScoutApm::Logging::Loggers::Formatter do
10
+ context 'severity' do
11
+ let(:exporter) { OpenTelemetry::SDK::Logs::Export::InMemoryLogRecordExporter.new }
12
+
13
+ before do
14
+ ScoutApm::Logging::Context.instance
15
+
16
+ provider = OpenTelemetry::SDK::Logs::LoggerProvider.new
17
+ provider.add_log_record_processor(OpenTelemetry::SDK::Logs::Export::SimpleLogRecordProcessor.new(exporter))
18
+ ScoutApm::Logging::Loggers::OpenTelemetry.logger_provider = provider
19
+ end
20
+
21
+ {
22
+ 'DEBUG' => :SEVERITY_NUMBER_DEBUG,
23
+ 'INFO' => :SEVERITY_NUMBER_INFO,
24
+ 'WARN' => :SEVERITY_NUMBER_WARN,
25
+ 'ERROR' => :SEVERITY_NUMBER_ERROR,
26
+ 'FATAL' => :SEVERITY_NUMBER_FATAL,
27
+ 'ANY' => :SEVERITY_NUMBER_FATAL,
28
+ 'SOMETHING_ELSE' => :SEVERITY_NUMBER_UNSPECIFIED
29
+ }.each do |label, expected|
30
+ it "emits #{expected} for #{label}" do
31
+ described_class.new.call(label, Time.now, nil, 'msg')
32
+
33
+ record = exporter.emitted_log_records.last
34
+ expect(record.severity_text).to eq(label)
35
+ expect(record.severity_number).to eq(OpenTelemetry::Logs::SeverityNumber.const_get(expected))
36
+ end
37
+ end
38
+
39
+ it 'maps Logger#unknown to FATAL' do
40
+ logger = ::Logger.new(StringIO.new)
41
+ logger.formatter = described_class.new
42
+ logger.unknown('gone')
43
+
44
+ expect(exporter.emitted_log_records.last.severity_number).to eq(OpenTelemetry::Logs::SeverityNumber::SEVERITY_NUMBER_FATAL)
45
+ end
46
+ end
47
+
48
+ context 'encoding' do
49
+ let(:formatter) { ScoutApm::Logging::Loggers::Formatter.new }
50
+ let(:otel_logger) { double(on_emit: nil) }
51
+
52
+ before do
53
+ allow(ScoutApm::Logging::Loggers::OpenTelemetry).to receive(:logger_provider).and_return(double(logger: otel_logger))
54
+ end
55
+
56
+ it 'should replace bytes that are not valid UTF-8' do
57
+ line = formatter.call('INFO', Time.now, nil, "bad \xFF byte")
58
+
59
+ expect(JSON.parse(line)['msg']).to eq("bad \u{FFFD} byte")
60
+ end
61
+
62
+ it 'should read binary text, as a request path is logged, as UTF-8' do
63
+ line = formatter.call('INFO', Time.now, nil, 'café /x?%FF'.b + "\xFF".b)
64
+
65
+ expect(JSON.parse(line)['msg']).to eq("café /x?%FF\u{FFFD}")
66
+ end
67
+
68
+ it 'should keep valid text in another encoding' do
69
+ line = formatter.call('INFO', Time.now, nil, 'café'.encode('UTF-16LE'))
70
+
71
+ expect(JSON.parse(line)['msg']).to eq('café')
72
+ end
73
+
74
+ it 'should replace bad bytes in an exception message' do
75
+ line = formatter.call('ERROR', Time.now, nil, RuntimeError.new("bad \xFF byte".b))
76
+
77
+ expect(JSON.parse(line)['msg']).to eq("bad \u{FFFD} byte (RuntimeError)\n")
78
+ end
79
+
80
+ it 'should emit the replaced text as the OpenTelemetry body' do
81
+ formatter.call('INFO', Time.now, nil, "bad \xFF byte".b)
82
+
83
+ expect(otel_logger).to have_received(:on_emit).with(hash_including(body: "bad \u{FFFD} byte"))
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,94 @@
1
+ require 'logger'
2
+ require 'stringio'
3
+
4
+ require 'spec_helper'
5
+
6
+ require 'scout_apm_logging'
7
+
8
+ def capture_stdout
9
+ old_stdout = $stdout
10
+ $stdout = StringIO.new
11
+ yield
12
+ $stdout.string
13
+ ensure
14
+ $stdout = old_stdout
15
+ end
16
+
17
+ describe ScoutApm::Logging::Loggers::Logger do
18
+ before do
19
+ allow_any_instance_of(ScoutApm::Logging::Loggers::Formatter).to receive(:emit_log)
20
+ end
21
+
22
+ it 'should not capture call stack or log line' do
23
+ ScoutApm::Logging::Context.instance
24
+
25
+ output_from_log = capture_stdout do
26
+ logger = ScoutApm::Logging::Loggers::FileLogger.new($stdout).tap do |instance|
27
+ instance.level = 0
28
+ instance.formatter = ScoutApm::Logging::Loggers::Formatter.new
29
+ end
30
+
31
+ logger.info('Hi')
32
+ end
33
+
34
+ expect(output_from_log).not_to include('"log_location":"')
35
+ expect(output_from_log).not_to include('"msg":"[logger_spec.rb')
36
+ end
37
+
38
+ it 'should capture call stack' do
39
+ ENV['SCOUT_LOGS_CAPTURE_CALL_STACK'] = 'true'
40
+ ScoutApm::Logging::Context.instance
41
+
42
+ output_from_log = capture_stdout do
43
+ logger = ScoutApm::Logging::Loggers::FileLogger.new($stdout).tap do |instance|
44
+ instance.level = 0
45
+ instance.formatter = ScoutApm::Logging::Loggers::Formatter.new
46
+ end
47
+
48
+ logger.info('Hi')
49
+ end
50
+
51
+ expect(output_from_log).to include('"log_location":"')
52
+ expect(output_from_log).not_to include('"msg":"[logger_spec.rb')
53
+ ENV['SCOUT_LOGS_CAPTURE_LOG_LINE'] = 'false' # set back to default
54
+ end
55
+
56
+ it 'should capture log line and call stack' do
57
+ ENV['SCOUT_LOGS_CAPTURE_CALL_STACK'] = 'true'
58
+ ENV['SCOUT_LOGS_CAPTURE_LOG_LINE'] = 'true'
59
+
60
+ ScoutApm::Logging::Context.instance
61
+
62
+ output_from_log = capture_stdout do
63
+ logger = ScoutApm::Logging::Loggers::FileLogger.new($stdout).tap do |instance|
64
+ instance.level = 0
65
+ instance.formatter = ScoutApm::Logging::Loggers::Formatter.new
66
+ end
67
+
68
+ logger.info('Hi')
69
+ end
70
+
71
+ expect(output_from_log).to include('"msg":"[logger_spec.rb')
72
+ expect(output_from_log).to include('"log_location":"')
73
+ ENV['SCOUT_LOGS_CAPTURE_CALL_STACK'] = 'false' # set back to default
74
+ ENV['SCOUT_LOGS_CAPTURE_LOG_LINE'] = 'false' # set back to default
75
+ end
76
+
77
+ it 'should capture log line' do
78
+ ENV['SCOUT_LOGS_CAPTURE_LOG_LINE'] = 'true'
79
+ ScoutApm::Logging::Context.instance
80
+
81
+ output_from_log = capture_stdout do
82
+ logger = ScoutApm::Logging::Loggers::FileLogger.new($stdout).tap do |instance|
83
+ instance.level = 0
84
+ instance.formatter = ScoutApm::Logging::Loggers::Formatter.new
85
+ end
86
+
87
+ logger.info('Hi')
88
+ end
89
+
90
+ expect(output_from_log).not_to include('"log_location":"')
91
+ expect(output_from_log).to include('"msg":"[logger_spec.rb')
92
+ ENV['SCOUT_LOGS_CAPTURE_LOG_LINE'] = 'false' # set back to default
93
+ end
94
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ require_relative '../../../../lib/scout_apm/logging/loggers/opentelemetry/opentelemetry'
4
+
5
+ describe OpenTelemetry::SDK::Logs::LogRecordData do
6
+ let(:exporter) { OpenTelemetry::SDK::Logs::Export::InMemoryLogRecordExporter.new }
7
+
8
+ let(:log_record_data) do
9
+ provider = OpenTelemetry::SDK::Logs::LoggerProvider.new
10
+ provider.add_log_record_processor(OpenTelemetry::SDK::Logs::Export::SimpleLogRecordProcessor.new(exporter))
11
+
12
+ provider.logger(name: 'scout_apm', version: '0.1.0').on_emit(
13
+ severity_text: 'INFO',
14
+ severity_number: OpenTelemetry::Logs::SeverityNumber::SEVERITY_NUMBER_INFO,
15
+ attributes: { 'controller' => 'UsersController' },
16
+ timestamp: Time.now,
17
+ body: 'Processing by UsersController#index'
18
+ )
19
+
20
+ exporter.emitted_log_records.first
21
+ end
22
+
23
+ it 'responds to the fields read by every supported exporter version' do
24
+ expect(log_record_data.total_recorded_attributes).to eq(1)
25
+ expect(log_record_data.dropped_attributes_count).to eq(0)
26
+ expect(log_record_data).to respond_to(:event_name)
27
+ end
28
+
29
+ it 'exports through the installed OTLP exporter without raising' do
30
+ otlp_exporter = OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new
31
+ # Exporter >= 0.7.0 moved encoding into opentelemetry-exporter-otlp-common.
32
+ encoded = if otlp_exporter.respond_to?(:encode, true)
33
+ otlp_exporter.send(:encode, [log_record_data])
34
+ else
35
+ OpenTelemetry::Exporter::OTLP::Common.as_encoded_elsr([log_record_data])
36
+ end
37
+ request = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode(encoded)
38
+ record = request.resource_logs.first.scope_logs.first.log_records.first
39
+
40
+ expect(record.body.string_value).to eq('Processing by UsersController#index')
41
+ expect(record.severity_number).to eq(:SEVERITY_NUMBER_INFO)
42
+ expect(record.dropped_attributes_count).to eq(0)
43
+ end
44
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_apm_logging
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scout APM
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-03-28 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: googleapis-common-protos-types
@@ -94,6 +93,20 @@ dependencies:
94
93
  - - ">="
95
94
  - !ruby/object:Gem::Version
96
95
  version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: opentelemetry-logs-api
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
97
110
  - !ruby/object:Gem::Dependency
98
111
  name: opentelemetry-logs-sdk
99
112
  requirement: !ruby/object:Gem::Requirement
@@ -192,6 +205,20 @@ dependencies:
192
205
  - - ">="
193
206
  - !ruby/object:Gem::Version
194
207
  version: '0'
208
+ - !ruby/object:Gem::Dependency
209
+ name: action_cable_client
210
+ requirement: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ type: :development
216
+ prerelease: false
217
+ version_requirements: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - ">="
220
+ - !ruby/object:Gem::Version
221
+ version: '0'
195
222
  description: Sets up log monitoring for Scout APM Ruby clients.
196
223
  email:
197
224
  - support@scoutapp.com
@@ -199,6 +226,7 @@ executables: []
199
226
  extensions: []
200
227
  extra_rdoc_files: []
201
228
  files:
229
+ - ".github/workflows/release.yml"
202
230
  - ".github/workflows/test.yml"
203
231
  - ".gitignore"
204
232
  - ".rubocop.yml"
@@ -216,7 +244,7 @@ files:
216
244
  - lib/scout_apm/logging/loggers/formatter.rb
217
245
  - lib/scout_apm/logging/loggers/logger.rb
218
246
  - lib/scout_apm/logging/loggers/opentelemetry/NOTICE
219
- - lib/scout_apm/logging/loggers/opentelemetry/log_record_patch.rb
247
+ - lib/scout_apm/logging/loggers/opentelemetry/log_record_data_compat.rb
220
248
  - lib/scout_apm/logging/loggers/opentelemetry/opentelemetry.rb
221
249
  - lib/scout_apm/logging/loggers/patches/rails_logger.rb
222
250
  - lib/scout_apm/logging/loggers/patches/tagged_logging.rb
@@ -235,11 +263,13 @@ files:
235
263
  - spec/spec_helper.rb
236
264
  - spec/unit/config_spec.rb
237
265
  - spec/unit/loggers/capture_spec.rb
266
+ - spec/unit/loggers/formatter_spec.rb
267
+ - spec/unit/loggers/logger_spec.rb
268
+ - spec/unit/loggers/opentelemetry/log_record_data_compat_spec.rb
238
269
  homepage: https://github.com/scoutapp/scout_apm_ruby_logging
239
270
  licenses:
240
271
  - MIT
241
272
  metadata: {}
242
- post_install_message:
243
273
  rdoc_options: []
244
274
  require_paths:
245
275
  - lib
@@ -254,15 +284,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
254
284
  - !ruby/object:Gem::Version
255
285
  version: '0'
256
286
  requirements: []
257
- rubygems_version: 3.3.26
258
- signing_key:
287
+ rubygems_version: 4.0.20
259
288
  specification_version: 4
260
289
  summary: Managed log monitoring for Ruby applications.
261
- test_files:
262
- - spec/data/config_test_1.yml
263
- - spec/data/mock_config.yml
264
- - spec/integration/rails/lifecycle_spec.rb
265
- - spec/rails/app.rb
266
- - spec/spec_helper.rb
267
- - spec/unit/config_spec.rb
268
- - spec/unit/loggers/capture_spec.rb
290
+ test_files: []
@@ -1,38 +0,0 @@
1
- module OpenTelemetry
2
- module Exporter
3
- module OTLP
4
- module Logs
5
- # Patch the log record creation to add back severity number.
6
- class LogsExporter
7
- def as_otlp_log_record(log_record_data)
8
- Opentelemetry::Proto::Logs::V1::LogRecord.new(
9
- time_unix_nano: log_record_data.timestamp,
10
- observed_time_unix_nano: log_record_data.observed_timestamp,
11
- severity_number: as_otlp_severity_number(log_record_data.severity_number),
12
- severity_text: log_record_data.severity_text,
13
- body: as_otlp_any_value(log_record_data.body),
14
- attributes: log_record_data.attributes&.map { |k, v| as_otlp_key_value(k, v) },
15
- dropped_attributes_count: log_record_data.total_recorded_attributes - log_record_data.attributes&.size.to_i,
16
- flags: log_record_data.trace_flags.instance_variable_get(:@flags),
17
- trace_id: log_record_data.trace_id,
18
- span_id: log_record_data.span_id
19
- )
20
- end
21
-
22
- # TODO: This has been removed in newer versions of the OpenTelemetry Ruby Logs SDK, however
23
- # it appears that we are missing the severity number. Investigate this.
24
- def as_otlp_severity_number(severity_number)
25
- case severity_number
26
- when 0 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_DEBUG
27
- when 1 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_INFO
28
- when 2 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_WARN
29
- when 3 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_ERROR
30
- when 4 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_FATAL
31
- when 5 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_UNSPECIFIED
32
- end
33
- end
34
- end
35
- end
36
- end
37
- end
38
- end