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 +4 -4
- data/.github/workflows/release.yml +30 -0
- data/.github/workflows/test.yml +13 -8
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +28 -0
- data/README.md +5 -1
- data/lib/scout_apm/logging/config.rb +26 -15
- data/lib/scout_apm/logging/context.rb +8 -0
- data/lib/scout_apm/logging/loggers/capture.rb +1 -1
- data/lib/scout_apm/logging/loggers/formatter.rb +37 -18
- data/lib/scout_apm/logging/loggers/logger.rb +127 -1
- data/lib/scout_apm/logging/loggers/opentelemetry/log_record_data_compat.rb +34 -0
- data/lib/scout_apm/logging/loggers/opentelemetry/opentelemetry.rb +1 -1
- data/lib/scout_apm/logging/version.rb +1 -1
- data/lib/scout_apm_logging.rb +1 -3
- data/scout_apm_logging.gemspec +4 -0
- data/spec/integration/rails/lifecycle_spec.rb +74 -14
- data/spec/rails/app.rb +37 -2
- data/spec/unit/loggers/formatter_spec.rb +86 -0
- data/spec/unit/loggers/logger_spec.rb +94 -0
- data/spec/unit/loggers/opentelemetry/log_record_data_compat_spec.rb +44 -0
- metadata +37 -15
- data/lib/scout_apm/logging/loggers/opentelemetry/log_record_patch.rb +0 -38
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3e6660d99d9d31d6e78ba6ef801432d582a5a9fe78917d8b027a8cfae85e834
|
|
4
|
+
data.tar.gz: 37fd12719798598626ac9d97bfd3c13d186be2d54a0e6ac86b572ecc246c31b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/.github/workflows/test.yml
CHANGED
|
@@ -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-
|
|
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' && !
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
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=
|
|
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
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
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
|
-
'
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
data/lib/scout_apm_logging.rb
CHANGED
|
@@ -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.
|
|
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
|
data/scout_apm_logging.gemspec
CHANGED
|
@@ -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
|
-
|
|
18
|
-
file.puts(
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
expect(
|
|
74
|
-
expect(
|
|
75
|
-
expect(
|
|
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
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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/
|
|
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:
|
|
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
|