logstruct 0.1.11 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +146 -1
- data/README.md +76 -68
- data/lib/log_struct/concerns/configuration.rb +139 -91
- data/lib/log_struct/concerns/error_handling.rb +36 -6
- data/lib/log_struct/config_struct/error_handling_modes.rb +3 -1
- data/lib/log_struct/config_struct/filters.rb +126 -22
- data/lib/log_struct/config_struct/integrations.rb +0 -4
- data/lib/log_struct/config_struct/limits.rb +29 -0
- data/lib/log_struct/configuration.rb +2 -0
- data/lib/log_struct/enums/event.rb +3 -1
- data/lib/log_struct/enums/log_field.rb +5 -0
- data/lib/log_struct/formatter.rb +304 -56
- data/lib/log_struct/hash_utils.rb +33 -4
- data/lib/log_struct/integrations/action_mailer/error_handling.rb +78 -20
- data/lib/log_struct/integrations/action_mailer/fallback_error_handlers.rb +81 -0
- data/lib/log_struct/integrations/action_mailer/metadata_collection.rb +0 -20
- data/lib/log_struct/integrations/action_mailer.rb +22 -17
- data/lib/log_struct/integrations/active_job/log_subscriber.rb +177 -25
- data/lib/log_struct/integrations/active_job.rb +2 -7
- data/lib/log_struct/integrations/active_model_serializers.rb +9 -3
- data/lib/log_struct/integrations/active_record.rb +45 -17
- data/lib/log_struct/integrations/active_storage.rb +31 -31
- data/lib/log_struct/integrations/ahoy.rb +19 -15
- data/lib/log_struct/integrations/carrierwave.rb +55 -51
- data/lib/log_struct/integrations/dotenv.rb +22 -32
- data/lib/log_struct/integrations/good_job/logger.rb +30 -24
- data/lib/log_struct/integrations/good_job.rb +111 -40
- data/lib/log_struct/integrations/host_authorization.rb +47 -36
- data/lib/log_struct/integrations/lograge.rb +39 -4
- data/lib/log_struct/integrations/puma.rb +74 -34
- data/lib/log_struct/integrations/rack_error_handler/debug_exceptions_logging.rb +35 -0
- data/lib/log_struct/integrations/rack_error_handler/error_lines.rb +160 -0
- data/lib/log_struct/integrations/rack_error_handler/middleware.rb +14 -169
- data/lib/log_struct/integrations/rack_error_handler/parse_error_logging.rb +36 -0
- data/lib/log_struct/integrations/rack_error_handler.rb +20 -4
- data/lib/log_struct/integrations/rack_middleware.rb +23 -0
- data/lib/log_struct/integrations/rails_log_subscribers.rb +32 -0
- data/lib/log_struct/integrations/request_context/middleware.rb +4 -9
- data/lib/log_struct/integrations/shrine.rb +48 -73
- data/lib/log_struct/integrations/sidekiq/job_context_middleware.rb +32 -0
- data/lib/log_struct/integrations/sidekiq/logger.rb +17 -9
- data/lib/log_struct/integrations/sidekiq.rb +98 -7
- data/lib/log_struct/integrations/sorbet.rb +10 -46
- data/lib/log_struct/integrations.rb +3 -2
- data/lib/log_struct/log/{good_job/enqueue.rb → active_job/discard.rb} +13 -13
- data/lib/log_struct/log/active_job/enqueue.rb +0 -4
- data/lib/log_struct/log/{good_job/schedule.rb → active_job/error.rb} +15 -15
- data/lib/log_struct/log/{good_job/start.rb → active_job/retry.rb} +13 -13
- data/lib/log_struct/log/{good_job/finish.rb → active_job/retry_stopped.rb} +13 -17
- data/lib/log_struct/log/active_job.rb +4 -0
- data/lib/log_struct/log/active_storage/delete.rb +6 -2
- data/lib/log_struct/log/active_storage/download.rb +2 -4
- data/lib/log_struct/log/active_storage/exist.rb +2 -2
- data/lib/log_struct/log/active_storage/metadata.rb +4 -4
- data/lib/log_struct/log/active_storage/upload.rb +2 -10
- data/lib/log_struct/log/active_storage/url.rb +2 -6
- data/lib/log_struct/log/active_storage.rb +2 -3
- data/lib/log_struct/log/error.rb +24 -1
- data/lib/log_struct/log/good_job.rb +0 -4
- data/lib/log_struct/log/puma/error.rb +63 -0
- data/lib/log_struct/log/puma.rb +1 -0
- data/lib/log_struct/log/security/blocked_host.rb +0 -4
- data/lib/log_struct/log/shrine/exist.rb +0 -2
- data/lib/log_struct/log/shrine/metadata.rb +2 -4
- data/lib/log_struct/log/shrine/upload.rb +4 -0
- data/lib/log_struct/log/sidekiq/error.rb +63 -0
- data/lib/log_struct/log/{active_storage/stream.rb → sidekiq/log.rb} +17 -11
- data/lib/log_struct/log/sidekiq.rb +3 -45
- data/lib/log_struct/log.rb +4 -2
- data/lib/log_struct/logged_errors.rb +25 -0
- data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +2 -3
- data/lib/log_struct/param_filters.rb +51 -38
- data/lib/log_struct/params_errors.rb +78 -0
- data/lib/log_struct/rails_boot_banner_silencer.rb +7 -2
- data/lib/log_struct/railtie.rb +29 -14
- data/lib/log_struct/semantic_logger/color_formatter.rb +60 -53
- data/lib/log_struct/semantic_logger/concerns/log_methods.rb +65 -57
- data/lib/log_struct/semantic_logger/formatter.rb +79 -57
- data/lib/log_struct/semantic_logger/logger.rb +29 -53
- data/lib/log_struct/semantic_logger/reopen_after_fork.rb +40 -0
- data/lib/log_struct/semantic_logger/setup.rb +115 -112
- data/lib/log_struct/shared/merge_additional_data_fields.rb +14 -1
- data/lib/log_struct/shared/serialize_common.rb +2 -7
- data/lib/log_struct/string_scrubber.rb +229 -20
- data/lib/log_struct/version.rb +1 -1
- data/lib/log_struct.rb +6 -4
- data/logstruct.gemspec +19 -10
- metadata +63 -11
- data/lib/log_struct/builders/active_job.rb +0 -84
- data/lib/log_struct/integrations/good_job/log_subscriber.rb +0 -179
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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.
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
#
|
|
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
|
-
# #
|
|
46
|
-
#
|
|
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
|
|
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
|
-
# #
|
|
61
|
-
# logger.info("User logged in
|
|
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
|