logstruct 0.1.12 → 0.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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +133 -1
  3. data/README.md +76 -68
  4. data/lib/log_struct/concerns/configuration.rb +139 -91
  5. data/lib/log_struct/concerns/error_handling.rb +36 -6
  6. data/lib/log_struct/config_struct/error_handling_modes.rb +3 -1
  7. data/lib/log_struct/config_struct/filters.rb +116 -33
  8. data/lib/log_struct/config_struct/integrations.rb +0 -4
  9. data/lib/log_struct/config_struct/limits.rb +29 -0
  10. data/lib/log_struct/configuration.rb +2 -0
  11. data/lib/log_struct/enums/event.rb +3 -1
  12. data/lib/log_struct/enums/log_field.rb +5 -0
  13. data/lib/log_struct/formatter.rb +304 -56
  14. data/lib/log_struct/hash_utils.rb +33 -4
  15. data/lib/log_struct/integrations/action_mailer/error_handling.rb +78 -20
  16. data/lib/log_struct/integrations/action_mailer/fallback_error_handlers.rb +81 -0
  17. data/lib/log_struct/integrations/action_mailer/metadata_collection.rb +0 -20
  18. data/lib/log_struct/integrations/action_mailer.rb +22 -17
  19. data/lib/log_struct/integrations/active_job/log_subscriber.rb +177 -25
  20. data/lib/log_struct/integrations/active_job.rb +2 -7
  21. data/lib/log_struct/integrations/active_model_serializers.rb +7 -1
  22. data/lib/log_struct/integrations/active_record.rb +39 -17
  23. data/lib/log_struct/integrations/active_storage.rb +31 -31
  24. data/lib/log_struct/integrations/ahoy.rb +19 -15
  25. data/lib/log_struct/integrations/carrierwave.rb +55 -51
  26. data/lib/log_struct/integrations/dotenv.rb +22 -32
  27. data/lib/log_struct/integrations/good_job/logger.rb +30 -24
  28. data/lib/log_struct/integrations/good_job.rb +111 -40
  29. data/lib/log_struct/integrations/host_authorization.rb +47 -36
  30. data/lib/log_struct/integrations/lograge.rb +39 -4
  31. data/lib/log_struct/integrations/puma.rb +74 -34
  32. data/lib/log_struct/integrations/rack_error_handler/debug_exceptions_logging.rb +35 -0
  33. data/lib/log_struct/integrations/rack_error_handler/error_lines.rb +160 -0
  34. data/lib/log_struct/integrations/rack_error_handler/middleware.rb +14 -169
  35. data/lib/log_struct/integrations/rack_error_handler/parse_error_logging.rb +36 -0
  36. data/lib/log_struct/integrations/rack_error_handler.rb +20 -4
  37. data/lib/log_struct/integrations/rack_middleware.rb +23 -0
  38. data/lib/log_struct/integrations/rails_log_subscribers.rb +32 -0
  39. data/lib/log_struct/integrations/request_context/middleware.rb +4 -9
  40. data/lib/log_struct/integrations/shrine.rb +48 -73
  41. data/lib/log_struct/integrations/sidekiq/job_context_middleware.rb +32 -0
  42. data/lib/log_struct/integrations/sidekiq/logger.rb +17 -9
  43. data/lib/log_struct/integrations/sidekiq.rb +98 -7
  44. data/lib/log_struct/integrations/sorbet.rb +10 -46
  45. data/lib/log_struct/integrations.rb +3 -2
  46. data/lib/log_struct/log/{good_job/enqueue.rb → active_job/discard.rb} +13 -13
  47. data/lib/log_struct/log/active_job/enqueue.rb +0 -4
  48. data/lib/log_struct/log/{good_job/schedule.rb → active_job/error.rb} +15 -15
  49. data/lib/log_struct/log/{good_job/start.rb → active_job/retry.rb} +13 -13
  50. data/lib/log_struct/log/{good_job/finish.rb → active_job/retry_stopped.rb} +13 -17
  51. data/lib/log_struct/log/active_job.rb +4 -0
  52. data/lib/log_struct/log/active_storage/delete.rb +6 -2
  53. data/lib/log_struct/log/active_storage/download.rb +2 -4
  54. data/lib/log_struct/log/active_storage/exist.rb +2 -2
  55. data/lib/log_struct/log/active_storage/metadata.rb +4 -4
  56. data/lib/log_struct/log/active_storage/upload.rb +2 -10
  57. data/lib/log_struct/log/active_storage/url.rb +2 -6
  58. data/lib/log_struct/log/active_storage.rb +2 -3
  59. data/lib/log_struct/log/error.rb +24 -1
  60. data/lib/log_struct/log/good_job.rb +0 -4
  61. data/lib/log_struct/log/puma/error.rb +63 -0
  62. data/lib/log_struct/log/puma.rb +1 -0
  63. data/lib/log_struct/log/security/blocked_host.rb +0 -4
  64. data/lib/log_struct/log/shrine/exist.rb +0 -2
  65. data/lib/log_struct/log/shrine/metadata.rb +2 -4
  66. data/lib/log_struct/log/shrine/upload.rb +4 -0
  67. data/lib/log_struct/log/sidekiq/error.rb +63 -0
  68. data/lib/log_struct/log/{active_storage/stream.rb → sidekiq/log.rb} +17 -11
  69. data/lib/log_struct/log/sidekiq.rb +3 -45
  70. data/lib/log_struct/log.rb +4 -2
  71. data/lib/log_struct/logged_errors.rb +25 -0
  72. data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +2 -3
  73. data/lib/log_struct/param_filters.rb +51 -38
  74. data/lib/log_struct/params_errors.rb +78 -0
  75. data/lib/log_struct/rails_boot_banner_silencer.rb +7 -2
  76. data/lib/log_struct/railtie.rb +29 -14
  77. data/lib/log_struct/semantic_logger/color_formatter.rb +60 -53
  78. data/lib/log_struct/semantic_logger/concerns/log_methods.rb +65 -57
  79. data/lib/log_struct/semantic_logger/formatter.rb +79 -57
  80. data/lib/log_struct/semantic_logger/logger.rb +29 -53
  81. data/lib/log_struct/semantic_logger/reopen_after_fork.rb +40 -0
  82. data/lib/log_struct/semantic_logger/setup.rb +115 -112
  83. data/lib/log_struct/shared/serialize_common.rb +2 -7
  84. data/lib/log_struct/string_scrubber.rb +229 -20
  85. data/lib/log_struct/version.rb +1 -1
  86. data/lib/log_struct.rb +6 -4
  87. data/logstruct.gemspec +19 -10
  88. metadata +63 -11
  89. data/lib/log_struct/builders/active_job.rb +0 -87
  90. data/lib/log_struct/integrations/good_job/log_subscriber.rb +0 -189
  91. data/lib/log_struct/integrations/rack.rb +0 -31
@@ -10,89 +10,97 @@ module LogStruct
10
10
  requires_ancestor { LogStruct::SemanticLogger::Logger }
11
11
 
12
12
  # Override log methods to handle LogStruct types and broadcast
13
- sig { params(message: T.untyped, payload: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
14
- def debug(message = nil, payload = nil, &block)
13
+ sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
14
+ def debug(message = nil, payload = nil, exception = nil, &block)
15
+ result = super(*semantic_logger_args(message, payload, exception), &block)
15
16
  instrument_log(message, :debug)
16
- result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
17
- super(nil, payload: message, &block)
18
- else
19
- super
20
- end
21
- broadcasts.each do |logger|
22
- next unless logger.respond_to?(:debug)
23
- message.is_a?(String) ? logger.debug(message) : (logger.debug(&block) if block)
24
- end
17
+ broadcast_message(:debug, message, block)
25
18
  result
26
19
  end
27
20
 
28
- sig { params(message: T.untyped, payload: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
29
- def info(message = nil, payload = nil, &block)
21
+ sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
22
+ def info(message = nil, payload = nil, exception = nil, &block)
23
+ result = super(*semantic_logger_args(message, payload, exception), &block)
30
24
  instrument_log(message, :info)
31
- result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
32
- super(nil, payload: message, &block)
33
- else
34
- super
35
- end
36
- broadcasts.each do |logger|
37
- next unless logger.respond_to?(:info)
38
- message.is_a?(String) ? logger.info(message) : (logger.info(&block) if block)
39
- end
25
+ broadcast_message(:info, message, block)
40
26
  result
41
27
  end
42
28
 
43
- sig { params(message: T.untyped, payload: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
44
- def warn(message = nil, payload = nil, &block)
29
+ sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
30
+ def warn(message = nil, payload = nil, exception = nil, &block)
31
+ result = super(*semantic_logger_args(message, payload, exception), &block)
45
32
  instrument_log(message, :warn)
46
- result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
47
- super(nil, payload: message, &block)
48
- else
49
- super
50
- end
51
- broadcasts.each do |logger|
52
- next unless logger.respond_to?(:warn)
53
- message.is_a?(String) ? logger.warn(message) : (logger.warn(&block) if block)
54
- end
33
+ broadcast_message(:warn, message, block)
55
34
  result
56
35
  end
57
36
 
58
- sig { params(message: T.untyped, payload: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
59
- def error(message = nil, payload = nil, &block)
37
+ sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
38
+ def error(message = nil, payload = nil, exception = nil, &block)
39
+ result = super(*semantic_logger_args(message, payload, exception), &block)
60
40
  instrument_log(message, :error)
61
- result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
62
- super(nil, payload: message, &block)
63
- else
64
- super
65
- end
66
- broadcasts.each do |logger|
67
- next unless logger.respond_to?(:error)
68
- message.is_a?(String) ? logger.error(message) : (logger.error(&block) if block)
69
- end
41
+ broadcast_message(:error, message, block)
70
42
  result
71
43
  end
72
44
 
73
- sig { params(message: T.untyped, payload: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
74
- def fatal(message = nil, payload = nil, &block)
45
+ sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
46
+ def fatal(message = nil, payload = nil, exception = nil, &block)
47
+ result = super(*semantic_logger_args(message, payload, exception), &block)
75
48
  instrument_log(message, :fatal)
76
- result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
77
- super(nil, payload: message, &block)
78
- else
79
- super
80
- end
81
- broadcasts.each do |logger|
82
- next unless logger.respond_to?(:fatal)
83
- message.is_a?(String) ? logger.fatal(message) : (logger.fatal(&block) if block)
84
- end
49
+ broadcast_message(:fatal, message, block)
85
50
  result
86
51
  end
87
52
 
88
53
  private
89
54
 
90
- # Instrument log events for subscribers
55
+ # SemanticLogger's arguments for a line: a LogStruct entry or struct goes in the payload, and a Hash is the
56
+ # payload itself, so its keys are written at the top level of the line. An exception can come second
57
+ # (logger.error({id: 1}, error)) or third.
58
+ sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped).returns([T.untyped, T.untyped, T.untyped]) }
59
+ def semantic_logger_args(message, payload, exception)
60
+ if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct)
61
+ [nil, {payload: message}, nil]
62
+ elsif message.is_a?(Hash)
63
+ [nil, message, payload.is_a?(Exception) ? payload : exception]
64
+ else
65
+ [message, payload, exception]
66
+ end
67
+ end
68
+
69
+ # Sends a LogStruct entry to the log.logstruct subscribers, after its line is queued to be written, so a
70
+ # subscriber's error can't lose it. The error is handled like the app's other errors (standard_errors, which
71
+ # raises it from the log call by default), and the lines written while handling it aren't sent to the
72
+ # subscribers again.
91
73
  sig { params(message: T.untyped, level: Symbol).void }
92
74
  def instrument_log(message, level)
93
75
  return unless message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct)
76
+ return if Thread.current[:logstruct_instrumenting_log]
77
+
78
+ Thread.current[:logstruct_instrumenting_log] = true
79
+ begin
80
+ ::ActiveSupport::Notifications.instrument("log.logstruct", log: message, level: level)
81
+ rescue => e
82
+ LogStruct.handle_exception(e, source: Source::App, context: {notification: "log.logstruct"})
83
+ ensure
84
+ Thread.current[:logstruct_instrumenting_log] = false
85
+ end
86
+ end
94
87
 
95
- ::ActiveSupport::Notifications.instrument("log.logstruct", log: message, level: level)
88
+ # Other loggers get String messages, and what blocks return, scrubbed like a log line's strings
89
+ sig { params(level: Symbol, message: T.untyped, block: T.nilable(T.proc.returns(String))).void }
90
+ def broadcast_message(level, message, block)
91
+ broadcasts.each do |logger|
92
+ next unless logger.respond_to?(level)
93
+
94
+ if message.is_a?(String)
95
+ logger.public_send(level, StringScrubber.scrub(message))
96
+ elsif block
97
+ logger.public_send(level) do
98
+ # The block type says String, but nothing checks what a block returns
99
+ text = T.let(block.call, T.untyped)
100
+ text.is_a?(String) ? StringScrubber.scrub(text) : text
101
+ end
102
+ end
103
+ end
96
104
  end
97
105
  end
98
106
  end
@@ -3,53 +3,13 @@
3
3
 
4
4
  require "semantic_logger"
5
5
  require_relative "../formatter"
6
+ require_relative "../params_errors"
6
7
 
7
8
  module LogStruct
8
9
  module SemanticLogger
9
- # High-Performance JSON Formatter with LogStruct Integration
10
- #
11
- # This formatter extends SemanticLogger's JSON formatter to provide optimal
12
- # JSON serialization performance while preserving all LogStruct features
13
- # including data filtering, sensitive data scrubbing, and type-safe structures.
14
- #
15
- # ## Performance Advantages Over Rails Logger:
16
- #
17
- # ### Serialization Performance
18
- # - **Direct JSON generation**: Bypasses intermediate object creation
19
- # - **Streaming serialization**: Memory-efficient processing of large objects
20
- # - **Type-optimized paths**: Fast serialization for common data types
21
- # - **Zero-copy operations**: Minimal memory allocation during serialization
22
- #
23
- # ### Memory Efficiency
24
- # - **Object reuse**: Formatter instances are reused across log calls
25
- # - **Lazy evaluation**: Only processes data that will be included in output
26
- # - **Efficient buffering**: Optimal buffer sizes for JSON generation
27
- # - **Garbage collection friendly**: Minimal object allocation reduces GC pressure
28
- #
29
- # ### Integration Benefits
30
- # - **LogStruct compatibility**: Native support for typed log structures
31
- # - **Filter preservation**: Maintains all LogStruct filtering capabilities
32
- # - **Scrubbing integration**: Seamless sensitive data scrubbing
33
- # - **Error handling**: Robust handling of serialization errors
34
- #
35
- # ## Feature Preservation:
36
- # This formatter maintains full compatibility with LogStruct's features:
37
- # - Sensitive data filtering (passwords, tokens, etc.)
38
- # - Recursive object scrubbing and processing
39
- # - Type-safe log structure handling
40
- # - Custom field transformations
41
- # - Metadata preservation and enrichment
42
- #
43
- # ## JSON Output Structure:
44
- # The formatter produces consistent, parseable JSON that includes:
45
- # - Standard log fields (timestamp, level, message, logger name)
46
- # - LogStruct-specific fields (source, event, context)
47
- # - SemanticLogger metadata (process ID, thread ID, tags)
48
- # - Application-specific payload data
49
- #
50
- # This combination provides the performance benefits of SemanticLogger with
51
- # the structured data benefits of LogStruct, resulting in faster, more
52
- # reliable logging for high-traffic applications.
10
+ # Writes each SemanticLogger line as LogStruct's JSON: LogStruct entries, Hashes, messages and exceptions, with
11
+ # SemanticLogger's tags and named tags (the request ID and the running Sidekiq job's context among them). LogStruct::Formatter
12
+ # filters and scrubs every value, within the size limits in config.limits.
53
13
  class Formatter < ::SemanticLogger::Formatters::Json
54
14
  extend T::Sig
55
15
 
@@ -64,12 +24,30 @@ module LogStruct
64
24
  # Extract LogStruct from various locations where it might be stored
65
25
  logstruct = extract_logstruct(log)
66
26
 
27
+ # The request ID, the running Sidekiq job's context and the other named tags (logger.tagged(user_id: 1))
28
+ # go in req_id and ctx, and tags (logger.tagged("billing"), config.log_tags, ActiveJob's) in tags
29
+ named_tags = log.named_tags
30
+ request_id = named_tags[:request_id]
31
+ job_context = named_tags[:sidekiq_context]
32
+ context = named_tags.except(:request_id, :sidekiq_context)
33
+ context = context.merge(job_context) if job_context.is_a?(Hash)
34
+ metadata = {
35
+ request_id: request_id.is_a?(String) ? request_id : nil,
36
+ context: context.empty? ? nil : context,
37
+ tags: log.tags.empty? ? nil : log.tags
38
+ }
39
+
67
40
  json = if logstruct
68
41
  # Use our formatter to process LogStruct types directly
69
- @logstruct_formatter.call(log.level, log.time, log.name, logstruct)
70
- elsif log.payload.is_a?(Hash) || log.payload.is_a?(T::Struct)
71
- # Process hashes and T::Structs through our formatter
72
- @logstruct_formatter.call(log.level, log.time, log.name, log.payload)
42
+ @logstruct_formatter.call(log.level, log.time, log.name, logstruct, **metadata)
43
+ elsif log.exception
44
+ @logstruct_formatter.call(log.level, log.time, log.name, exception_log(log), **metadata)
45
+ elsif log.payload.is_a?(Hash)
46
+ # Process hashes through our formatter
47
+ payload = with_message(without_reserved_keys(log.payload), log.message)
48
+ @logstruct_formatter.call(log.level, log.time, log.name, payload, **metadata)
49
+ elsif log.payload.is_a?(T::Struct)
50
+ @logstruct_formatter.call(log.level, log.time, log.name, log.payload, **metadata)
73
51
  else
74
52
  # For plain messages, create a Plain log entry
75
53
  message_data = log.payload || log.message
@@ -77,15 +55,7 @@ module LogStruct
77
55
  message: message_data,
78
56
  timestamp: log.time
79
57
  )
80
- @logstruct_formatter.call(log.level, log.time, log.name, plain_log)
81
- end
82
-
83
- # Add request_id from named_tags if present
84
- request_id = log.named_tags[:request_id]
85
- if request_id
86
- data = JSON.parse(json)
87
- data["req_id"] = request_id
88
- json = data.to_json
58
+ @logstruct_formatter.call(log.level, log.time, log.name, plain_log, **metadata)
89
59
  end
90
60
 
91
61
  # SemanticLogger appenders typically add their own newline. Avoid double newlines by stripping ours.
@@ -94,6 +64,58 @@ module LogStruct
94
64
 
95
65
  private
96
66
 
67
+ # logger.error(exception) and logger.error("Message", exception): an error line with the
68
+ # exception's class, message and backtrace. A logged message goes in msg, and the exception's
69
+ # message in error_message. A Hash logged with them adds its keys to the line.
70
+ sig { params(log: ::SemanticLogger::Log).returns(LogStruct::Log::Error) }
71
+ def exception_log(log)
72
+ exception = T.let(log.exception, Exception)
73
+ message = log.message
74
+ logged_message = (message.is_a?(String) && !message.empty?) ? message : nil
75
+ payload = log.payload
76
+ # Without the input a params parser quoted
77
+ exception_message = ParamsErrors.messages(exception).first
78
+ LogStruct::Log::Error.new(
79
+ source: Source::App,
80
+ error_class: exception.class,
81
+ message: logged_message || exception_message,
82
+ error_message: logged_message ? exception_message : nil,
83
+ backtrace: exception.backtrace,
84
+ additional_data: payload.is_a?(Hash) ? without_reserved_keys(payload) : nil,
85
+ timestamp: log.time
86
+ )
87
+ end
88
+
89
+ # A message logged beside a Hash (logger.info("Saved", id: 1)) goes in msg, unless the Hash
90
+ # has its own
91
+ sig { params(payload: T::Hash[T.untyped, T.untyped], message: T.untyped).returns(T::Hash[T.untyped, T.untyped]) }
92
+ def with_message(payload, message)
93
+ return payload unless message.is_a?(String) && !message.empty?
94
+ return payload if payload.key?(:msg) || payload.key?("msg")
95
+
96
+ {msg: message}.merge(payload)
97
+ end
98
+
99
+ # A logged Hash may set its own src and evt, but the line's timestamp and level come from the logger
100
+ HASH_RESERVED_KEYS = T.let(%w[ts lvl].freeze, T::Array[String])
101
+
102
+ sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[T.untyped, T.untyped]) }
103
+ def without_reserved_keys(hash)
104
+ reserved = hash.keys.select { |key| HASH_RESERVED_KEYS.include?(key.to_s) }
105
+ return hash if reserved.empty?
106
+
107
+ # A copy, since other appenders format the same Hash
108
+ copy = hash.dup
109
+ reserved.each do |key|
110
+ LogStruct.handle_exception(
111
+ ArgumentError.new("A logged Hash attempted to set reserved key: #{key}"),
112
+ source: Source::Internal
113
+ )
114
+ copy.delete(key)
115
+ end
116
+ copy
117
+ end
118
+
97
119
  # Extract a LogStruct from the various places it might be stored in a SemanticLogger::Log
98
120
  sig do
99
121
  params(log: ::SemanticLogger::Log).returns(
@@ -1,68 +1,28 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "active_support/logger"
4
5
  require "semantic_logger"
5
6
  require_relative "concerns/log_methods"
6
7
 
7
8
  module LogStruct
8
9
  module SemanticLogger
9
- # High-Performance Logger with LogStruct Integration
10
- #
11
- # This logger extends SemanticLogger::Logger to provide optimal logging performance
12
- # while seamlessly integrating with LogStruct's typed logging system.
13
- #
14
- # ## Key Benefits Over Rails.logger:
15
- #
16
- # ### Performance
17
- # - **10-100x faster** than Rails' default logger for high-volume applications
18
- # - **Non-blocking I/O**: Uses background threads for actual log writes
19
- # - **Minimal memory allocation**: Efficient object reuse and zero-copy operations
20
- # - **Batched writes**: Reduces system calls by batching multiple log entries
21
- #
22
- # ### Reliability
23
- # - **Thread-safe operations**: Safe for use in multi-threaded environments
24
- # - **Error resilience**: Logger failures don't crash your application
25
- # - **Graceful fallbacks**: Continues operating even if appenders fail
26
- #
27
- # ### Features
28
- # - **Structured logging**: Native support for LogStruct types and hashes
29
- # - **Rich metadata**: Automatic inclusion of process ID, thread ID, timestamps
30
- # - **Tagged context**: Hierarchical tagging for request/job tracking
31
- # - **Multiple destinations**: Simultaneously log to files, STDOUT, cloud services
32
- #
33
- # ### Development Experience
34
- # - **Colorized output**: Beautiful ANSI-colored logs in development
35
- # - **Detailed timing**: Built-in measurement of log processing time
36
- # - **Context preservation**: Maintains Rails.logger compatibility
37
- #
38
- # ## Usage Examples
39
- #
40
- # The logger automatically handles LogStruct types, hashes, and plain messages:
10
+ # Rails.logger's replacement: a SemanticLogger logger that also takes LogStruct's typed entries and Hashes, and
11
+ # passes each line on to the loggers added with broadcast_to.
41
12
  #
42
13
  # ```ruby
43
14
  # logger = LogStruct::SemanticLogger::Logger.new("MyApp")
44
15
  #
45
- # # LogStruct typed logging (optimal performance)
46
- # log_entry = LogStruct::Log::Plain.new(
47
- # message: "User authenticated",
48
- # source: LogStruct::Source::App,
49
- # event: LogStruct::Event::Security
50
- # )
51
- # logger.info(log_entry)
16
+ # # A typed entry
17
+ # logger.info(LogStruct::Log::Plain.new(message: "User authenticated", additional_data: {user_id: 123}))
52
18
  #
53
- # # Hash logging (automatically structured)
54
- # logger.info({
55
- # action: "user_login",
56
- # user_id: 123,
57
- # ip_address: "192.168.1.1"
58
- # })
19
+ # # A Hash: its keys are written at the top level of the line
20
+ # logger.info(action: "user_login", user_id: 123)
59
21
  #
60
- # # Plain string logging (backward compatibility)
61
- # logger.info("User logged in successfully")
22
+ # # A message, with a Hash or an exception beside it
23
+ # logger.info("User logged in", user_id: 123)
24
+ # logger.error("Import failed", error)
62
25
  # ```
63
- #
64
- # The logger is a drop-in replacement for Rails.logger and maintains full
65
- # API compatibility while providing significantly enhanced performance.
66
26
  class Logger < ::SemanticLogger::Logger
67
27
  extend T::Sig
68
28
 
@@ -90,6 +50,14 @@ module LogStruct
90
50
  # ::Logger, ActiveSupport::Logger, test doubles, etc.
91
51
  sig { params(logger: T.untyped).returns(T.untyped) }
92
52
  def broadcast_to(logger)
53
+ # Every SemanticLogger logger writes to the same appenders, so broadcasting to one
54
+ # would write each message twice. Sidekiq's Rails integration broadcasts Rails.logger
55
+ # to Sidekiq's logger, for example.
56
+ return logger if logger.is_a?(::SemanticLogger::Logger)
57
+ # `rails server` in development broadcasts to a STDOUT logger, which would print each
58
+ # message again as text beside LogStruct's line
59
+ return logger if console_logger_with_console_appender?(logger)
60
+
93
61
  @broadcasts << logger
94
62
  logger
95
63
  end
@@ -104,11 +72,11 @@ module LogStruct
104
72
  # Support for tagged logging
105
73
  sig { params(tags: T.untyped, block: T.proc.returns(T.untyped)).returns(T.untyped) }
106
74
  def tagged(*tags, &block)
107
- # Convert tags to array and pass individually to avoid splat issues
108
75
  tag_array = tags.flatten
109
76
  if tag_array.empty?
110
77
  super(&block)
111
78
  else
79
+ # Sorbet can't check a splat of an Array whose size it doesn't know
112
80
  super(*T.unsafe(tag_array), &block)
113
81
  end
114
82
  end
@@ -138,13 +106,21 @@ module LogStruct
138
106
  ::SemanticLogger.pop_tags(count)
139
107
  end
140
108
 
141
- # Support for << operator (used by RailsLogSplitter)
109
+ # Support for << operator (used by RailsLogSplitter). info passes the message on to the broadcast loggers.
142
110
  sig { params(msg: String).returns(T.self_type) }
143
111
  def <<(msg)
144
112
  info(msg)
145
- @broadcasts.each { |logger| logger << msg if logger.respond_to?(:<<) }
146
113
  self
147
114
  end
115
+
116
+ private
117
+
118
+ sig { params(logger: T.untyped).returns(T::Boolean) }
119
+ def console_logger_with_console_appender?(logger)
120
+ # rails server's logger writes to the STDOUT constant, which differs from $stdout when $stdout was replaced
121
+ ::SemanticLogger.appenders.console_output? &&
122
+ ::ActiveSupport::Logger.logger_outputs_to?(logger, $stdout, $stderr, STDOUT, STDERR) # rubocop:disable Style/GlobalStdStream
123
+ end
148
124
  end
149
125
 
150
126
  # Proxy object to provide ActiveJob-compatible formatter interface
@@ -0,0 +1,40 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LogStruct
5
+ module SemanticLogger
6
+ # SemanticLogger writes lines from a background thread, and a forked child doesn't get its parent's threads, so
7
+ # lines logged in Puma, Unicorn, Passenger or Pitchfork workers, Resque jobs and other forked children were never
8
+ # written. Ruby calls Process._fork for every fork, so each child reopens the appenders (a new writer thread).
9
+ module ReopenAfterFork
10
+ extend T::Sig
11
+
12
+ @installed = T.let(false, T::Boolean)
13
+
14
+ sig { void }
15
+ def self.install!
16
+ return if @installed
17
+ @installed = true
18
+
19
+ Process.singleton_class.prepend(ForkHook)
20
+ end
21
+
22
+ module ForkHook
23
+ extend T::Sig
24
+
25
+ sig { returns(Integer) }
26
+ def _fork
27
+ pid = T.let(super, Integer)
28
+ if pid.zero?
29
+ begin
30
+ ::SemanticLogger.reopen
31
+ rescue => e
32
+ LogStruct.handle_exception(e, source: LogStruct::Source::Internal, context: {integration: :semantic_logger})
33
+ end
34
+ end
35
+ pid
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end