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.
@@ -12,6 +12,9 @@ module ScoutApm
12
12
  module Logs
13
13
  # The SDK implementation of OpenTelemetry::Logs::LoggerProvider.
14
14
  class LoggerProvider < OpenTelemetry::Logs::LoggerProvider
15
+ Key = Struct.new(:name, :version)
16
+ private_constant(:Key)
17
+
15
18
  UNEXPECTED_ERROR_MESSAGE = 'unexpected error in ' \
16
19
  'OpenTelemetry::SDK::Logs::LoggerProvider#%s'
17
20
 
@@ -21,13 +24,18 @@ module ScoutApm
21
24
  #
22
25
  # @param [optional Resource] resource The resource to associate with
23
26
  # new LogRecords created by {Logger}s created by this LoggerProvider.
27
+ # @param [optional LogRecordLimits] log_record_limits The limits for
28
+ # attributes count and attribute length for LogRecords.
24
29
  #
25
30
  # @return [OpenTelemetry::SDK::Logs::LoggerProvider]
26
- def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create)
31
+ def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create, log_record_limits: LogRecordLimits::DEFAULT)
27
32
  @log_record_processors = []
33
+ @log_record_limits = log_record_limits
28
34
  @mutex = Mutex.new
29
35
  @resource = resource
30
36
  @stopped = false
37
+ @registry = {}
38
+ @registry_mutex = Mutex.new
31
39
  end
32
40
 
33
41
  # Returns an {OpenTelemetry::SDK::Logs::Logger} instance.
@@ -44,7 +52,9 @@ module ScoutApm
44
52
  "invalid name. Name provided: #{name.inspect}")
45
53
  end
46
54
 
47
- Logger.new(name, version, self)
55
+ @registry_mutex.synchronize do
56
+ @registry[Key.new(name, version)] ||= Logger.new(name, version, self)
57
+ end
48
58
  end
49
59
 
50
60
  # Adds a new log record processor to this LoggerProvider's
@@ -134,6 +144,7 @@ module ScoutApm
134
144
  trace_flags: nil,
135
145
  instrumentation_scope: nil,
136
146
  context: nil)
147
+ return if @stopped
137
148
 
138
149
  log_record = LogRecord.new(timestamp: timestamp,
139
150
  observed_timestamp: observed_timestamp,
@@ -145,7 +156,8 @@ module ScoutApm
145
156
  span_id: span_id,
146
157
  trace_flags: trace_flags,
147
158
  resource: @resource,
148
- instrumentation_scope: instrumentation_scope)
159
+ instrumentation_scope: instrumentation_scope,
160
+ log_record_limits: @log_record_limits)
149
161
 
150
162
  @log_record_processors.each { |processor| processor.on_emit(log_record, context) }
151
163
  end
@@ -11,10 +11,10 @@ module ScoutApm
11
11
  module SDK
12
12
  module Logs
13
13
  # Current OpenTelemetry logs sdk version
14
- VERSION = '0.1.0'
14
+ VERSION = '0.2.0'
15
15
  end
16
16
  end
17
17
  end
18
18
  end
19
19
  end
20
- end
20
+ end
@@ -11,6 +11,7 @@ require_relative 'logs/log_record'
11
11
  require_relative 'logs/log_record_data'
12
12
  require_relative 'logs/log_record_processor'
13
13
  require_relative 'logs/export'
14
+ require_relative 'logs/log_record_limits'
14
15
 
15
16
  module ScoutApm
16
17
  module Logging
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ScoutApm
4
4
  module Logging
5
- VERSION = '1.0.3'
5
+ VERSION = '1.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
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
7
7
  s.authors = 'Scout APM'
8
8
  s.email = ['support@scoutapp.com']
9
9
  s.homepage = 'https://github.com/scoutapp/scout_apm_ruby_logging'
10
- s.summary = 'Ruby Logging Support'
10
+ s.summary = 'Managed log monitoring for Ruby applications.'
11
11
  s.description = 'Sets up log monitoring for Scout APM Ruby clients.'
12
12
  s.license = 'MIT'
13
13
 
@@ -29,4 +29,7 @@ Gem::Specification.new do |s|
29
29
  s.add_development_dependency 'rubocop', '1.50.2'
30
30
  s.add_development_dependency 'rubocop-ast', '1.30.0'
31
31
  s.add_development_dependency 'webmock'
32
+ # Old, but works. It is a small wrapper library around websocket-eventmachine
33
+ # for ActionCable protocols.
34
+ s.add_development_dependency 'action_cable_client'
32
35
  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'
@@ -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')
@@ -54,18 +85,19 @@ describe ScoutApm::Logging do
54
85
  end
55
86
  end
56
87
 
88
+ puts lines
89
+
57
90
  local_messages = lines.map { |item| item['msg'] }
91
+ puts local_messages
58
92
 
59
93
  # Verify we have all the logs in the local log file
60
94
  expect(local_messages.count('[TEST] Some log')).to eq(1)
61
95
  expect(local_messages.count('[YIELD] Yield Test')).to eq(1)
62
96
  expect(local_messages.count('Another Log')).to eq(1)
63
97
  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)
98
+ expect(local_messages.count('Warn level log')).to eq(1)
99
+ expect(local_messages.count('Error level log')).to eq(1)
100
+ expect(local_messages.count('Fatal level log')).to eq(1)
69
101
 
70
102
  # Verify the logs are sent to the receiver
71
103
  receiver_contents = File.readlines(@file_path, chomp: true)
@@ -73,9 +105,21 @@ describe ScoutApm::Logging do
73
105
  expect(receiver_contents.count('[YIELD] Yield Test')).to eq(1)
74
106
  expect(receiver_contents.count('Another Log')).to eq(1)
75
107
  expect(receiver_contents.count('Should not be captured')).to eq(0)
108
+ expect(local_messages.count('Warn level log')).to eq(1)
109
+ expect(local_messages.count('Error level log')).to eq(1)
110
+ expect(local_messages.count('Fatal level log')).to eq(1)
111
+
112
+ # Verify we recorded server ActionCable messages
113
+ expect(receiver_contents.count { |msg| msg.include?('ActionCable Connected:') }).to eq(1)
114
+ expect(receiver_contents.count('Subscribed to test_channel')).to eq(1)
115
+ expect(receiver_contents.count { |msg| msg.include?('Ding received with data: {"message"') }).to eq(1)
116
+ expect(local_messages.count { |msg| msg.include?('ActionCable Connected:') }).to eq(1)
117
+ expect(local_messages.count('Subscribed to test_channel')).to eq(1)
118
+ expect(local_messages.count { |msg| msg.include?('Ding received with data: {"message"') }).to eq(1)
76
119
 
77
120
  # Kill the rails process. We use kill as using any other signal throws a long log line.
78
121
  Process.kill('KILL', rails_pid)
122
+ Process.kill('KILL', channel_client)
79
123
  end
80
124
 
81
125
  private
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,95 @@
1
+ require 'logger'
2
+ require 'stringio'
3
+
4
+ require 'spec_helper'
5
+
6
+ require 'scout_apm_logging'
7
+
8
+ ScoutApm::Logging::Loggers::Formatter.class_exec do
9
+ define_method(:emit_log) do |msg, severity, time, attributes_to_log|
10
+ end
11
+ end
12
+
13
+ def capture_stdout
14
+ old_stdout = $stdout
15
+ $stdout = StringIO.new
16
+ yield
17
+ $stdout.string
18
+ ensure
19
+ $stdout = old_stdout
20
+ end
21
+
22
+ describe ScoutApm::Logging::Loggers::Logger do
23
+ it 'should not capture call stack or log line' do
24
+ ScoutApm::Logging::Context.instance
25
+
26
+ output_from_log = capture_stdout do
27
+ logger = ScoutApm::Logging::Loggers::FileLogger.new($stdout).tap do |instance|
28
+ instance.level = 0
29
+ instance.formatter = ScoutApm::Logging::Loggers::Formatter.new
30
+ end
31
+
32
+ logger.info('Hi')
33
+ end
34
+
35
+ expect(output_from_log).not_to include('"log_location":"')
36
+ expect(output_from_log).not_to include('"msg":"[logger_spec.rb')
37
+ end
38
+
39
+ it 'should capture call stack' do
40
+ ENV['SCOUT_LOGS_CAPTURE_CALL_STACK'] = 'true'
41
+ ScoutApm::Logging::Context.instance
42
+
43
+ output_from_log = capture_stdout do
44
+ logger = ScoutApm::Logging::Loggers::FileLogger.new($stdout).tap do |instance|
45
+ instance.level = 0
46
+ instance.formatter = ScoutApm::Logging::Loggers::Formatter.new
47
+ end
48
+
49
+ logger.info('Hi')
50
+ end
51
+
52
+ expect(output_from_log).to include('"log_location":"')
53
+ expect(output_from_log).not_to include('"msg":"[logger_spec.rb')
54
+ ENV['SCOUT_LOGS_CAPTURE_LOG_LINE'] = 'false' # set back to default
55
+ end
56
+
57
+ it 'should capture log line and call stack' do
58
+ ENV['SCOUT_LOGS_CAPTURE_CALL_STACK'] = 'true'
59
+ ENV['SCOUT_LOGS_CAPTURE_LOG_LINE'] = 'true'
60
+
61
+ ScoutApm::Logging::Context.instance
62
+
63
+ output_from_log = capture_stdout do
64
+ logger = ScoutApm::Logging::Loggers::FileLogger.new($stdout).tap do |instance|
65
+ instance.level = 0
66
+ instance.formatter = ScoutApm::Logging::Loggers::Formatter.new
67
+ end
68
+
69
+ logger.info('Hi')
70
+ end
71
+
72
+ expect(output_from_log).to include('"msg":"[logger_spec.rb')
73
+ expect(output_from_log).to include('"log_location":"')
74
+ ENV['SCOUT_LOGS_CAPTURE_CALL_STACK'] = 'false' # set back to default
75
+ ENV['SCOUT_LOGS_CAPTURE_LOG_LINE'] = 'false' # set back to default
76
+ end
77
+
78
+ it 'should capture log line' do
79
+ ENV['SCOUT_LOGS_CAPTURE_LOG_LINE'] = 'true'
80
+ ScoutApm::Logging::Context.instance
81
+
82
+ output_from_log = capture_stdout do
83
+ logger = ScoutApm::Logging::Loggers::FileLogger.new($stdout).tap do |instance|
84
+ instance.level = 0
85
+ instance.formatter = ScoutApm::Logging::Loggers::Formatter.new
86
+ end
87
+
88
+ logger.info('Hi')
89
+ end
90
+
91
+ expect(output_from_log).not_to include('"log_location":"')
92
+ expect(output_from_log).to include('"msg":"[logger_spec.rb')
93
+ ENV['SCOUT_LOGS_CAPTURE_LOG_LINE'] = 'false' # set back to default
94
+ end
95
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_apm_logging
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scout APM
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-27 00:00:00.000000000 Z
11
+ date: 2025-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: googleapis-common-protos-types
@@ -164,6 +164,20 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: action_cable_client
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
167
181
  description: Sets up log monitoring for Scout APM Ruby clients.
168
182
  email:
169
183
  - support@scoutapp.com
@@ -208,6 +222,7 @@ files:
208
222
  - lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/export/log_record_exporter.rb
209
223
  - lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/log_record.rb
210
224
  - lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/log_record_data.rb
225
+ - lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/log_record_limits.rb
211
226
  - lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/log_record_processor.rb
212
227
  - lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/logger.rb
213
228
  - lib/scout_apm/logging/loggers/opentelemetry/sdk/logs/logger_provider.rb
@@ -229,6 +244,7 @@ files:
229
244
  - spec/spec_helper.rb
230
245
  - spec/unit/config_spec.rb
231
246
  - spec/unit/loggers/capture_spec.rb
247
+ - spec/unit/loggers/logger_spec.rb
232
248
  homepage: https://github.com/scoutapp/scout_apm_ruby_logging
233
249
  licenses:
234
250
  - MIT
@@ -251,7 +267,7 @@ requirements: []
251
267
  rubygems_version: 3.3.26
252
268
  signing_key:
253
269
  specification_version: 4
254
- summary: Ruby Logging Support
270
+ summary: Managed log monitoring for Ruby applications.
255
271
  test_files:
256
272
  - spec/data/config_test_1.yml
257
273
  - spec/data/mock_config.yml
@@ -260,3 +276,4 @@ test_files:
260
276
  - spec/spec_helper.rb
261
277
  - spec/unit/config_spec.rb
262
278
  - spec/unit/loggers/capture_spec.rb
279
+ - spec/unit/loggers/logger_spec.rb