logstruct 0.0.1 → 0.0.2.pre.rc1
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 +26 -2
- data/LICENSE +21 -0
- data/README.md +67 -0
- data/lib/log_struct/concerns/configuration.rb +93 -0
- data/lib/log_struct/concerns/error_handling.rb +94 -0
- data/lib/log_struct/concerns/logging.rb +45 -0
- data/lib/log_struct/config_struct/error_handling_modes.rb +25 -0
- data/lib/log_struct/config_struct/filters.rb +80 -0
- data/lib/log_struct/config_struct/integrations.rb +89 -0
- data/lib/log_struct/configuration.rb +59 -0
- data/lib/log_struct/enums/error_handling_mode.rb +22 -0
- data/lib/log_struct/enums/error_reporter.rb +14 -0
- data/lib/log_struct/enums/event.rb +48 -0
- data/lib/log_struct/enums/level.rb +66 -0
- data/lib/log_struct/enums/source.rb +26 -0
- data/lib/log_struct/enums.rb +9 -0
- data/lib/log_struct/formatter.rb +224 -0
- data/lib/log_struct/handlers.rb +27 -0
- data/lib/log_struct/hash_utils.rb +21 -0
- data/lib/log_struct/integrations/action_mailer/callbacks.rb +100 -0
- data/lib/log_struct/integrations/action_mailer/error_handling.rb +173 -0
- data/lib/log_struct/integrations/action_mailer/event_logging.rb +90 -0
- data/lib/log_struct/integrations/action_mailer/metadata_collection.rb +78 -0
- data/lib/log_struct/integrations/action_mailer.rb +50 -0
- data/lib/log_struct/integrations/active_job/log_subscriber.rb +104 -0
- data/lib/log_struct/integrations/active_job.rb +38 -0
- data/lib/log_struct/integrations/active_record.rb +258 -0
- data/lib/log_struct/integrations/active_storage.rb +94 -0
- data/lib/log_struct/integrations/carrierwave.rb +111 -0
- data/lib/log_struct/integrations/good_job/log_subscriber.rb +228 -0
- data/lib/log_struct/integrations/good_job/logger.rb +73 -0
- data/lib/log_struct/integrations/good_job.rb +111 -0
- data/lib/log_struct/integrations/host_authorization.rb +81 -0
- data/lib/log_struct/integrations/integration_interface.rb +21 -0
- data/lib/log_struct/integrations/lograge.rb +114 -0
- data/lib/log_struct/integrations/rack.rb +31 -0
- data/lib/log_struct/integrations/rack_error_handler/middleware.rb +146 -0
- data/lib/log_struct/integrations/rack_error_handler.rb +32 -0
- data/lib/log_struct/integrations/shrine.rb +75 -0
- data/lib/log_struct/integrations/sidekiq/logger.rb +43 -0
- data/lib/log_struct/integrations/sidekiq.rb +39 -0
- data/lib/log_struct/integrations/sorbet.rb +49 -0
- data/lib/log_struct/integrations.rb +41 -0
- data/lib/log_struct/log/action_mailer.rb +55 -0
- data/lib/log_struct/log/active_job.rb +64 -0
- data/lib/log_struct/log/active_storage.rb +78 -0
- data/lib/log_struct/log/carrierwave.rb +82 -0
- data/lib/log_struct/log/error.rb +76 -0
- data/lib/log_struct/log/good_job.rb +151 -0
- data/lib/log_struct/log/interfaces/additional_data_field.rb +20 -0
- data/lib/log_struct/log/interfaces/common_fields.rb +42 -0
- data/lib/log_struct/log/interfaces/message_field.rb +20 -0
- data/lib/log_struct/log/interfaces/request_fields.rb +36 -0
- data/lib/log_struct/log/plain.rb +53 -0
- data/lib/log_struct/log/request.rb +76 -0
- data/lib/log_struct/log/security.rb +80 -0
- data/lib/log_struct/log/shared/add_request_fields.rb +29 -0
- data/lib/log_struct/log/shared/merge_additional_data_fields.rb +28 -0
- data/lib/log_struct/log/shared/serialize_common.rb +36 -0
- data/lib/log_struct/log/shrine.rb +70 -0
- data/lib/log_struct/log/sidekiq.rb +50 -0
- data/lib/log_struct/log/sql.rb +126 -0
- data/lib/log_struct/log.rb +43 -0
- data/lib/log_struct/log_keys.rb +102 -0
- data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +36 -0
- data/lib/log_struct/multi_error_reporter.rb +149 -0
- data/lib/log_struct/param_filters.rb +89 -0
- data/lib/log_struct/railtie.rb +31 -0
- data/lib/log_struct/semantic_logger/color_formatter.rb +209 -0
- data/lib/log_struct/semantic_logger/formatter.rb +94 -0
- data/lib/log_struct/semantic_logger/logger.rb +129 -0
- data/lib/log_struct/semantic_logger/setup.rb +219 -0
- data/lib/log_struct/sorbet/serialize_symbol_keys.rb +23 -0
- data/lib/log_struct/sorbet.rb +13 -0
- data/lib/log_struct/string_scrubber.rb +84 -0
- data/lib/log_struct/version.rb +6 -0
- data/lib/log_struct.rb +37 -0
- data/lib/logstruct.rb +2 -6
- data/logstruct.gemspec +52 -0
- metadata +221 -5
- data/Rakefile +0 -5
@@ -0,0 +1,149 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "enums/error_reporter"
|
5
|
+
|
6
|
+
# Try to require all supported error reporting libraries
|
7
|
+
# Users may have multiple installed, so we should load all of them
|
8
|
+
%w[sentry-ruby bugsnag rollbar honeybadger].each do |gem_name|
|
9
|
+
require gem_name
|
10
|
+
rescue LoadError
|
11
|
+
# If a particular gem is not available, we'll still load the others
|
12
|
+
end
|
13
|
+
|
14
|
+
module LogStruct
|
15
|
+
# MultiErrorReporter provides a unified interface for reporting errors to various services.
|
16
|
+
# You can also override this with your own error reporter by setting
|
17
|
+
# LogStruct#.config.error_reporting_handler
|
18
|
+
# NOTE: This is used for cases where an error should be reported
|
19
|
+
# but the operation should be allowed to continue (e.g. scrubbing log data.)
|
20
|
+
class MultiErrorReporter
|
21
|
+
# Class variable to store the selected reporter
|
22
|
+
@reporter = T.let(nil, T.nilable(ErrorReporter))
|
23
|
+
|
24
|
+
class << self
|
25
|
+
extend T::Sig
|
26
|
+
|
27
|
+
sig { returns(ErrorReporter) }
|
28
|
+
def reporter
|
29
|
+
@reporter ||= detect_reporter
|
30
|
+
end
|
31
|
+
|
32
|
+
# Set the reporter to use (user-friendly API that accepts symbols)
|
33
|
+
sig { params(reporter_type: T.any(ErrorReporter, Symbol)).returns(ErrorReporter) }
|
34
|
+
def reporter=(reporter_type)
|
35
|
+
@reporter = case reporter_type
|
36
|
+
when ErrorReporter
|
37
|
+
reporter_type
|
38
|
+
when Symbol
|
39
|
+
case reporter_type
|
40
|
+
when :sentry then ErrorReporter::Sentry
|
41
|
+
when :bugsnag then ErrorReporter::Bugsnag
|
42
|
+
when :rollbar then ErrorReporter::Rollbar
|
43
|
+
when :honeybadger then ErrorReporter::Honeybadger
|
44
|
+
when :rails_logger then ErrorReporter::RailsLogger
|
45
|
+
else
|
46
|
+
valid_types = ErrorReporter.values.map { |v| ":#{v.serialize}" }.join(", ")
|
47
|
+
raise ArgumentError, "Unknown reporter type: #{reporter_type}. Valid types are: #{valid_types}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Auto-detect which error reporting service to use
|
53
|
+
sig { returns(ErrorReporter) }
|
54
|
+
def detect_reporter
|
55
|
+
if defined?(::Sentry)
|
56
|
+
ErrorReporter::Sentry
|
57
|
+
elsif defined?(::Bugsnag)
|
58
|
+
ErrorReporter::Bugsnag
|
59
|
+
elsif defined?(::Rollbar)
|
60
|
+
ErrorReporter::Rollbar
|
61
|
+
elsif defined?(::Honeybadger)
|
62
|
+
ErrorReporter::Honeybadger
|
63
|
+
else
|
64
|
+
ErrorReporter::RailsLogger
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Report an error to the configured error reporting service
|
69
|
+
sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
|
70
|
+
def report_error(error, context = {})
|
71
|
+
# Call the appropriate reporter method based on what's available
|
72
|
+
case reporter
|
73
|
+
when ErrorReporter::Sentry
|
74
|
+
report_to_sentry(error, context)
|
75
|
+
when ErrorReporter::Bugsnag
|
76
|
+
report_to_bugsnag(error, context)
|
77
|
+
when ErrorReporter::Rollbar
|
78
|
+
report_to_rollbar(error, context)
|
79
|
+
when ErrorReporter::Honeybadger
|
80
|
+
report_to_honeybadger(error, context)
|
81
|
+
else
|
82
|
+
fallback_logging(error, context)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# Report to Sentry
|
89
|
+
sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
|
90
|
+
def report_to_sentry(error, context = {})
|
91
|
+
return unless defined?(::Sentry)
|
92
|
+
|
93
|
+
# Use the proper Sentry interface defined in the RBI
|
94
|
+
::Sentry.capture_exception(error, extra: context)
|
95
|
+
rescue => e
|
96
|
+
fallback_logging(e, {original_error: error.class.to_s})
|
97
|
+
end
|
98
|
+
|
99
|
+
# Report to Bugsnag
|
100
|
+
sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
|
101
|
+
def report_to_bugsnag(error, context = {})
|
102
|
+
return unless defined?(::Bugsnag)
|
103
|
+
|
104
|
+
::Bugsnag.notify(error) do |report|
|
105
|
+
report.add_metadata(:context, context)
|
106
|
+
end
|
107
|
+
rescue => e
|
108
|
+
fallback_logging(e, {original_error: error.class.to_s})
|
109
|
+
end
|
110
|
+
|
111
|
+
# Report to Rollbar
|
112
|
+
sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
|
113
|
+
def report_to_rollbar(error, context = {})
|
114
|
+
return unless defined?(::Rollbar)
|
115
|
+
|
116
|
+
::Rollbar.error(error, context)
|
117
|
+
rescue => e
|
118
|
+
fallback_logging(e, {original_error: error.class.to_s})
|
119
|
+
end
|
120
|
+
|
121
|
+
# Report to Honeybadger
|
122
|
+
sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
|
123
|
+
def report_to_honeybadger(error, context = {})
|
124
|
+
return unless defined?(::Honeybadger)
|
125
|
+
|
126
|
+
::Honeybadger.notify(error, context: context)
|
127
|
+
rescue => e
|
128
|
+
fallback_logging(e, {original_error: error.class.to_s})
|
129
|
+
end
|
130
|
+
|
131
|
+
# Fallback logging when no error reporting services are available
|
132
|
+
# Uses the LogStruct.error method to properly log the error
|
133
|
+
sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
|
134
|
+
def fallback_logging(error, context = {})
|
135
|
+
return if error.nil?
|
136
|
+
|
137
|
+
# Create a proper error log entry
|
138
|
+
error_log = Log::Error.from_exception(
|
139
|
+
Source::LogStruct,
|
140
|
+
error,
|
141
|
+
context
|
142
|
+
)
|
143
|
+
|
144
|
+
# Use LogStruct.error to properly log the error
|
145
|
+
LogStruct.error(error_log)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "digest"
|
5
|
+
require_relative "hash_utils"
|
6
|
+
|
7
|
+
module LogStruct
|
8
|
+
# This class contains methods for filtering sensitive data in logs
|
9
|
+
# It is used by Formatter to determine which keys should be filtered
|
10
|
+
class ParamFilters
|
11
|
+
class << self
|
12
|
+
extend T::Sig
|
13
|
+
|
14
|
+
# Check if a key should be filtered based on our defined sensitive keys
|
15
|
+
sig { params(key: T.any(String, Symbol)).returns(T::Boolean) }
|
16
|
+
def should_filter_key?(key)
|
17
|
+
LogStruct.config.filters.filter_keys.include?(key.to_s.downcase.to_sym)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Check if a key should be hashed rather than completely filtered
|
21
|
+
sig { params(key: T.any(String, Symbol)).returns(T::Boolean) }
|
22
|
+
def should_include_string_hash?(key)
|
23
|
+
LogStruct.config.filters.filter_keys_with_hashes.include?(key.to_s.downcase.to_sym)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Convert a value to a filtered summary hash (e.g. { _filtered: { class: "String", ... }})
|
27
|
+
sig { params(key: T.any(String, Symbol), data: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
28
|
+
def summarize_json_attribute(key, data)
|
29
|
+
case data
|
30
|
+
when Hash
|
31
|
+
summarize_hash(data)
|
32
|
+
when Array
|
33
|
+
summarize_array(data)
|
34
|
+
when String
|
35
|
+
summarize_string(data, should_include_string_hash?(key))
|
36
|
+
else
|
37
|
+
{_class: data.class}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Summarize a String for logging, including details and an SHA256 hash (if configured)
|
42
|
+
sig { params(string: String, include_hash: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
|
43
|
+
def summarize_string(string, include_hash)
|
44
|
+
filtered_string = {
|
45
|
+
_class: String
|
46
|
+
}
|
47
|
+
if include_hash
|
48
|
+
filtered_string[:_hash] = HashUtils.hash_value(string)
|
49
|
+
else
|
50
|
+
filtered_string[:_bytes] = string.bytesize
|
51
|
+
end
|
52
|
+
|
53
|
+
filtered_string
|
54
|
+
end
|
55
|
+
|
56
|
+
# Summarize a Hash for logging, including details about the size and keys
|
57
|
+
sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
58
|
+
def summarize_hash(hash)
|
59
|
+
return {_class: "Hash", _empty: true} if hash.empty?
|
60
|
+
|
61
|
+
# Don't include byte size if hash contains any filtered keys
|
62
|
+
has_sensitive_keys = hash.keys.any? { |key| should_filter_key?(key) }
|
63
|
+
|
64
|
+
summary = {
|
65
|
+
_class: Hash,
|
66
|
+
_keys_count: hash.keys.size,
|
67
|
+
_keys: hash.keys.map(&:to_sym).take(10)
|
68
|
+
}
|
69
|
+
|
70
|
+
# Only add byte size if no sensitive keys are present
|
71
|
+
summary[:_bytes] = hash.to_json.bytesize unless has_sensitive_keys
|
72
|
+
|
73
|
+
summary
|
74
|
+
end
|
75
|
+
|
76
|
+
# Summarize an Array for logging, including details about the size and items
|
77
|
+
sig { params(array: T::Array[T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
78
|
+
def summarize_array(array)
|
79
|
+
return {_class: "Array", _empty: true} if array.empty?
|
80
|
+
|
81
|
+
{
|
82
|
+
_class: Array,
|
83
|
+
_count: array.size,
|
84
|
+
_bytes: array.to_json.bytesize
|
85
|
+
}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "rails"
|
5
|
+
require "semantic_logger"
|
6
|
+
require_relative "formatter"
|
7
|
+
require_relative "semantic_logger/setup"
|
8
|
+
|
9
|
+
module LogStruct
|
10
|
+
# Railtie to integrate with Rails
|
11
|
+
class Railtie < ::Rails::Railtie
|
12
|
+
# Configure early, right after logger initialization
|
13
|
+
initializer "logstruct.configure_logger", after: :initialize_logger do |app|
|
14
|
+
next unless LogStruct.enabled?
|
15
|
+
|
16
|
+
# Use SemanticLogger for powerful logging features
|
17
|
+
LogStruct::SemanticLogger::Setup.configure_semantic_logger(app)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Setup all integrations after logger setup is complete
|
21
|
+
initializer "logstruct.setup", before: :build_middleware_stack do |app|
|
22
|
+
next unless LogStruct.enabled?
|
23
|
+
|
24
|
+
# Merge Rails filter parameters into our filters
|
25
|
+
LogStruct.merge_rails_filter_parameters!
|
26
|
+
|
27
|
+
# Set up all integrations
|
28
|
+
Integrations.setup_integrations
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "semantic_logger"
|
5
|
+
require_relative "formatter"
|
6
|
+
|
7
|
+
module LogStruct
|
8
|
+
module SemanticLogger
|
9
|
+
# Development-Optimized Colorized JSON Formatter
|
10
|
+
#
|
11
|
+
# This formatter extends SemanticLogger's Color formatter to provide beautiful,
|
12
|
+
# readable JSON output in development environments. It significantly improves
|
13
|
+
# the developer experience when working with structured logs.
|
14
|
+
#
|
15
|
+
# ## Benefits of Colorized Output:
|
16
|
+
#
|
17
|
+
# ### Readability
|
18
|
+
# - **Syntax highlighting**: JSON keys, values, and data types are color-coded
|
19
|
+
# - **Visual hierarchy**: Different colors help identify structure at a glance
|
20
|
+
# - **Error spotting**: Quickly identify malformed data or unexpected values
|
21
|
+
# - **Context separation**: Log entries are visually distinct from each other
|
22
|
+
#
|
23
|
+
# ### Performance in Development
|
24
|
+
# - **Faster debugging**: Quickly scan logs without reading every character
|
25
|
+
# - **Pattern recognition**: Colors help identify common log patterns
|
26
|
+
# - **Reduced cognitive load**: Less mental effort required to parse log output
|
27
|
+
# - **Improved workflow**: Spend less time reading logs, more time coding
|
28
|
+
#
|
29
|
+
# ### Customization
|
30
|
+
# - **Configurable colors**: Customize colors for keys, strings, numbers, etc.
|
31
|
+
# - **Environment-aware**: Automatically disabled in production/CI environments
|
32
|
+
# - **Fallback support**: Gracefully falls back to standard formatting if needed
|
33
|
+
#
|
34
|
+
# ## Color Mapping:
|
35
|
+
# - **Keys**: Yellow - Easy to spot field names
|
36
|
+
# - **Strings**: Green - Clear indication of text values
|
37
|
+
# - **Numbers**: Blue - Numeric values stand out
|
38
|
+
# - **Booleans**: Magenta - true/false values are distinctive
|
39
|
+
# - **Null**: Red - Missing values are immediately visible
|
40
|
+
# - **Logger names**: Cyan - Source identification
|
41
|
+
#
|
42
|
+
# ## Integration with SemanticLogger:
|
43
|
+
# This formatter preserves all SemanticLogger benefits (performance, threading,
|
44
|
+
# reliability) while adding visual enhancements. It processes LogStruct types,
|
45
|
+
# hashes, and plain messages with appropriate colorization.
|
46
|
+
#
|
47
|
+
# The formatter is automatically enabled in development when `enable_color_output`
|
48
|
+
# is true (default), providing zero-configuration enhanced logging experience.
|
49
|
+
class ColorFormatter < ::SemanticLogger::Formatters::Color
|
50
|
+
extend T::Sig
|
51
|
+
|
52
|
+
sig { params(color_map: T.nilable(T::Hash[Symbol, Symbol]), args: T.untyped).void }
|
53
|
+
def initialize(color_map: nil, **args)
|
54
|
+
super(**args)
|
55
|
+
@logstruct_formatter = T.let(LogStruct::Formatter.new, LogStruct::Formatter)
|
56
|
+
|
57
|
+
# Set up custom color mapping
|
58
|
+
@custom_colors = T.let(color_map || default_color_map, T::Hash[Symbol, Symbol])
|
59
|
+
end
|
60
|
+
|
61
|
+
sig { override.params(log: ::SemanticLogger::Log, logger: T.untyped).returns(String) }
|
62
|
+
def call(log, logger)
|
63
|
+
# Handle LogStruct types specially with colorization
|
64
|
+
if log.payload.is_a?(LogStruct::Log::Interfaces::CommonFields)
|
65
|
+
# Get the LogStruct formatted JSON
|
66
|
+
logstruct_json = @logstruct_formatter.call(log.level, log.time, log.name, log.payload)
|
67
|
+
|
68
|
+
# Parse and colorize it
|
69
|
+
begin
|
70
|
+
parsed_data = T.let(JSON.parse(logstruct_json), T::Hash[String, T.untyped])
|
71
|
+
colorized_json = colorize_json(parsed_data)
|
72
|
+
|
73
|
+
# Use SemanticLogger's prefix formatting but with our colorized content
|
74
|
+
prefix = format("%<time>s %<level>s [%<process>s] %<name>s -- ",
|
75
|
+
time: format_time(log.time),
|
76
|
+
level: format_level(log.level),
|
77
|
+
process: log.process_info,
|
78
|
+
name: format_name(log.name))
|
79
|
+
|
80
|
+
"#{prefix}#{colorized_json}\n"
|
81
|
+
rescue JSON::ParserError
|
82
|
+
# Fallback to standard formatting
|
83
|
+
super
|
84
|
+
end
|
85
|
+
elsif log.payload.is_a?(Hash) || log.payload.is_a?(T::Struct)
|
86
|
+
# Process hashes through our formatter then colorize
|
87
|
+
begin
|
88
|
+
logstruct_json = @logstruct_formatter.call(log.level, log.time, log.name, log.payload)
|
89
|
+
parsed_data = T.let(JSON.parse(logstruct_json), T::Hash[String, T.untyped])
|
90
|
+
colorized_json = colorize_json(parsed_data)
|
91
|
+
|
92
|
+
prefix = format("%<time>s %<level>s [%<process>s] %<name>s -- ",
|
93
|
+
time: format_time(log.time),
|
94
|
+
level: format_level(log.level),
|
95
|
+
process: log.process_info,
|
96
|
+
name: format_name(log.name))
|
97
|
+
|
98
|
+
"#{prefix}#{colorized_json}\n"
|
99
|
+
rescue JSON::ParserError
|
100
|
+
# Fallback to standard formatting
|
101
|
+
super
|
102
|
+
end
|
103
|
+
else
|
104
|
+
# For plain messages, use SemanticLogger's default colorization
|
105
|
+
super
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
sig { returns(LogStruct::Formatter) }
|
112
|
+
attr_reader :logstruct_formatter
|
113
|
+
|
114
|
+
# Default color mapping for LogStruct JSON
|
115
|
+
sig { returns(T::Hash[Symbol, Symbol]) }
|
116
|
+
def default_color_map
|
117
|
+
{
|
118
|
+
key: :yellow,
|
119
|
+
string: :green,
|
120
|
+
number: :blue,
|
121
|
+
bool: :magenta,
|
122
|
+
nil: :red,
|
123
|
+
name: :cyan
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
# Simple JSON colorizer that adds ANSI codes
|
128
|
+
sig { params(data: T::Hash[String, T.untyped]).returns(String) }
|
129
|
+
def colorize_json(data)
|
130
|
+
# For now, just return a simple colorized version of the JSON
|
131
|
+
# This is much simpler than the full recursive approach
|
132
|
+
json_str = JSON.pretty_generate(data)
|
133
|
+
|
134
|
+
# Apply basic colorization with regex
|
135
|
+
json_str.gsub(/"([^"]+)":/, colorize_text('\1', :key) + ":")
|
136
|
+
.gsub(/: "([^"]*)"/, ": " + colorize_text('\1', :string))
|
137
|
+
.gsub(/: (\d+\.?\d*)/, ": " + colorize_text('\1', :number))
|
138
|
+
.gsub(/: (true|false)/, ": " + colorize_text('\1', :bool))
|
139
|
+
.gsub(": null", ": " + colorize_text("null", :nil))
|
140
|
+
end
|
141
|
+
|
142
|
+
# Add ANSI color codes to text
|
143
|
+
sig { params(text: String, color_type: Symbol).returns(String) }
|
144
|
+
def colorize_text(text, color_type)
|
145
|
+
color = @custom_colors[color_type] || :white
|
146
|
+
"\e[#{color_code_for(color)}m#{text}\e[0m"
|
147
|
+
end
|
148
|
+
|
149
|
+
# Format timestamp
|
150
|
+
sig { params(time: Time).returns(String) }
|
151
|
+
def format_time(time)
|
152
|
+
time.strftime("%Y-%m-%d %H:%M:%S.%6N")
|
153
|
+
end
|
154
|
+
|
155
|
+
# Format log level with color
|
156
|
+
sig { params(level: T.any(String, Symbol)).returns(String) }
|
157
|
+
def format_level(level)
|
158
|
+
level_str = level.to_s.upcase[0]
|
159
|
+
color = level_color_for(level.to_sym)
|
160
|
+
"\e[#{color_code_for(color)}m#{level_str}\e[0m"
|
161
|
+
end
|
162
|
+
|
163
|
+
# Format logger name with color
|
164
|
+
sig { params(name: T.nilable(String)).returns(String) }
|
165
|
+
def format_name(name)
|
166
|
+
return "" unless name
|
167
|
+
color = @custom_colors[:name] || :cyan
|
168
|
+
"\e[#{color_code_for(color)}m#{name}\e[0m"
|
169
|
+
end
|
170
|
+
|
171
|
+
# Get color for log level
|
172
|
+
sig { params(level: Symbol).returns(Symbol) }
|
173
|
+
def level_color_for(level)
|
174
|
+
case level
|
175
|
+
when :debug then :magenta
|
176
|
+
when :info then :cyan
|
177
|
+
when :warn then :yellow
|
178
|
+
when :error then :red
|
179
|
+
when :fatal then :red
|
180
|
+
else :cyan
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Get ANSI color code for color symbol
|
185
|
+
sig { params(color: Symbol).returns(String) }
|
186
|
+
def color_code_for(color)
|
187
|
+
case color
|
188
|
+
when :black then "30"
|
189
|
+
when :red then "31"
|
190
|
+
when :green then "32"
|
191
|
+
when :yellow then "33"
|
192
|
+
when :blue then "34"
|
193
|
+
when :magenta then "35"
|
194
|
+
when :cyan then "36"
|
195
|
+
when :white then "37"
|
196
|
+
when :bright_black then "90"
|
197
|
+
when :bright_red then "91"
|
198
|
+
when :bright_green then "92"
|
199
|
+
when :bright_yellow then "93"
|
200
|
+
when :bright_blue then "94"
|
201
|
+
when :bright_magenta then "95"
|
202
|
+
when :bright_cyan then "96"
|
203
|
+
when :bright_white then "97"
|
204
|
+
else "37" # default to white
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "semantic_logger"
|
5
|
+
require_relative "../formatter"
|
6
|
+
|
7
|
+
module LogStruct
|
8
|
+
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.
|
53
|
+
class Formatter < ::SemanticLogger::Formatters::Json
|
54
|
+
extend T::Sig
|
55
|
+
|
56
|
+
sig { void }
|
57
|
+
def initialize
|
58
|
+
super
|
59
|
+
@logstruct_formatter = T.let(LogStruct::Formatter.new, LogStruct::Formatter)
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { params(log: ::SemanticLogger::Log, logger: T.untyped).returns(String) }
|
63
|
+
def call(log, logger)
|
64
|
+
# Handle LogStruct types specially - they get wrapped in payload hash by SemanticLogger
|
65
|
+
if log.payload.is_a?(Hash) && log.payload[:payload].is_a?(LogStruct::Log::Interfaces::CommonFields)
|
66
|
+
# Use our formatter to process LogStruct types
|
67
|
+
@logstruct_formatter.call(log.level, log.time, log.name, log.payload[:payload])
|
68
|
+
elsif log.payload.is_a?(LogStruct::Log::Interfaces::CommonFields)
|
69
|
+
# Direct LogStruct (fallback case)
|
70
|
+
@logstruct_formatter.call(log.level, log.time, log.name, log.payload)
|
71
|
+
elsif log.payload.is_a?(Hash) && log.payload[:payload].is_a?(T::Struct)
|
72
|
+
# T::Struct wrapped in payload hash
|
73
|
+
@logstruct_formatter.call(log.level, log.time, log.name, log.payload[:payload])
|
74
|
+
elsif log.payload.is_a?(Hash) || log.payload.is_a?(T::Struct)
|
75
|
+
# Process hashes and T::Structs through our formatter
|
76
|
+
@logstruct_formatter.call(log.level, log.time, log.name, log.payload)
|
77
|
+
else
|
78
|
+
# For plain messages, create a Plain log entry
|
79
|
+
message_data = log.payload || log.message
|
80
|
+
plain_log = LogStruct::Log::Plain.new(
|
81
|
+
message: message_data,
|
82
|
+
timestamp: log.time
|
83
|
+
)
|
84
|
+
@logstruct_formatter.call(log.level, log.time, log.name, plain_log)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
sig { returns(LogStruct::Formatter) }
|
91
|
+
attr_reader :logstruct_formatter
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|