legion-logging 1.5.6 → 1.5.19

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.
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Legion
6
+ module Logging
7
+ # Keeps the standard Logger interface used by Sinatra/APM on the async queue.
8
+ class OutputLogger < ::Logger
9
+ attr_accessor :async_writer
10
+
11
+ def add(severity, message = nil, progname = nil)
12
+ writer = @async_writer
13
+ return super unless writer && !writer.current_thread?
14
+
15
+ severity ||= UNKNOWN
16
+ return true if @logdev.nil? || severity < level
17
+
18
+ progname ||= @progname
19
+ if message.nil?
20
+ if block_given?
21
+ message = yield
22
+ else
23
+ message = progname
24
+ progname = @progname
25
+ end
26
+ end
27
+ enqueue_output(severity: severity, message: message, progname: progname)
28
+ end
29
+ alias log add
30
+
31
+ def <<(message)
32
+ writer = @async_writer
33
+ return super unless writer && !writer.current_thread?
34
+
35
+ enqueue_output(severity: nil, message: message, progname: nil)
36
+ end
37
+
38
+ private
39
+
40
+ def enqueue_output(severity:, message:, progname:) # rubocop:disable Naming/PredicateMethod
41
+ entry = Legion::Logging::AsyncWriter::OutputEntry.new(
42
+ severity: severity,
43
+ message: Legion::Logging::AsyncWriter.snapshot_value(value: message),
44
+ progname: Legion::Logging::AsyncWriter.snapshot_value(value: progname),
45
+ segments: Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[:legion_log_segments]),
46
+ method_ctx: Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[:legion_log_method]),
47
+ caller_trace: Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[:legion_log_caller]),
48
+ conv_id: Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[:legion_log_conv_id]),
49
+ request_id: Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[:legion_log_request_id]),
50
+ exchange_id: Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[:legion_log_exchange_id]),
51
+ chain_id: Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[:legion_log_chain_id]),
52
+ origin: { timestamp: Time.now.utc, thread: Thread.current.object_id }
53
+ )
54
+ raise ClosedQueueError, 'async logging writer is unavailable' unless @async_writer.alive? && @async_writer.push(entry)
55
+
56
+ true
57
+ end
58
+ end
59
+ end
60
+ end
@@ -74,6 +74,9 @@ module Legion
74
74
  end
75
75
 
76
76
  def all_patterns
77
+ return PATTERNS unless defined?(Legion::Settings)
78
+ return PATTERNS unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
79
+
77
80
  @all_patterns ||= build_patterns
78
81
  end
79
82
 
@@ -85,8 +88,6 @@ module Legion
85
88
  end
86
89
 
87
90
  def custom_patterns
88
- return {} unless defined?(Legion::Settings)
89
-
90
91
  raw = Legion::Settings.dig(:logging, :redactor, :custom_patterns)
91
92
  return {} unless raw.is_a?(Hash)
92
93
 
@@ -3,15 +3,31 @@
3
3
  module Legion
4
4
  module Logging
5
5
  module Settings
6
+ # Log levels forwarded to the transport bus (and in-process Hooks) by
7
+ # default. Preserves historic behavior: only warn/error/fatal logs build a
8
+ # structured event. Override at runtime via logging.transport.log_levels
9
+ # (e.g. add :debug/:info) — see Methods#forwardable_log_levels.
10
+ DEFAULT_FORWARD_LOG_LEVELS = %i[warn error fatal].freeze
11
+
6
12
  def self.default
7
13
  {
8
- level: :info,
9
- trace: true,
10
- trace_size: 4,
11
- extended: true,
12
- backtrace_limit: nil
14
+ level: :info,
15
+ trace: true,
16
+ trace_size: 4,
17
+ extended: true,
18
+ backtrace_limit: nil,
19
+ forward_log_levels: DEFAULT_FORWARD_LOG_LEVELS
13
20
  }
14
21
  end
22
+
23
+ def self.async_buffer_size
24
+ return 10_000 unless defined?(Legion::Settings) && Legion::Settings.respond_to?(:[])
25
+ return 10_000 unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
26
+
27
+ logging = Legion::Settings[:logging]
28
+ async = logging[:async] if logging.is_a?(Hash)
29
+ async.is_a?(Hash) ? (async[:buffer_size] || 10_000) : 10_000
30
+ end
15
31
  end
16
32
  end
17
33
  end
@@ -26,7 +26,7 @@ module Legion
26
26
  end
27
27
  true
28
28
  rescue StandardError => e
29
- Legion::Logging.error("FileTransport ship failed: #{e.message}") if defined?(Legion::Logging)
29
+ Kernel.warn("FileTransport ship failed: #{e.message}")
30
30
  false
31
31
  end
32
32
 
@@ -40,6 +40,7 @@ module Legion
40
40
 
41
41
  def settings_path
42
42
  return nil unless defined?(Legion::Settings)
43
+ return nil unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
43
44
 
44
45
  Legion::Settings.dig(:logging, :shipper, :file, :path)
45
46
  end
@@ -24,7 +24,7 @@ module Legion
24
24
  response = post(uri, body)
25
25
  response.is_a?(Net::HTTPSuccess)
26
26
  rescue StandardError => e
27
- Legion::Logging.error("HttpTransport ship failed: #{e.message}") if defined?(Legion::Logging)
27
+ Kernel.warn("HttpTransport ship failed: #{e.message}")
28
28
  false
29
29
  end
30
30
 
@@ -36,7 +36,7 @@ module Legion
36
36
  apply_auth(req, uri)
37
37
 
38
38
  Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https',
39
- open_timeout: 5, read_timeout: 10) do |http|
39
+ open_timeout: 5, read_timeout: 10, write_timeout: 10) do |http|
40
40
  http.request(req, body)
41
41
  end
42
42
  end
@@ -67,12 +67,14 @@ module Legion
67
67
 
68
68
  def auth_token
69
69
  return nil unless defined?(Legion::Settings)
70
+ return nil unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
70
71
 
71
72
  Legion::Settings.dig(:logging, :shipper, :auth_token)
72
73
  end
73
74
 
74
75
  def resolve_endpoint
75
76
  return nil unless defined?(Legion::Settings)
77
+ return nil unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
76
78
 
77
79
  Legion::Settings.dig(:logging, :shipper, :endpoint)
78
80
  end
@@ -8,134 +8,183 @@ module Legion
8
8
  module Logging
9
9
  module Shipper
10
10
  LEVEL_ORDER = %w[debug info warn error fatal].freeze
11
-
12
- TRANSPORTS = {
13
- file: FileTransport,
14
- http: HttpTransport
15
- }.freeze
11
+ TRANSPORTS = { file: FileTransport, http: HttpTransport }.freeze
12
+ MUTEX = Mutex.new
13
+ WAKE = ConditionVariable.new
14
+ CHANGED = ConditionVariable.new
15
+ private_constant :MUTEX, :WAKE, :CHANGED
16
+
17
+ @buffer = []
18
+ @accepted = 0
19
+ @delivered = 0
20
+ @attempt = 0
21
+ @flush_target = 0
16
22
 
17
23
  class << self
18
24
  def ship(event)
19
25
  return unless enabled?
20
26
  return unless shippable_level?(event[:level] || event['level'])
27
+ return unless TRANSPORTS.key?(transport_type)
21
28
 
22
- redacted = Redactor.redact(event)
23
- transport = TRANSPORTS[transport_type]
24
- buffer_event(redacted) if transport
25
- end
29
+ redacted = Redactor.redact(event)
30
+ raise ClosedQueueError, 'log shipper is stopping' unless start
26
31
 
27
- def flush
28
- @mutex ||= Mutex.new
29
- batch = @mutex.synchronize do
30
- return true if @buffer.nil? || @buffer.empty?
32
+ MUTEX.synchronize do
33
+ raise ThreadError, 'log shipper cannot enqueue from its delivery thread' if Thread.current.equal?(@flush_thread)
31
34
 
32
- flushed = @buffer.dup
33
- @buffer.clear
34
- flushed
35
+ CHANGED.wait(MUTEX) while !@stopping && @accepted - @delivered >= @capacity
36
+ raise ClosedQueueError, 'log shipper is stopping' if @stopping
37
+
38
+ @buffer << redacted
39
+ @accepted += 1
40
+ @flush_requested = true if @buffer.size >= @batch_size
41
+ WAKE.signal if @flush_requested
35
42
  end
43
+ true
44
+ end
45
+
46
+ def flush(timeout: 5)
47
+ return true if MUTEX.synchronize { @accepted == @delivered }
48
+ return false unless start
49
+
50
+ deadline = timeout && (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout)
51
+ MUTEX.synchronize do
52
+ return false if Thread.current.equal?(@flush_thread)
53
+
54
+ target = @accepted
55
+ attempt = @attempt
56
+ @flush_target = [@flush_target, target].max
57
+ @flush_requested = true
58
+ WAKE.signal
59
+ while @delivered < target
60
+ return false if @attempt > attempt && !@last_success
61
+ return false unless @flush_thread&.alive?
36
62
 
37
- transport = TRANSPORTS[transport_type]
38
- return true unless transport
63
+ remaining = deadline && (deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC))
64
+ return false if remaining && remaining <= 0
39
65
 
40
- delivered = deliver(transport, batch)
41
- @mutex.synchronize { @buffer.prepend(*batch) } unless delivered
42
- delivered
66
+ CHANGED.wait(MUTEX, remaining)
67
+ end
68
+ true
69
+ end
43
70
  end
44
71
 
45
72
  def start
46
- @start_mutex ||= Mutex.new
47
- @start_mutex.synchronize do
48
- return unless enabled?
49
- return if @flush_thread&.alive?
50
-
51
- @buffer ||= []
52
- @mutex ||= Mutex.new
53
- @running = true
54
- interval = flush_interval
55
- @flush_thread = Thread.new do
56
- while @running
57
- sleep interval
58
- flush
59
- end
73
+ return false unless enabled?
74
+
75
+ MUTEX.synchronize do
76
+ return !@stopping if @flush_thread&.alive?
77
+
78
+ @capacity = Legion::Settings.dig(:logging, :shipper, :buffer_size) || 10_000
79
+ @batch_size = [batch_size, @capacity].min
80
+ @interval = flush_interval
81
+ unless @capacity.is_a?(Integer) && @capacity.positive? && @batch_size.is_a?(Integer) && @batch_size.positive? &&
82
+ @interval.is_a?(Numeric) && @interval.positive?
83
+ raise ArgumentError, 'shipper buffer_size, batch_size, and flush_interval must be positive'
60
84
  end
85
+
86
+ @transport = TRANSPORTS[transport_type]
87
+ return false unless @transport
88
+
89
+ @stopping = false
90
+ @flush_requested = @buffer.size >= @batch_size
91
+ @flush_thread = Thread.new { consume }
61
92
  @flush_thread.name = 'legion-log-shipper'
62
93
  @flush_thread.abort_on_exception = false
94
+ true
63
95
  end
64
96
  end
65
97
 
66
- def stop
67
- @running = false
68
- thread = @flush_thread
69
- @flush_thread = nil
70
- thread&.join(5)
71
- flush
98
+ def stop(timeout: 5)
99
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout if timeout
100
+ thread = MUTEX.synchronize do
101
+ @stopping = true
102
+ WAKE.broadcast
103
+ CHANGED.broadcast
104
+ if !@flush_thread&.alive? && @accepted > @delivered
105
+ @flush_thread = Thread.new { consume }
106
+ @flush_thread.name = 'legion-log-shipper'
107
+ @flush_thread.abort_on_exception = false
108
+ end
109
+ @flush_thread
110
+ end
111
+ return MUTEX.synchronize { @accepted == @delivered } unless thread
112
+ return false if Thread.current.equal?(thread)
113
+
114
+ remaining = [deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC), 0].max if deadline
115
+ thread.join(remaining)
116
+ MUTEX.synchronize { !thread.alive? && @accepted == @delivered }
72
117
  end
73
118
 
74
119
  def enabled?
75
120
  return false unless defined?(Legion::Settings)
121
+ return false unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
76
122
 
77
123
  Legion::Settings.dig(:logging, :shipper, :enabled) == true
78
124
  end
79
125
 
80
126
  private
81
127
 
82
- def buffer_event(event)
83
- @buffer ||= []
84
- @mutex ||= Mutex.new
85
-
86
- full = false
87
- @mutex.synchronize do
88
- @buffer << event
89
- full = @buffer.size >= batch_size
128
+ def consume
129
+ loop do
130
+ batch = MUTEX.synchronize do
131
+ WAKE.wait(MUTEX, @interval) unless @flush_requested || @stopping
132
+ @flush_requested = false
133
+ @buffer.shift(@batch_size)
134
+ end
135
+ delivered = batch.empty? || deliver(@transport, batch)
136
+ stopping = MUTEX.synchronize do
137
+ @attempt += 1
138
+ @last_success = delivered
139
+ if delivered
140
+ @delivered += batch.size
141
+ @flush_requested = @buffer.size >= @batch_size || @delivered < @flush_target
142
+ else
143
+ @buffer.prepend(*batch)
144
+ end
145
+ CHANGED.broadcast
146
+ @stopping && (!delivered || @buffer.empty?)
147
+ end
148
+ break if stopping
149
+ end
150
+ ensure
151
+ MUTEX.synchronize do
152
+ @stopping = true
153
+ CHANGED.broadcast
90
154
  end
91
-
92
- flush if full
93
155
  end
94
156
 
95
157
  def deliver(transport, batch)
96
- if transport.respond_to?(:ship_batch)
97
- transport.ship_batch(batch)
98
- else
99
- batch.all? { |event| transport.ship(event) }
100
- end
158
+ transport.ship_batch(batch)
101
159
  rescue StandardError => e
102
- Legion::Logging.error("Shipper deliver failed: #{e.message}") if defined?(Legion::Logging)
160
+ Kernel.warn("Shipper deliver failed: #{e.message}")
103
161
  false
104
162
  end
105
163
 
106
164
  def shippable_level?(level)
107
165
  return true if level.nil?
108
166
 
109
- min = minimum_level
110
- LEVEL_ORDER.index(level.to_s.downcase).to_i >= LEVEL_ORDER.index(min).to_i
167
+ LEVEL_ORDER.index(level.to_s.downcase).to_i >= LEVEL_ORDER.index(minimum_level).to_i
111
168
  end
112
169
 
113
170
  def transport_type
114
- return :file unless defined?(Legion::Settings)
115
-
116
- key = Legion::Settings.dig(:logging, :shipper, :transport)
171
+ key = Legion::Settings.dig(:logging, :shipper, :transport) if defined?(Legion::Settings)
117
172
  key ? key.to_sym : :file
118
173
  end
119
174
 
120
175
  def batch_size
121
- return 100 unless defined?(Legion::Settings)
122
-
123
176
  Legion::Settings.dig(:logging, :shipper, :batch_size) || 100
124
177
  end
125
178
 
126
179
  def flush_interval
127
- return 5 unless defined?(Legion::Settings)
128
-
129
180
  Legion::Settings.dig(:logging, :shipper, :flush_interval) || 5
130
181
  end
131
182
 
132
183
  def minimum_level
133
- return 'warn' unless defined?(Legion::Settings)
134
-
135
184
  levels = Legion::Settings.dig(:logging, :shipper, :levels)
136
185
  return 'warn' unless levels.is_a?(Array) && !levels.empty?
137
186
 
138
- levels.min_by { |l| LEVEL_ORDER.index(l.to_s) || 99 }.to_s
187
+ levels.min_by { |level| LEVEL_ORDER.index(level.to_s) || 99 }.to_s
139
188
  end
140
189
  end
141
190
  end
@@ -29,7 +29,7 @@ module Legion
29
29
  req.body = ::JSON.dump({ event: redact_phi(event.to_s), time: Time.now.to_f })
30
30
 
31
31
  Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https',
32
- open_timeout: 5, read_timeout: 10) do |http|
32
+ open_timeout: 5, read_timeout: 10, write_timeout: 10) do |http|
33
33
  http.request(req)
34
34
  end
35
35
  rescue StandardError => e
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Logging
5
- VERSION = '1.5.6'
5
+ VERSION = '1.5.19'
6
6
  end
7
7
  end
@@ -76,36 +76,37 @@ module Legion
76
76
  Hooks.clear_hooks!
77
77
  end
78
78
 
79
- def setup(level: 'debug', format: :text, async: true, **options)
80
- output(**options)
81
- log_level(level)
82
- log_format(format: format, **options)
83
- @color = options[:color]
84
- @color = format != :json && (options[:color] || (options[:color].nil? && options[:log_file].nil?))
85
- @current_settings = {
86
- level: level,
87
- format: format.to_sym,
88
- async: async,
89
- trace: options.fetch(:trace, true),
90
- trace_size: options.fetch(:trace_size, 4),
91
- extended: options.fetch(:extended, true),
92
- log_file: options[:log_file],
93
- log_stdout: options[:log_stdout],
94
- include_pid: options.fetch(:include_pid, false),
95
- color: @color
96
- }.freeze
97
- @configuration_generation = configuration_generation + 1
98
- Legion::Logging::Redactor.refresh_patterns! if defined?(Legion::Logging::Redactor)
99
- if async
100
- buffer = if defined?(Legion::Settings) && Legion::Settings.respond_to?(:[])
101
- logging_settings = Legion::Settings[:logging]
102
- async_settings = logging_settings[:async] if logging_settings.is_a?(Hash)
103
- async_settings[:buffer_size] if async_settings.is_a?(Hash)
104
- end
105
- buffer ||= 10_000
106
- start_async_writer(buffer_size: buffer)
107
- else
108
- stop_async_writer
79
+ def setup(level: 'debug', format: :text, async: true, shutdown_timeout: 2, **options)
80
+ with_lifecycle_lock do
81
+ return false if @async_writer && stop_async_writer(timeout: shutdown_timeout) == false
82
+ return false if output(**options) == false
83
+
84
+ log_level(level)
85
+ log_format(format: format, **options)
86
+ @color = options[:color]
87
+ @color = format != :json && (options[:color] || (options[:color].nil? && options[:log_file].nil?))
88
+ @current_settings = {
89
+ level: level,
90
+ format: format.to_sym,
91
+ async: async,
92
+ trace: options.fetch(:trace, true),
93
+ trace_size: options.fetch(:trace_size, 4),
94
+ extended: options.fetch(:extended, true),
95
+ log_file: options[:log_file],
96
+ log_stdout: options[:log_stdout],
97
+ include_pid: options.fetch(:include_pid, false),
98
+ color: @color
99
+ }.freeze
100
+ @configuration_generation = configuration_generation + 1
101
+ Legion::Logging::Redactor.refresh_patterns! if defined?(Legion::Logging::Redactor)
102
+ if async
103
+ start_async_writer(buffer_size: Legion::Logging::Settings.async_buffer_size)
104
+ else
105
+ @async_writer = nil
106
+ @async = false
107
+ log.async_writer = nil
108
+ true
109
+ end
109
110
  end
110
111
  end
111
112
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-logging
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.6
4
+ version: 1.5.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -70,6 +70,7 @@ files:
70
70
  - lib/legion/logging/method_tracer.rb
71
71
  - lib/legion/logging/methods.rb
72
72
  - lib/legion/logging/multi_io.rb
73
+ - lib/legion/logging/output_logger.rb
73
74
  - lib/legion/logging/redactor.rb
74
75
  - lib/legion/logging/settings.rb
75
76
  - lib/legion/logging/shipper.rb