scout_apm_logging 1.0.3 → 1.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: b0d9d5323e07f8b2fe838a030d93123d00a5fb6e289fe7bc78d7ac0bcc2ebc79
4
- data.tar.gz: e00d25c837617410f02ec4d6523fd5afdbb37efa633b3ecaf625f5929a1b960f
3
+ metadata.gz: 83a18019240ac4086ab5fbbc2c729c334123830425a569442629783bb0e20a65
4
+ data.tar.gz: 9f4e105cf2d85cc9352434c7749fc61bbc3545ee2b5baee56c86a0f0a47b14a8
5
5
  SHA512:
6
- metadata.gz: 0d126cf0149770bb461251442e7cf2fdb40c536076d3d09131ad3d0df3763e10fb5ec4bb44ceb780b11851ab4c2276934b476033d77bcbb7fa8db7e89eed0339
7
- data.tar.gz: 2c441b2bf745ca77aa154d44a536509de405051b8c363d9d8ccb95a6828fa77110b0bc3e196cf3044e7d0e1b837b295de855b428c64e8e6c7fa39d910fcb3d15
6
+ metadata.gz: 426922ace12c94d8e84c9840971884b55ca9ac2328c15f76553f3259987448ea264bed665b85fb3bfa34d14230a556ababd1b7810fa339174c31bc851210a33d
7
+ data.tar.gz: 72503cc9faaf7130028e466f71ea785ec3ece65a82cc5a88d318e68f4123ba89464961a9a6c50c2e22061b0c39cdce0d8af045b4e0c3577e2f4b86709ae527d7
@@ -8,20 +8,35 @@ 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' ]
11
+ ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4']
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
+ - name: Extract Library Version
19
+ run: |
20
+ VERSION=$(grep -oE '[0-9]+\.[0-9]+' lib/scout_apm/logging/version.rb | head -n 1 | cut -d '.' -f1)
21
+ echo "LIBRARY_VERSION=$VERSION" >> $GITHUB_ENV
18
22
  - name: Set up Ruby
19
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)) }}
20
26
  with:
21
27
  ruby-version: ${{ matrix.ruby }}
22
28
  bundler-cache: true
23
29
  - name: Run RSpec
24
- run: bundle exec rake test
30
+ run: |
31
+ if [[ "$LIBRARY_VERSION" == "1" && ("${{ matrix.ruby }}" == "3.4" ) ]]; then
32
+ echo "Skipping Ruby ${{ matrix.ruby }} for Library v1.x"
33
+ exit 0
34
+ 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
+ exit 0
38
+ fi
39
+ bundle exec rake test
25
40
 
26
41
  rubocop:
27
42
  name: 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,18 @@
1
+ ## 1.2.0
2
+ * Add ability to capture log line with logs.
3
+ * `logs_capture_log_line: true`
4
+ * Add ability to capture stack trace with logs.
5
+ * `logs_capture_call_stack: true`
6
+ * See [#98](https://github.com/scoutapp/scout_apm_ruby_logging/pull/98) for benchmarks.
7
+ * Add ability to disable warn message on method_missing.
8
+ * `logs_method_missing_warning: false`
9
+ * Add ability to log stack trace on method_missing.
10
+ * `logs_method_missing_call_stack: true`
11
+ * Add tests for ActionCable.
12
+
13
+ ## 1.1.0
14
+ * Bump vendored SDK version to 0.2.0.
15
+
1
16
  ## 1.0.3
2
17
  * Add capturing of queue for background jobs.
3
18
  * Fix entrypoint name capturing for namespaced controllers.
@@ -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|
@@ -12,40 +12,44 @@ module ScoutApm
12
12
  class Formatter < ::Logger::Formatter
13
13
  DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%LZ'
14
14
 
15
- def call(severity, time, progname, msg) # rubocop:disable Metrics/AbcSize
15
+ def call(severity, time, progname, msg)
16
16
  attributes_to_log = {
17
17
  severity: severity,
18
18
  time: format_datetime(time),
19
19
  msg: msg2str(msg)
20
20
  }
21
21
 
22
+ log_location = Thread.current[:scout_log_location]
23
+
22
24
  attributes_to_log[:progname] = progname if progname
23
25
  attributes_to_log['service.name'] = service_name
26
+ attributes_to_log['log_location'] = log_location if log_location
24
27
 
25
28
  attributes_to_log.merge!(scout_transaction_id)
26
29
  attributes_to_log.merge!(scout_layer)
27
30
  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
31
 
31
- message = "#{attributes_to_log.to_json}\n"
32
+ emit_log(msg, severity, time, attributes_to_log)
33
+
34
+ "#{attributes_to_log.to_json}\n"
35
+ end
36
+
37
+ private
32
38
 
39
+ def emit_log(msg, severity, time, attributes_to_log)
33
40
  ScoutApm::Logging::Loggers::OpenTelemetry.logger_provider.logger(
34
41
  name: 'scout_apm',
35
42
  version: '0.1.0'
36
43
  ).on_emit(
37
44
  severity_text: severity,
38
45
  severity_number: ::Logger::Severity.const_get(severity),
39
- attributes: attributes_to_log,
46
+ attributes: attributes_to_log.transform_keys(&:to_s),
40
47
  timestamp: time,
41
48
  body: msg,
42
49
  context: ::OpenTelemetry::Context.current
43
50
  )
44
- message
45
51
  end
46
52
 
47
- private
48
-
49
53
  def format_datetime(time)
50
54
  time.utc.strftime(DATETIME_FORMAT)
51
55
  end
@@ -103,15 +107,6 @@ module ScoutApm
103
107
  { "scout_transaction_id": ScoutApm::RequestManager.lookup.transaction_id }
104
108
  end
105
109
 
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
110
  def context
116
111
  ScoutApm::Logging::Context.new.tap do |context|
117
112
  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.