All Files ( 92.84% covered at 196.42 hits/line )
163 files in total.
5883 relevant lines,
5462 lines covered and
421 lines missed.
(
92.84%
)
-
# typed: strict
-
# frozen_string_literal: true
-
-
# Core library files
-
2
require "log_struct/sorbet"
-
2
require "log_struct/version"
-
2
require "log_struct/enums"
-
2
require "log_struct/configuration"
-
2
require "log_struct/formatter"
-
2
require "log_struct/logged_errors"
-
2
require "log_struct/railtie"
-
2
require "log_struct/concerns/error_handling"
-
2
require "log_struct/concerns/configuration"
-
2
require "log_struct/concerns/logging"
-
-
# Require integrations
-
2
require "log_struct/integrations"
-
-
# SemanticLogger: the logger, formatters and setup LogStruct writes its lines with
-
2
require "log_struct/semantic_logger/formatter"
-
2
require "log_struct/semantic_logger/color_formatter"
-
2
require "log_struct/semantic_logger/logger"
-
2
require "log_struct/semantic_logger/setup"
-
2
require "log_struct/rails_boot_banner_silencer"
-
-
# Monkey patches for Rails compatibility
-
2
require "log_struct/monkey_patches/active_support/tagged_logging/formatter"
-
-
2
module LogStruct
-
2
extend T::Sig
-
-
2
@server_mode = T.let(false, T::Boolean)
-
-
2
class Error < StandardError; end
-
-
2
extend Concerns::ErrorHandling::ClassMethods
-
2
extend Concerns::Configuration::ClassMethods
-
2
extend Concerns::Logging::ClassMethods
-
-
4
sig { returns(T::Boolean) }
-
2
def self.server_mode?
-
37
@server_mode
-
end
-
-
3
sig { params(value: T::Boolean).void }
-
2
def self.server_mode=(value)
-
27
@server_mode = value
-
end
-
-
# Set enabled at require time based on current Rails environment.
-
# (Users can override this in their initializer which runs before the Railtie checks enabled)
-
2
set_enabled_from_rails_env!
-
-
# Silence Rails boot banners for cleaner server output
-
2
LogStruct::RailsBootBannerSilencer.install!
-
-
# Patch Puma immediately for server runs so we can convert its lifecycle
-
# messages into structured logs reliably. Not when LogStruct is disabled, and not
-
# when Puma isn't installed (install_patches! skips it).
-
2
if LogStruct.enabled? && ARGV.include?("server")
-
begin
-
require "log_struct/integrations/puma"
-
LogStruct::Integrations::Puma.install_patches!
-
-
# Patches installed now; Rack handler patch covers server boot path
-
rescue => e
-
if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env.test?
-
raise e
-
else
-
LogStruct.handle_exception(e, source: LogStruct::Source::Internal, context: {integration: :puma})
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
# Collects structured logs during very early boot before the logger is ready.
-
2
module BootBuffer
-
2
extend T::Sig
-
-
2
@@logs = T.let([], T::Array[LogStruct::Log::Interfaces::CommonFields])
-
-
4
sig { params(log: LogStruct::Log::Interfaces::CommonFields).void }
-
2
def self.add(log)
-
5
@@logs << log
-
end
-
-
4
sig { void }
-
2
def self.flush
-
4
return if @@logs.empty?
-
5
@@logs.each { |l| LogStruct.info(l) }
-
2
@@logs.clear
-
end
-
-
3
sig { void }
-
2
def self.clear
-
5
@@logs.clear
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "active_support/parameter_filter"
-
2
require "active_support/core_ext/object/duplicable"
-
2
require_relative "../configuration"
-
-
2
module LogStruct
-
2
module Concerns
-
# LogStruct.configure, whether LogStruct is on in this process, and merging Rails' filter_parameters
-
2
module Configuration
-
2
module ClassMethods
-
2
extend T::Sig
-
-
2
CONSOLE_COMMAND_ARGS = T.let(["console", "c"].freeze, T::Array[String])
-
2
EMPTY_ARGV = T.let([].freeze, T::Array[String])
-
2
RAILS_PATTERNS_LABEL = "Rails filter_parameters"
-
2
RAILS_REGEXPS_LABEL = "Rails filter_parameters (Regexps)"
-
2
RAILS_PATHS_LABEL = "Rails filter_parameters (paths)"
-
2
RAILS_PATTERN_LABELS = T.let([RAILS_PATTERNS_LABEL, RAILS_REGEXPS_LABEL].freeze, T::Array[String])
-
2
RAILS_FILTER_MASK = "[FILTERED]"
-
2
RAILS_FILTER_PROBE = "value"
-
-
4
sig { params(block: T.proc.params(config: LogStruct::Configuration).void).void }
-
2
def configure(&block)
-
55
yield(config)
-
end
-
-
4
sig { returns(LogStruct::Configuration) }
-
2
def config
-
61113
LogStruct::Configuration.instance
-
end
-
-
# (Can't use alias_method since this module is extended into LogStruct)
-
3
sig { returns(LogStruct::Configuration) }
-
2
def configuration
-
905
config
-
end
-
-
# Setter method to replace the configuration (for testing purposes)
-
3
sig { params(config: LogStruct::Configuration).void }
-
2
def configuration=(config)
-
1084
LogStruct::Configuration.set_instance(config)
-
end
-
-
4
sig { returns(T::Boolean) }
-
2
def enabled?
-
44
config.enabled
-
end
-
-
4
sig { void }
-
2
def set_enabled_from_rails_env!
-
# 1. When LOGSTRUCT_ENABLED is set, it decides: on for true, t, yes, y or 1, off for anything else.
-
# 2. Otherwise LogStruct is on when the Rails environment is in enabled_environments, the process
-
# isn't a console, and it's a server (server_process?) or runs in the test environment.
-
28
env_var_enabled = enabled_from_env_var
-
28
config.enabled = if env_var_enabled.nil?
-
22
is_console = console_process?
-
22
is_server = server_process?
-
22
in_enabled_env = config.enabled_environments.include?(::Rails.env.to_sym)
-
-
22
in_enabled_env && !is_console && (is_server || ::Rails.env.test?)
-
else
-
6
env_var_enabled
-
end
-
-
# apply_initializer_settings! compares the settings with these
-
28
@enabled_from_rails_env = T.let(config.enabled, T.nilable(T::Boolean))
-
28
@enabled_environments_from_rails_env = T.let(config.enabled_environments.dup, T.nilable(T::Array[Symbol]))
-
end
-
-
# After config/initializers run: LOGSTRUCT_ENABLED takes precedence, then `config.enabled`
-
# if an initializer changed it, then `config.enabled_environments` if an initializer changed that
-
4
sig { void }
-
2
def apply_initializer_settings!
-
5
env_var_enabled = enabled_from_env_var
-
5
if !env_var_enabled.nil?
-
2
config.enabled = env_var_enabled
-
3
elsif config.enabled == @enabled_from_rails_env && config.enabled_environments != @enabled_environments_from_rails_env
-
1
set_enabled_from_rails_env!
-
end
-
end
-
-
3
sig { returns(T::Boolean) }
-
2
def is_local?
-
13
config.local_environments.include?(::Rails.env.to_sym)
-
end
-
-
3
sig { returns(T::Boolean) }
-
2
def is_production?
-
13
!is_local?
-
end
-
-
4
sig { void }
-
2
def merge_rails_filter_parameters!
-
15
return unless ::Rails.application.config.respond_to?(:filter_parameters)
-
-
15
rails_filter_params = ::Rails.application.config.filter_parameters
-
15
return unless rails_filter_params.is_a?(Array)
-
15
return if rails_filter_params.empty?
-
-
63
blocks, patterns = rails_filter_params.partition { |entry| callable_filter?(entry) }
-
# Rails matches entries with a dot against a key's path (user.password), and the rest against the key
-
59
path_patterns, key_patterns = patterns.partition { |entry| path_pattern?(entry) }
-
56
regexps, partial_names = key_patterns.partition { |entry| entry.is_a?(Regexp) }
-
# Rails gives blocks with any arity but 2 the parameters as a third argument
-
19
path_blocks, key_blocks = blocks.partition { |block| arity_for_filter(block) == 3 }
-
-
17
matchers = key_blocks.map { |block| build_callable_filter_matcher(block) }
-
15
if partial_names.any?
-
8
matchers << build_rails_pattern_matcher(partial_names, label: RAILS_PATTERNS_LABEL, matches_logstruct_fields: false)
-
end
-
15
if regexps.any?
-
2
matchers << build_rails_pattern_matcher(regexps, label: RAILS_REGEXPS_LABEL, matches_logstruct_fields: true)
-
end
-
17
path_matchers = path_blocks.map { |block| build_path_block_matcher(block) }
-
15
path_matchers << build_rails_path_matcher(path_patterns) if path_patterns.any?
-
-
# Exact names also filter LogStruct's own fields, which the partial matches skip
-
56
names = key_patterns.filter_map { |entry| entry.to_s.downcase.to_sym if entry.is_a?(Symbol) || entry.is_a?(String) }
-
15
config.filters.filter_keys |= names
-
-
# The latest list replaces the patterns merged before
-
15
registered = config.filters.filter_matchers
-
42
registered.reject! { |matcher| RAILS_PATTERN_LABELS.include?(matcher.label) }
-
15
matchers.each do |matcher|
-
25
registered << matcher unless registered.any? { |existing| existing.label == matcher.label }
-
end
-
15
registered_paths = config.filters.path_matchers
-
15
registered_paths.reject! { |matcher| matcher.label == RAILS_PATHS_LABEL }
-
15
path_matchers.each do |matcher|
-
4
registered_paths << matcher unless registered_paths.any? { |existing| existing.label == matcher.label }
-
end
-
end
-
-
2
private
-
-
4
sig { returns(T.nilable(T::Boolean)) }
-
2
def enabled_from_env_var
-
33
value = ENV["LOGSTRUCT_ENABLED"]
-
33
return nil if value.nil?
-
-
8
%w[true t yes y 1].include?(value.strip.downcase)
-
end
-
-
3
sig { returns(T::Boolean) }
-
2
def console_process?
-
22
return true if defined?(::Rails::Console)
-
-
81
current_argv.any? { |arg| CONSOLE_COMMAND_ARGS.include?(arg) }
-
end
-
-
3
sig { returns(T::Boolean) }
-
2
def server_process?
-
22
return true if logstruct_server_mode?
-
22
return true if puma_server?
-
20
return true if defined?(::Unicorn::HttpServer)
-
20
return true if defined?(::Thin::Server)
-
20
return true if defined?(::Falcon::Server)
-
20
return true if defined?(::Rails::Server)
-
19
return true if sidekiq_server?
-
19
return true if good_job_cli?
-
-
18
false
-
end
-
-
3
sig { returns(T::Boolean) }
-
2
def puma_server?
-
# Just checking defined?(::Puma::Server) is not reliable - Puma might be installed
-
# but not running. Check $PROGRAM_NAME and ARGV to verify we're actually running puma.
-
# ARGV check is needed when running through wrapper scripts like gosu.
-
22
return true if $PROGRAM_NAME.include?("puma")
-
63
return true if current_argv.any? { |arg| arg.include?("puma") }
-
-
20
false
-
end
-
-
3
sig { returns(T::Boolean) }
-
2
def sidekiq_server?
-
19
!!(defined?(::Sidekiq) && ::Sidekiq.respond_to?(:server?) && ::Sidekiq.server?)
-
end
-
-
# GoodJob's executable (`good_job start`) sets within_exe before it loads the app
-
3
sig { returns(T::Boolean) }
-
2
def good_job_cli?
-
19
!!(defined?(::GoodJob::CLI) && ::GoodJob::CLI.respond_to?(:within_exe?) && ::GoodJob::CLI.within_exe?)
-
end
-
-
3
sig { returns(T::Boolean) }
-
2
def logstruct_server_mode?
-
22
::LogStruct.server_mode?
-
end
-
-
3
sig { returns(T::Array[String]) }
-
2
def current_argv
-
42
raw = ::ARGV
-
164
strings = raw.map { |arg| arg.to_s }
-
42
T.let(strings, T::Array[String])
-
rescue NameError
-
EMPTY_ARGV
-
end
-
-
# Rails matches Symbols and Strings as case-insensitive substrings (:passw filters
-
# "user_password") and Regexps as they are. Entries with a dot match a key by its path
-
# instead (build_rails_path_matcher).
-
#
-
# The substring matches skip LogStruct's own fields (Rails' :_key would hide GoodJob's
-
# cron_key); their exact names are added to filter_keys instead. A Regexp says exactly
-
# what it matches, so Regexps apply to every key, LogStruct's fields included.
-
4
sig { params(patterns: T::Array[T.untyped], label: String, matches_logstruct_fields: T::Boolean).returns(ConfigStruct::FilterMatcher) }
-
2
def build_rails_pattern_matcher(patterns, label:, matches_logstruct_fields:)
-
10
parameter_filter = ::ActiveSupport::ParameterFilter.new(patterns, mask: RAILS_FILTER_MASK)
-
10
callable = Kernel.lambda do |key, _value|
-
2005
parameter_filter.filter_param(key, RAILS_FILTER_PROBE) == RAILS_FILTER_MASK
-
end
-
10
ConfigStruct::FilterMatcher.new(callable: callable, label: label, matches_logstruct_fields: matches_logstruct_fields)
-
end
-
-
4
sig { params(filter: T.untyped).returns(T::Boolean) }
-
2
def callable_filter?(filter)
-
48
filter.respond_to?(:call)
-
end
-
-
# Rails treats Strings and Symbols with a dot, and Regexps with an escaped dot, as paths
-
4
sig { params(entry: T.untyped).returns(T::Boolean) }
-
2
def path_pattern?(entry)
-
44
if entry.is_a?(Regexp)
-
3
entry.source.include?("\\.")
-
else
-
41
entry.to_s.include?(".")
-
end
-
end
-
-
# Rails joins a nested parameter's keys with dots ("user.password") and matches its dotted
-
# entries against that, as case-insensitive substrings or as Regexps. LogStruct's paths
-
# start at the line's field ("params.user.password"), so an entry matches the path from
-
# there or from the field's value down, as it would in Rails.
-
3
sig { params(patterns: T::Array[T.untyped]).returns(ConfigStruct::PathFilterMatcher) }
-
2
def build_rails_path_matcher(patterns)
-
5
regexps = patterns.select { |entry| entry.is_a?(Regexp) }
-
7
strings = patterns.reject { |entry| entry.is_a?(Regexp) }.map { |entry| Regexp.escape(entry.to_s) }
-
2
regexps << Regexp.new(strings.join("|"), true) if strings.any?
-
2
callable = Kernel.lambda do |path, _value, _root|
-
23
full_path = path.join(".")
-
23
field_path = path.drop(1).join(".")
-
53
regexps.any? { |regexp| regexp.match?(full_path) || (!field_path.empty? && regexp.match?(field_path)) }
-
end
-
2
ConfigStruct::PathFilterMatcher.new(callable: callable, label: RAILS_PATHS_LABEL, uses_root: false)
-
end
-
-
# A Rails filter block with three arguments, given the parameters around the key: the
-
# line's top-level field's value (a request line's params), or the line for a top-level key
-
3
sig { params(filter: T.untyped).returns(ConfigStruct::PathFilterMatcher) }
-
2
def build_path_block_matcher(filter)
-
2
callable = Kernel.lambda do |path, original_value, root|
-
10
original_key = path.last.to_s
-
10
key = original_key.dup
-
10
value = original_value.duplicable? ? original_value.dup : original_value
-
10
result = filter.call(key, value, root)
-
10
!!result || (value.is_a?(String) && value != original_value)
-
rescue => e
-
handle_filter_error(e, filter, original_key || "")
-
false
-
end
-
2
ConfigStruct::PathFilterMatcher.new(callable: callable, label: filter.inspect)
-
end
-
-
# Rails gives filter blocks copies of the key and value, ignores what they return, and
-
# logs the value as the block left it: the Rails guides' example reverses it in place,
-
# and a block can replace it with "[FILTERED]". LogStruct filters a key when its block
-
# returns a truthy value or changes a String it's given, and never changes the logged
-
# value itself. Blocks with three arguments are path matchers (build_path_block_matcher).
-
3
sig { params(filter: T.untyped).returns(ConfigStruct::FilterMatcher) }
-
2
def build_callable_filter_matcher(filter)
-
2
callable = Kernel.lambda do |original_key, original_value|
-
5
key = original_key.dup
-
5
value = original_value.duplicable? ? original_value.dup : original_value
-
5
call_args = case arity_for_filter(filter)
-
when 0
-
[]
-
when 1
-
[key]
-
else
-
5
[key, value]
-
end
-
-
5
result = filter.call(*call_args)
-
5
!!result || (value.is_a?(String) && value != original_value)
-
rescue ArgumentError
-
begin
-
!!filter.call(key)
-
rescue => e
-
handle_filter_error(e, filter, key)
-
false
-
end
-
rescue => e
-
handle_filter_error(e, filter, key)
-
false
-
end
-
2
ConfigStruct::FilterMatcher.new(callable: callable, label: filter.inspect)
-
end
-
-
3
sig { params(filter: T.untyped).returns(Integer) }
-
2
def arity_for_filter(filter)
-
9
filter.respond_to?(:arity) ? filter.arity : 2
-
end
-
-
2
sig { params(error: StandardError, filter: T.untyped, key: String).void }
-
2
def handle_filter_error(error, filter, key)
-
context = {
-
filter: filter.class.name,
-
key: key,
-
filter_label: begin
-
filter.inspect
-
rescue
-
"unknown"
-
end
-
}
-
-
LogStruct.handle_exception(error, source: Source::Internal, context: context)
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Concerns
-
# Concern for handling errors according to configured modes
-
2
module ErrorHandling
-
2
module ClassMethods
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
# Needed for raise
-
2
requires_ancestor { Module }
-
-
# Get the error handling mode for a given source
-
3
sig { params(source: Source).returns(ErrorHandlingMode) }
-
2
def error_handling_mode_for(source)
-
59
config = LogStruct.config
-
-
# Use a case statement for type-safety
-
59
case source
-
when Source::TypeChecking
-
9
config.error_handling_modes.type_checking_errors
-
when Source::Internal
-
8
config.error_handling_modes.logstruct_errors
-
when Source::Security
-
2
config.error_handling_modes.security_errors
-
when Source::Rails, Source::App, Source::Job, Source::Storage, Source::Mailer,
-
Source::Shrine, Source::CarrierWave, Source::Sidekiq, Source::Dotenv, Source::Puma
-
40
config.error_handling_modes.standard_errors
-
else
-
# Ensures the case statement is exhaustive
-
T.absurd(source)
-
end
-
end
-
-
# Log an errors with structured data
-
3
sig { params(error: StandardError, source: Source, context: T.nilable(T::Hash[Symbol, T.untyped])).void }
-
2
def log_error(error, source:, context: nil)
-
# Create structured log entry
-
17
error_log = Log.from_exception(source, error, context || {})
-
17
LogStruct.error(error_log)
-
end
-
-
# Log an error and report it using the configured handler or MultiErrorReporter
-
2
sig { params(error: StandardError, source: Source, context: T.nilable(T::Hash[Symbol, T.untyped])).void }
-
2
def log_and_report_error(error, source:, context: nil)
-
log_error(error, source: source, context: context)
-
report_error(error, source: source, context: context)
-
end
-
-
# Report an error using the configured handler or MultiErrorReporter, without logging it. A reporter that
-
# raises (a timeout calling its service) doesn't change what happens to the error: its own error is logged,
-
# and not reported through the same reporter.
-
3
sig { params(error: StandardError, source: Source, context: T.nilable(T::Hash[Symbol, T.untyped])).void }
-
2
def report_error(error, source:, context: nil)
-
20
context = filtered_context(context)
-
20
error_handler = LogStruct.config.error_reporting_handler
-
20
if error_handler
-
# Use the configured handler
-
15
error_handler.call(error, context, source)
-
else
-
# Fall back to MultiErrorReporter (detects Sentry, Bugsnag, etc.)
-
5
LogStruct::MultiErrorReporter.report_error(error, context || {})
-
end
-
rescue => e
-
4
LogStruct.error(Log::Error.new(
-
source: Source::Internal,
-
error_class: e.class,
-
message: "The error reporter raised while reporting #{error.class}",
-
error_message: e.message,
-
backtrace: e.backtrace,
-
level: Level::Error
-
))
-
end
-
-
# The context filtered and scrubbed like a log line, for error reporters: it can hold a
-
# request's parameters or a mail's recipients
-
3
sig { params(context: T.nilable(T::Hash[Symbol, T.untyped])).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
-
2
def filtered_context(context)
-
20
return context if context.nil? || context.empty?
-
-
13
filtered = Formatter.new.process_values(context)
-
13
filtered.is_a?(Hash) ? filtered : {}
-
end
-
-
# Handle an error according to the configured error handling mode (log, report, raise, etc).
-
# Pass `log: false` when the caller has already logged the error in its own structured line.
-
3
sig { params(error: StandardError, source: Source, context: T.nilable(T::Hash[Symbol, T.untyped]), log: T::Boolean).void }
-
2
def handle_exception(error, source:, context: nil, log: true)
-
34
mode = error_handling_mode_for(source)
-
-
# Log / report in production, raise locally (dev/test)
-
34
if mode == ErrorHandlingMode::LogProduction || mode == ErrorHandlingMode::ReportProduction
-
18
raise(error) if !LogStruct.is_production?
-
end
-
-
24
case mode
-
when ErrorHandlingMode::Ignore
-
# Do nothing
-
-
when ErrorHandlingMode::Raise
-
2
raise(error)
-
-
when ErrorHandlingMode::Log, ErrorHandlingMode::LogProduction
-
11
log_error(error, source: source, context: context) if log
-
-
when ErrorHandlingMode::Report, ErrorHandlingMode::ReportProduction
-
10
log_error(error, source: source, context: context) if log
-
10
report_error(error, source: source, context: context)
-
-
else
-
# Ensures the case statement is exhaustive
-
T.absurd(mode)
-
end
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "../log"
-
-
2
module LogStruct
-
2
module Concerns
-
# Concern for handling errors according to configured modes
-
2
module Logging
-
2
module ClassMethods
-
2
extend T::Sig
-
-
# Log a log struct at debug level
-
3
sig { params(log: T.any(Log::Interfaces::CommonFields, Log::Interfaces::PublicCommonFields)).void }
-
2
def debug(log)
-
1
Rails.logger.debug(log)
-
end
-
-
# Log a log struct at info level
-
4
sig { params(log: T.any(Log::Interfaces::CommonFields, Log::Interfaces::PublicCommonFields)).void }
-
2
def info(log)
-
66
Rails.logger.info(log)
-
end
-
-
# Log a log struct at warn level
-
3
sig { params(log: T.any(Log::Interfaces::CommonFields, Log::Interfaces::PublicCommonFields)).void }
-
2
def warn(log)
-
1
Rails.logger.warn(log)
-
end
-
-
# Log a log struct at error level
-
3
sig { params(log: T.any(Log::Interfaces::CommonFields, Log::Interfaces::PublicCommonFields)).void }
-
2
def error(log)
-
68
Rails.logger.error(log)
-
end
-
-
# Log a log struct at fatal level
-
3
sig { params(log: T.any(Log::Interfaces::CommonFields, Log::Interfaces::PublicCommonFields)).void }
-
2
def fatal(log)
-
1
Rails.logger.fatal(log)
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module ConfigStruct
-
2
class ErrorHandlingModes < T::Struct
-
2
include Sorbet::SerializeSymbolKeys
-
-
# How to handle different types of errors
-
# Modes:
-
# - Ignore - Ignore the error
-
# - Log - Log the error
-
# - Report - Log and report to error tracking service (but don't crash)
-
# - LogProduction - Log error in production, raise locally (dev/test)
-
# - ReportProduction - Report error in production, raise locally (dev/test)
-
# - Raise - Always raise the error
-
-
# Configurable error handling categories
-
# Sorbet type errors in your app and its gems: logged and reported in production, so they don't pass unseen
-
2
prop :type_checking_errors, ErrorHandlingMode, default: ErrorHandlingMode::ReportProduction
-
# LogStruct's own errors
-
2
prop :logstruct_errors, ErrorHandlingMode, default: ErrorHandlingMode::LogProduction
-
2
prop :security_errors, ErrorHandlingMode, default: ErrorHandlingMode::Report
-
2
prop :standard_errors, ErrorHandlingMode, default: ErrorHandlingMode::Raise
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "active_support/hash_with_indifferent_access"
-
-
2
module LogStruct
-
# A key's words, lowercase and joined by underscores, so that keys match however they're written:
-
# apiKey, api-key, X-Api-Key and HTTP_X_API_KEY all contain "api_key"
-
2
module KeyWords
-
2
extend T::Sig
-
-
4
sig { params(key: T.untyped).returns(String) }
-
2
def self.normalize(key)
-
24652
key.to_s
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
-
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
-
.tr("- .", "___")
-
.downcase
-
end
-
end
-
-
2
module ConfigStruct
-
2
class FilterMatcher < T::Struct
-
2
extend T::Sig
-
-
2
const :callable, T.proc.params(key: String, value: T.untyped).returns(T::Boolean)
-
2
const :label, String
-
# Whether the matcher also applies to the names of LogStruct's own fields at the top of a line.
-
# LogStruct's matcher for Rails' partial matches doesn't: Rails' default :_key would filter cron_key.
-
2
const :matches_logstruct_fields, T::Boolean, default: true
-
-
4
sig { params(key: String, value: T.untyped).returns(T::Boolean) }
-
2
def matches?(key, value)
-
14273
callable.call(key, value)
-
end
-
end
-
-
# A filter that needs more than the key: Rails' entries with a dot (`card.number`) match a key by its
-
# path, and Rails gives filter blocks with three arguments the parameters around the key. The path
-
# starts at the line's top-level field (`["params", "card", "number"]`); `root` is that field's value,
-
# or the whole line for a top-level key, with indifferent access as Rails gives it. Building it copies
-
# every value inside it, so a matcher that doesn't read it sets uses_root to false and gets nil.
-
2
class PathFilterMatcher < T::Struct
-
2
extend T::Sig
-
-
2
const :callable, T.proc.params(path: T::Array[String], value: T.untyped, root: T.untyped).returns(T::Boolean)
-
2
const :label, String
-
2
const :uses_root, T::Boolean, default: true
-
-
3
sig { params(path: T::Array[String], value: T.untyped, root: T.untyped).returns(T::Boolean) }
-
2
def matches?(path, value, root)
-
35
root = nil unless uses_root
-
35
callable.call(path, value, root.is_a?(LazyRoot) ? root.value : root)
-
end
-
end
-
-
# The root a PathFilterMatcher gets, built the first time a matcher reads it
-
2
class LazyRoot
-
2
extend T::Sig
-
-
3
sig { params(value: T.untyped).void }
-
2
def initialize(value)
-
16
@source = value
-
16
@built = T.let(false, T::Boolean)
-
16
@value = T.let(nil, T.untyped)
-
end
-
-
# The value with indifferent access, as Rails gives filter blocks the request's parameters
-
3
sig { returns(T.untyped) }
-
2
def value
-
8
return @value if @built
-
-
4
@built = true
-
@value = begin
-
4
@source.is_a?(Hash) ? ActiveSupport::HashWithIndifferentAccess.new(@source) : @source
-
rescue SystemStackError, StandardError
-
@source
-
end
-
end
-
end
-
-
2
class Filters < T::Struct
-
2
include Sorbet::SerializeSymbolKeys
-
-
# Keys that should be filtered in nested structures such as request params and job arguments.
-
# A filtered value is replaced by a summary of it, with no length (which would say how long a
-
# secret is):
-
#
-
# { _filtered: { _class: "Hash", _keys_count: 3, _keys: [:key1, :key2, :key3] } } # first 10 keys
-
# { _filtered: { _class: "Array", _count: 2 } }
-
# { _filtered: { _class: "String" } } # plus its _hash for filter_keys_with_hashes
-
#
-
# A key matches when its name, or its words joined by underscores (KeyWords.normalize), equal an
-
# entry: :api_key also filters apiKey and X-Api-Key, but doesn't by itself filter api_key_id (the
-
# default filter_matchers' sensitive words do).
-
#
-
# Default: [:password, :password_confirmation, :pass, :pw, :token, :secret, :credentials, :auth,
-
# :authentication, :authorization, :credit_card, :ssn, :social_security]
-
#
-
2
prop :filter_keys,
-
T::Array[Symbol],
-
factory: -> {
-
113
%i[
-
password password_confirmation pass pw token secret
-
credentials auth authentication authorization
-
credit_card ssn social_security
-
]
-
}
-
-
# Keys that are filtered like filter_keys, and whose String values also get a _hash in their summary
-
# (see hash_salt), so lines about the same person can be followed without logging the address.
-
# Default: [:email, :email_address]
-
2
prop :filter_keys_with_hashes,
-
T::Array[Symbol],
-
113
factory: -> { %i[email email_address] }
-
-
# The key for the HMAC-SHA256 hashes of emails and filter_keys_with_hashes values. Anyone who has
-
# the key and a list of candidate emails can match them to the hashes, so keep it secret.
-
# Default: nil, which uses a key derived from the Rails app's secret_key_base. Set it to keep the
-
# same hashes in every app that logs the same people.
-
2
prop :hash_salt, T.nilable(String), default: nil
-
-
# How many hex characters of each hash to log
-
# Default: 16
-
2
prop :hash_length, Integer, default: 16
-
-
# Filter email addresses. Also controls email filtering for the ActionMailer integration
-
# (to, from, recipient fields, etc.)
-
# Default: true
-
2
prop :email_addresses, T::Boolean, default: true
-
-
# Filter URL passwords
-
# Default: true
-
2
prop :url_passwords, T::Boolean, default: true
-
-
# Filter sensitive params in URLs' query strings: params that filter_keys or the filter matchers
-
# would filter as keys (?token=, ?api_key=, user[password]=), and OAuth's ?code=
-
# Default: true
-
2
prop :url_query_params, T::Boolean, default: true
-
-
# Scrub the credentials after Bearer and Basic in strings (an Authorization header's value,
-
# logged as text): Bearer tokens of 16 or more characters, and Basic credentials that decode to
-
# user:password
-
# Default: true
-
2
prop :authorization_credentials, T::Boolean, default: true
-
-
# Filter credit card numbers
-
# Default: true
-
2
prop :credit_card_numbers, T::Boolean, default: true
-
-
# Filter phone numbers
-
# Default: true
-
2
prop :phone_numbers, T::Boolean, default: true
-
-
# Filter social security numbers
-
# Default: true
-
2
prop :ssns, T::Boolean, default: true
-
-
# Filter IP addresses
-
# Default: false
-
2
prop :ip_addresses, T::Boolean, default: false
-
-
# Filter MAC addresses
-
# Default: false
-
2
prop :mac_addresses, T::Boolean, default: false
-
-
# Default pattern for sensitive keys, matched against a key's words joined by underscores
-
# (KeyWords.normalize: apiKey, api-key and API_KEY are all "api_key"):
-
# - these words anywhere in the key: password, passwd, passphrase, passcode, pwd, token, secret, auth,
-
# authorization, authentication, cred, credential, cookie, session, jwt, bearer, otp, apikey, cvv,
-
# cvc, ssn, social_security, social_security_number, credit_card, card_number, crypt, crypted (and
-
# their plurals): access_token, newPassword, Set-Cookie, HTTP_AUTHORIZATION, credit_card_number
-
# - api_key, secret_key, private_key, access_key, encryption_key, signing_key, master_key: X-Api-Key
-
# - signature, sig, salt and certificate as the last word: X-Amz-Signature (but not signature_field_id)
-
# Matching whole words avoids false positives like "keyboard", "author" or "cron_key".
-
2
DEFAULT_SENSITIVE_KEY_PATTERN = T.let(
-
/(?:\A|_)(?:passwords?|passwd|passphrases?|passcodes?|pwd|tokens?|secrets?|auth|authorization|authentication|creds?|credentials?|cookies?|sessions?|jwts?|bearer|otps?|apikeys?|cvv|cvc|ssn|social_security(?:_numbers?)?|credit_cards?|card_numbers?|crypt|crypted)(?:_|\z)|(?:\A|_)(?:api|secret|private|access|encryption|signing|master)_keys?(?:_|\z)|(?:\A|_)(?:signature|sig|salt|certificate)s?\z/,
-
Regexp
-
)
-
-
# Matchers for keys that aren't listed by name in filter_keys. Each matcher receives the key (String)
-
# and optional value, returning true when the pair should be filtered. Includes a regex matcher for
-
# common sensitive key patterns, and LogStruct adds matchers for Rails' filter_parameters at boot.
-
2
prop :filter_matchers,
-
T::Array[FilterMatcher],
-
factory: -> {
-
[
-
113
FilterMatcher.new(
-
12258
callable: ->(key, _value) { DEFAULT_SENSITIVE_KEY_PATTERN.match?(KeyWords.normalize(key)) },
-
label: "default_sensitive_pattern"
-
)
-
]
-
}
-
-
# Matchers that need a key's path or the parameters around it (see PathFilterMatcher). LogStruct
-
# adds them for Rails' filter_parameters at boot.
-
2
prop :path_matchers,
-
T::Array[PathFilterMatcher],
-
113
factory: -> { [] }
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "active_support/notifications"
-
-
2
module LogStruct
-
2
module ConfigStruct
-
2
class Integrations < T::Struct
-
2
include Sorbet::SerializeSymbolKeys
-
-
# Enable or disable Sorbet error handler integration
-
# Default: true
-
2
prop :enable_sorbet_error_handlers, T::Boolean, default: true
-
-
# Enable or disable Lograge integration
-
# Default: true
-
2
prop :enable_lograge, T::Boolean, default: true
-
-
# Custom options for Lograge
-
# Default: nil
-
2
prop :lograge_custom_options, T.nilable(Handlers::LogrageCustomOptions), default: nil
-
-
# Enable or disable ActionMailer integration
-
# Default: true
-
2
prop :enable_actionmailer, T::Boolean, default: true
-
-
# Map instance variables on mailer to ID fields in additional_data
-
# Default: { account: :account_id, user: :user_id }
-
# Example: { organization: :org_id, company: :company_id }
-
101
prop :actionmailer_id_mapping, T::Hash[Symbol, Symbol], factory: -> { {account: :account_id, user: :user_id} }
-
-
# Enable or disable host authorization logging
-
# Default: true
-
2
prop :enable_host_authorization, T::Boolean, default: true
-
-
# Enable or disable ActiveJob integration
-
# Default: true
-
2
prop :enable_activejob, T::Boolean, default: true
-
-
# Enable or disable Rack middleware
-
# Default: true
-
2
prop :enable_rack_error_handler, T::Boolean, default: true
-
-
# Enable or disable Sidekiq integration
-
# Default: true
-
2
prop :enable_sidekiq, T::Boolean, default: true
-
-
# Enable or disable Shrine integration
-
# Default: true
-
2
prop :enable_shrine, T::Boolean, default: true
-
-
# Enable or disable ActiveStorage integration
-
# Default: true
-
2
prop :enable_activestorage, T::Boolean, default: true
-
-
# Enable or disable CarrierWave integration
-
# Default: true
-
2
prop :enable_carrierwave, T::Boolean, default: true
-
-
# Enable or disable GoodJob integration
-
# Default: true
-
2
prop :enable_goodjob, T::Boolean, default: true
-
-
# Enable SQL query logging through ActiveRecord instrumentation
-
# Default: false (can be resource intensive)
-
2
prop :enable_sql_logging, T::Boolean, default: false
-
-
# Only log SQL queries slower than this threshold (in milliseconds)
-
# Set to 0 or nil to log all queries
-
# Default: 100.0 (log queries taking >100ms)
-
2
prop :sql_slow_query_threshold, T.nilable(Float), default: 100.0
-
-
# Include bind parameters in SQL logs (disable in production for security)
-
# Default: true in development/test, false in production
-
101
prop :sql_log_bind_params, T::Boolean, factory: -> { !defined?(::Rails) || !::Rails.respond_to?(:env) || !::Rails.env.production? }
-
-
# Enable Ahoy (analytics events) integration
-
# Default: true (safe no-op unless Ahoy is defined)
-
2
prop :enable_ahoy, T::Boolean, default: true
-
-
# Enable ActiveModelSerializers integration
-
# Default: true (safe no-op unless ActiveModelSerializers is defined)
-
2
prop :enable_active_model_serializers, T::Boolean, default: true
-
-
# Enable dotenv-rails integration (convert to structured logs)
-
# Default: true
-
2
prop :enable_dotenv, T::Boolean, default: true
-
-
# Enable Puma integration (convert server lifecycle logs)
-
# Default: true
-
2
prop :enable_puma, T::Boolean, default: true
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module ConfigStruct
-
# How much of each logged value LogStruct writes. The limits keep a huge value (a request body,
-
# a large Hash) from slowing down the thread that writes the logs.
-
2
class Limits < T::Struct
-
2
include Sorbet::SerializeSymbolKeys
-
-
# The most keys written from a Hash. The rest are counted in _more_keys.
-
# Default: 100
-
2
prop :max_hash_keys, Integer, default: 100
-
-
# The most items written from an Array, followed by "... and N more items"
-
# Default: 10
-
2
prop :max_array_items, Integer, default: 10
-
-
# The most lines written from an Array that looks like a backtrace, such as an error line's
-
# backtrace. The rest are counted in a last "... and N more lines" item.
-
# Default: 20
-
2
prop :max_backtrace_lines, Integer, default: 20
-
-
# Strings longer than this many bytes are cut, and say how long they were
-
# Default: 32768 (32 KB)
-
2
prop :max_string_bytes, Integer, default: 32_768
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "handlers"
-
2
require_relative "config_struct/error_handling_modes"
-
2
require_relative "config_struct/integrations"
-
2
require_relative "config_struct/filters"
-
2
require_relative "config_struct/limits"
-
-
2
module LogStruct
-
# Core configuration class that provides a type-safe API
-
2
class Configuration < T::Struct
-
2
extend T::Sig
-
-
2
include Sorbet::SerializeSymbolKeys
-
-
# -------------------------------------------------------------------------------------
-
# Props
-
# -------------------------------------------------------------------------------------
-
-
2
prop :enabled, T::Boolean, default: true
-
100
prop :enabled_environments, T::Array[Symbol], factory: -> { [:test, :production] }
-
100
prop :local_environments, T::Array[Symbol], factory: -> { [:development, :test] }
-
-
# Prefer production-style JSON in development when LogStruct is enabled
-
2
prop :prefer_json_in_development, T::Boolean, default: true
-
-
# Enable colorful human formatter in development
-
2
prop :enable_color_output, T::Boolean, default: true
-
-
# Custom color map for the color formatter
-
2
prop :color_map, T.nilable(T::Hash[Symbol, Symbol]), default: nil
-
-
# Filter noisy loggers (ActionView, etc.)
-
2
prop :filter_noisy_loggers, T::Boolean, default: false
-
-
100
const :integrations, ConfigStruct::Integrations, factory: -> { ConfigStruct::Integrations.new }
-
71
const :filters, ConfigStruct::Filters, factory: -> { ConfigStruct::Filters.new }
-
100
const :limits, ConfigStruct::Limits, factory: -> { ConfigStruct::Limits.new }
-
-
# Custom log scrubbing handler for any additional string scrubbing
-
# Default: nil
-
2
prop :string_scrubbing_handler, T.nilable(Handlers::StringScrubber)
-
-
# Custom handler for error reporting
-
# Default: Errors are handled by MultiErrorReporter
-
# (auto-detects Sentry, Bugsnag, Rollbar, Honeybadger, etc.)
-
2
prop :error_reporting_handler, T.nilable(Handlers::ErrorReporter), default: nil
-
-
# How to handle errors from various sources
-
2
const :error_handling_modes,
-
ConfigStruct::ErrorHandlingModes,
-
factory: -> {
-
98
ConfigStruct::ErrorHandlingModes.new
-
}
-
-
# -------------------------------------------------------------------------------------
-
# Class Methods
-
# -------------------------------------------------------------------------------------
-
-
# Class‐instance variable
-
2
@instance = T.let(nil, T.nilable(Configuration))
-
-
4
sig { returns(Configuration) }
-
2
def self.instance
-
61140
@instance ||= T.let(Configuration.new, T.nilable(Configuration))
-
end
-
-
3
sig { params(config: Configuration).void }
-
2
def self.set_instance(config)
-
1084
@instance = config
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# Require all enums in this directory
-
2
require_relative "enums/error_handling_mode"
-
2
require_relative "enums/error_reporter"
-
2
require_relative "enums/event"
-
2
require_relative "enums/level"
-
2
require_relative "enums/source"
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
# Enum for error handling modes
-
2
class ErrorHandlingMode < T::Enum
-
2
enums do
-
# Always ignore the error
-
2
Ignore = new(:ignore)
-
# Always log the error
-
2
Log = new(:log)
-
# Always report to tracking service and continue
-
2
Report = new(:report)
-
# Log in production, raise locally (dev/test)
-
2
LogProduction = new(:log_production)
-
# Report in production, raise locally (dev/test)
-
2
ReportProduction = new(:report_production)
-
# Always raise regardless of environment
-
2
Raise = new(:raise)
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
class ErrorReporter < T::Enum
-
2
enums do
-
2
RailsLogger = new(:rails_logger)
-
2
Sentry = new(:sentry)
-
2
Bugsnag = new(:bugsnag)
-
2
Rollbar = new(:rollbar)
-
2
Honeybadger = new(:honeybadger)
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
# Define log event types as an enum
-
2
class Event < T::Enum
-
2
enums do
-
# Plain log messages
-
2
Log = new(:log)
-
-
# Request events
-
2
Request = new(:request)
-
-
# Job events
-
2
Enqueue = new(:enqueue)
-
2
Schedule = new(:schedule)
-
2
Start = new(:start)
-
2
Finish = new(:finish)
-
2
Retry = new(:retry)
-
2
RetryStopped = new(:retry_stopped)
-
2
Discard = new(:discard)
-
-
# File storage events (ActiveStorage, Shrine, CarrierWave, etc.)
-
2
Upload = new(:upload)
-
2
Download = new(:download)
-
2
Delete = new(:delete)
-
2
Metadata = new(:metadata)
-
2
Exist = new(:exist)
-
2
Url = new(:url)
-
-
# Data generation events
-
2
Generate = new(:generate)
-
-
# Email events
-
2
Delivery = new(:delivery)
-
2
Delivered = new(:delivered)
-
-
# Configuration / boot events
-
2
Load = new(:load)
-
2
Update = new(:update)
-
2
Save = new(:save)
-
2
Restore = new(:restore)
-
-
# Server lifecycle (e.g., Puma)
-
# Start already defined above
-
2
Shutdown = new(:shutdown)
-
-
# Security events
-
2
IPSpoof = new(:ip_spoof)
-
2
CSRFViolation = new(:csrf_violation)
-
2
BlockedHost = new(:blocked_host)
-
-
# Database events
-
2
Database = new(:database)
-
-
# Error events
-
2
Error = new(:error)
-
-
# Fallback
-
2
Unknown = new(:unknown)
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "logger"
-
-
2
module LogStruct
-
# Define log levels as an enum
-
2
class Level < T::Enum
-
2
extend T::Sig
-
-
2
enums do
-
# Standard log levels
-
2
Debug = new(:debug)
-
2
Info = new(:info)
-
2
Warn = new(:warn)
-
2
Error = new(:error)
-
2
Fatal = new(:fatal)
-
2
Unknown = new(:unknown)
-
end
-
-
# Convert a Level to the corresponding Logger integer constant
-
3
sig { returns(Integer) }
-
2
def to_severity_int
-
6
case serialize
-
1
when :debug then ::Logger::DEBUG
-
1
when :info then ::Logger::INFO
-
1
when :warn then ::Logger::WARN
-
1
when :error then ::Logger::ERROR
-
1
when :fatal then ::Logger::FATAL
-
1
else ::Logger::UNKNOWN
-
end
-
end
-
-
# Convert a string or integer severity to a Level
-
4
sig { params(severity: T.any(String, Symbol, Integer, NilClass)).returns(Level) }
-
2
def self.from_severity(severity)
-
1779
return Unknown if severity.nil?
-
1778
return from_severity_int(severity) if severity.is_a?(Integer)
-
1735
from_severity_sym(severity.downcase.to_sym)
-
end
-
-
4
sig { params(severity: Symbol).returns(Level) }
-
2
def self.from_severity_sym(severity)
-
1735
case severity.to_s.downcase.to_sym
-
25
when :debug then Debug
-
1559
when :info then Info
-
23
when :warn then Warn
-
119
when :error then Error
-
6
when :fatal then Fatal
-
3
else Unknown
-
end
-
end
-
-
3
sig { params(severity: Integer).returns(Level) }
-
2
def self.from_severity_int(severity)
-
43
case severity
-
1
when ::Logger::DEBUG then Debug
-
35
when ::Logger::INFO then Info
-
1
when ::Logger::WARN then Warn
-
3
when ::Logger::ERROR then Error
-
1
when ::Logger::FATAL then Fatal
-
2
else Unknown
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# NOTE:
-
# - This enum defines human‑readable field names (constants) that map to compact
-
# JSON key symbols via `serialize` (e.g., Database => :db).
-
# - The enum constant names are code‑generated into
-
# `schemas/meta/log-fields.json` by `scripts/generate_structs.rb` and
-
# referenced from `schemas/meta/log-source-schema.json` to strictly validate
-
# field keys in `schemas/log_sources/*`.
-
# - When adding or renaming fields here, run the generator so schema validation
-
# stays in sync.
-
#
-
# Use human-readable field names as the enum values and short field names for the JSON properties
-
-
2
module LogStruct
-
2
class LogField < T::Enum
-
2
enums do
-
# Shared fields
-
2
Source = new(:src)
-
2
Event = new(:evt)
-
2
Timestamp = new(:ts)
-
2
Level = new(:lvl)
-
-
# Common fields
-
2
Message = new(:msg)
-
2
Data = new(:data)
-
-
# Request-related fields
-
2
Path = new(:path)
-
2
HttpMethod = new(:method) # property name was http_method
-
2
SourceIp = new(:source_ip)
-
2
UserAgent = new(:user_agent)
-
2
Referer = new(:referer)
-
2
RequestId = new(:req_id)
-
2
Host = new(:host)
-
2
ContentType = new(:content_type)
-
2
Accept = new(:accept)
-
-
# HTTP-specific fields
-
2
Format = new(:format)
-
2
Controller = new(:controller)
-
2
Action = new(:action)
-
2
Status = new(:status)
-
# DurationMs already defined below for general metrics
-
2
View = new(:view)
-
2
Database = new(:db)
-
2
Params = new(:params)
-
-
# Security-specific fields
-
2
BlockedHost = new(:blocked_host)
-
2
BlockedHosts = new(:blocked_hosts)
-
2
AllowedHosts = new(:allowed_hosts)
-
2
AllowIpHosts = new(:allow_ip_hosts)
-
2
ClientIp = new(:client_ip)
-
2
XForwardedFor = new(:x_forwarded_for)
-
-
# Email-specific fields
-
2
To = new(:to)
-
2
From = new(:from)
-
2
Subject = new(:subject)
-
2
MessageId = new(:msg_id)
-
2
MailerClass = new(:mailer)
-
2
MailerAction = new(:mailer_action)
-
2
AttachmentCount = new(:attachments)
-
-
# Error fields
-
2
ErrorClass = new(:error_class)
-
2
Backtrace = new(:backtrace)
-
-
# Job-specific fields
-
2
JobId = new(:job_id)
-
2
JobClass = new(:job_class)
-
2
QueueName = new(:queue_name)
-
2
Arguments = new(:arguments)
-
2
RetryCount = new(:retry_count)
-
2
Retries = new(:retries)
-
2
Attempt = new(:attempt)
-
2
Executions = new(:executions)
-
2
ExceptionExecutions = new(:exception_executions)
-
2
ProviderJobId = new(:provider_job_id)
-
2
ScheduledAt = new(:scheduled_at)
-
2
StartedAt = new(:started_at)
-
2
FinishedAt = new(:finished_at)
-
2
DurationMs = new(:duration_ms)
-
2
WaitMs = new(:wait_ms)
-
# Deprecated: ExecutionTime/WaitTime/RunTime
-
2
ExecutionTime = new(:execution_time)
-
2
WaitTime = new(:wait_time)
-
2
RunTime = new(:run_time)
-
2
Priority = new(:priority)
-
2
CronKey = new(:cron_key)
-
2
ErrorMessage = new(:error_message)
-
# What the integration was doing when the error happened, e.g. Sidekiq's "Job raised exception"
-
2
ErrorContext = new(:error_context)
-
# The cause of an error the app didn't handle in a request (Exception#cause)
-
2
CauseClass = new(:cause_class)
-
2
CauseMessage = new(:cause_message)
-
2
Result = new(:result)
-
2
EnqueueCaller = new(:enqueue_caller)
-
-
# Dotenv fields
-
2
File = new(:file)
-
2
Vars = new(:vars)
-
2
Snapshot = new(:snapshot)
-
-
# Sidekiq-specific fields
-
2
ProcessId = new(:pid)
-
2
ThreadId = new(:tid)
-
2
Context = new(:ctx)
-
-
# Storage-specific fields (ActiveStorage)
-
2
Checksum = new(:checksum)
-
2
Exist = new(:exist)
-
2
Url = new(:url)
-
2
Prefix = new(:prefix)
-
2
Range = new(:range)
-
-
# Storage-specific fields (Shrine)
-
2
Storage = new(:storage)
-
2
Operation = new(:op)
-
2
FileId = new(:file_id)
-
2
Filename = new(:filename)
-
2
MimeType = new(:mime_type)
-
2
Size = new(:size)
-
2
Metadata = new(:metadata)
-
2
Location = new(:location)
-
2
UploadOptions = new(:upload_opts)
-
2
DownloadOptions = new(:download_opts)
-
2
Options = new(:opts)
-
2
Uploader = new(:uploader)
-
-
# CarrierWave-specific fields
-
2
Model = new(:model)
-
2
MountPoint = new(:mount_point)
-
2
Version = new(:version)
-
2
StorePath = new(:store_path)
-
2
Extension = new(:ext)
-
-
# SQL-specific fields
-
2
Sql = new(:sql)
-
2
Name = new(:name)
-
2
RowCount = new(:row_count)
-
# Use Adapter for both AMS and SQL adapter name
-
2
BindParams = new(:bind_params)
-
2
DatabaseName = new(:db_name)
-
2
ConnectionPoolSize = new(:pool_size)
-
2
ActiveConnections = new(:active_count)
-
2
OperationType = new(:op_type)
-
2
TableNames = new(:table_names)
-
-
# ActiveModelSerializers fields
-
2
Serializer = new(:serializer)
-
2
Adapter = new(:adapter)
-
2
ResourceClass = new(:resource_class)
-
-
# Ahoy-specific fields
-
2
AhoyEvent = new(:ahoy_event)
-
2
Properties = new(:properties)
-
-
# Puma / server lifecycle fields
-
2
Mode = new(:mode)
-
2
PumaVersion = new(:puma_version)
-
2
PumaCodename = new(:puma_codename)
-
2
RubyVersion = new(:ruby_version)
-
2
MinThreads = new(:min_threads)
-
2
MaxThreads = new(:max_threads)
-
2
Environment = new(:environment)
-
2
ListeningAddresses = new(:listening_addresses)
-
2
Address = new(:addr)
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
# Combined Source class that unifies log and error sources
-
2
class Source < T::Enum
-
2
enums do
-
# Error sources
-
2
TypeChecking = new(:type_checking) # For type checking errors (Sorbet)
-
2
Security = new(:security) # Security-related events
-
# Errors from LogStruct. (Cannot use LogStruct here because it confuses tapioca.)
-
2
Internal = new(:logstruct)
-
-
# Application sources
-
2
Rails = new(:rails) # For request-related logs/errors
-
2
Job = new(:job) # ActiveJob logs/errors
-
2
Storage = new(:storage) # ActiveStorage logs/errors
-
2
Mailer = new(:mailer) # ActionMailer logs/errors
-
2
App = new(:app) # General application logs/errors
-
-
# Third-party gem sources
-
2
Shrine = new(:shrine)
-
2
CarrierWave = new(:carrierwave)
-
2
Sidekiq = new(:sidekiq)
-
2
Dotenv = new(:dotenv)
-
2
Puma = new(:puma)
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "logger"
-
2
require "active_support/core_ext/object/blank"
-
2
require "active_support/hash_with_indifferent_access"
-
2
require "json"
-
2
require "globalid"
-
2
require_relative "enums/source"
-
2
require_relative "enums/event"
-
2
require_relative "enums/log_field"
-
2
require_relative "string_scrubber"
-
2
require_relative "log"
-
2
require_relative "param_filters"
-
2
require_relative "multi_error_reporter"
-
-
2
module LogStruct
-
2
class Formatter < ::Logger::Formatter
-
2
extend T::Sig
-
-
# Add current_tags method to support ActiveSupport::TaggedLogging
-
3
sig { returns(T::Array[String]) }
-
2
def current_tags
-
7
Thread.current[:activesupport_tagged_logging_tags] ||= []
-
end
-
-
# Add tagged method to support ActiveSupport::TaggedLogging
-
3
sig { params(tags: T::Array[String], blk: T.proc.params(formatter: Formatter).void).returns(T.untyped) }
-
2
def tagged(*tags, &blk)
-
1
new_tags = tags.flatten
-
1
current_tags.concat(new_tags) if new_tags.any?
-
1
yield self
-
ensure
-
1
current_tags.pop(new_tags.size) if new_tags&.any?
-
end
-
-
# Add clear_tags! method to support ActiveSupport::TaggedLogging
-
3
sig { void }
-
2
def clear_tags!
-
1
Thread.current[:activesupport_tagged_logging_tags] = []
-
end
-
-
2
sig { params(tags: T::Array[String]).returns(T.untyped) }
-
2
def push_tags(*tags)
-
current_tags.concat(tags)
-
end
-
-
4
sig { params(string: String).returns(String) }
-
2
def scrub_string(string)
-
# Use StringScrubber module to scrub sensitive information from strings
-
12773
StringScrubber.scrub(string)
-
end
-
-
2
MAX_RECURSION_DEPTH = 20
-
-
# The mask Rails' parameter filter puts in place of filtered values
-
2
RAILS_FILTERED_MASK = "[FILTERED]"
-
-
# LogStruct's own fields, which some filters don't apply to at the top of a line
-
2
LOG_FIELD_KEYS = T.let(LogField.values.map(&:serialize).to_set.freeze, T::Set[T.untyped])
-
-
# path and root are for filters that match a key by its path (see ParamFilters.should_filter_key?).
-
# They're only tracked when such filters exist: path is nil otherwise.
-
2
sig do
-
2
params(
-
arg: T.untyped,
-
recursion_depth: Integer,
-
ancestors: T::Hash[T.untyped, T::Boolean],
-
path: T.nilable(T::Array[String]),
-
root: T.untyped
-
).returns(T.untyped)
-
end
-
2
def process_values(arg, recursion_depth: 0, ancestors: {}.compare_by_identity, path: nil, root: nil)
-
17305
if recursion_depth == 0 && path.nil? && !LogStruct.config.filters.path_matchers.empty?
-
4
path = []
-
end
-
-
# Circular references and deeply nested values stop here. Values below this depth
-
# would skip filtering and scrubbing, so they're summarized instead of logged.
-
17305
return summarize_unprocessed_value(arg) if recursion_depth > MAX_RECURSION_DEPTH || ancestors.key?(arg)
-
-
17292
case arg
-
when Hash, Array
-
3881
ancestors[arg] = true
-
begin
-
3881
if arg.is_a?(Hash)
-
3584
process_hash(arg, recursion_depth: recursion_depth, ancestors: ancestors, path: path, root: root)
-
else
-
297
process_array(arg, recursion_depth: recursion_depth, ancestors: ancestors, path: path, root: root)
-
end
-
ensure
-
3881
ancestors.delete(arg)
-
end
-
when GlobalID::Identification
-
# Scrubbed like any String: a record's ID can be an email address
-
begin
-
22
scrub_string(arg.to_global_id.to_s)
-
rescue
-
begin
-
1
object = T.cast(arg, Object)
-
# Active Record is optional
-
1
if defined?(::ActiveRecord::Base) && object.is_a?(::ActiveRecord::Base)
-
scrub_string("#{object.class}(##{object.id})")
-
else
-
# For non-ActiveRecord objects that failed to_global_id, try to get a string representation
-
# If this also fails, we want to catch it and return the error placeholder
-
1
scrub_string(String(object))
-
end
-
rescue => e
-
1
LogStruct.handle_exception(e, source: Source::Internal)
-
1
"[GLOBALID_ERROR]"
-
end
-
end
-
when Source, Event
-
arg.serialize
-
when String
-
12752
scrub_limited_string(valid_utf8(arg))
-
when Time
-
arg.iso8601(3)
-
when Symbol
-
115
StringScrubber.scrub_name(arg)
-
when Float
-
# JSON has no NaN or Infinity
-
48
arg.finite? ? arg : arg.to_s
-
when Numeric, TrueClass, FalseClass, NilClass
-
343
arg
-
else
-
# Structs, ActionController::Parameters and other objects: filter and scrub what
-
# to_json would write
-
131
process_values(arg.as_json, recursion_depth: recursion_depth + 1, ancestors: ancestors, path: path, root: root)
-
end
-
rescue SystemStackError, StandardError => e
-
# A SystemStackError comes from an object whose as_json refers back to itself
-
2
e = RuntimeError.new("#{e.class}: #{e.message}") unless e.is_a?(StandardError)
-
# Report error through LogStruct's framework
-
2
context = {
-
processor_method: "process_values",
-
value_type: arg.class.name,
-
recursion_depth: recursion_depth
-
}
-
2
LogStruct.handle_exception(e, source: Source::Internal, context: context)
-
2
summarize_unprocessed_value(arg)
-
end
-
-
2
sig do
-
2
params(
-
hash: T::Hash[T.untyped, T.untyped],
-
recursion_depth: Integer,
-
ancestors: T::Hash[T.untyped, T::Boolean],
-
path: T.nilable(T::Array[String]),
-
root: T.untyped
-
).returns(T::Hash[T.untyped, T.untyped])
-
end
-
2
def process_hash(hash, recursion_depth:, ancestors:, path: nil, root: nil)
-
3584
result = {}
-
-
# Path matchers' roots are built only if a matcher reads them (most don't)
-
3584
line_root = (path && recursion_depth == 0) ? ConfigStruct::LazyRoot.new(hash) : nil
-
-
3584
max_keys = LogStruct.config.limits.max_hash_keys
-
3584
hash.each_with_index do |(key, value), index|
-
12187
if index >= max_keys
-
3
result[:_more_keys] = hash.size - max_keys
-
3
break
-
end
-
-
12184
key = valid_utf8_key(key)
-
12184
logstruct_field = recursion_depth == 0 && LOG_FIELD_KEYS.include?(key)
-
12184
key_path = path && [*path, key.to_s]
-
# A top-level key's root is the line; the keys below it get the top-level field's value
-
12184
key_root = (recursion_depth == 0) ? line_root : root
-
12184
children_root = (path && recursion_depth == 0) ? ConfigStruct::LazyRoot.new(value) : root
-
# Check if this key should be filtered at any depth
-
12184
filtered = ParamFilters.should_filter_key?(key, value, logstruct_field: logstruct_field, path: key_path, root: key_root)
-
# Keys are scrubbed too: an email address can be a key
-
12184
result[StringScrubber.scrub_name(key)] = if !filtered
-
12094
process_values(value, recursion_depth: recursion_depth + 1, ancestors: ancestors, path: key_path, root: children_root)
-
90
elsif value == RAILS_FILTERED_MASK
-
# Rails' parameter filter has already replaced the value
-
9
value
-
else
-
81
{_filtered: ParamFilters.summarize_json_attribute(key, value)}
-
end
-
end
-
-
3583
result
-
end
-
-
# The string as valid UTF-8. JSON can't hold other bytes: Puma passes headers as binary
-
# strings, and a byte like \xFF in a User-Agent would stop the whole line from being written.
-
# Bytes that aren't valid UTF-8 become U+FFFD.
-
4
sig { params(string: String).returns(String) }
-
2
def valid_utf8(string)
-
24936
StringScrubber.valid_utf8(string)
-
end
-
-
# A String or Symbol key as valid UTF-8 (symbolize_keys makes Symbols of binary String keys)
-
4
sig { params(key: T.untyped).returns(T.untyped) }
-
2
def valid_utf8_key(key)
-
12184
case key
-
when String
-
28
valid_utf8(key)
-
when Symbol
-
12156
name = key.name
-
12156
fixed = valid_utf8(name)
-
12156
fixed.equal?(name) ? key : fixed.to_sym
-
else
-
key
-
end
-
end
-
-
# The scrubbers see a little more than a cut string keeps, so data at the cut is scrubbed whole
-
2
SCRUB_MARGIN_BYTES = 256
-
-
# A string scrubbed, and cut to limits.max_string_bytes. The scrubbers see its first max_string_bytes plus
-
# SCRUB_MARGIN_BYTES: a value running past that was only partly seen, so scrub_cut_end replaces it if it can
-
# be a credential.
-
4
sig { params(string: String).returns(String) }
-
2
def scrub_limited_string(string)
-
12752
max_bytes = LogStruct.config.limits.max_string_bytes
-
12752
return scrub_string(string) if string.bytesize <= max_bytes
-
-
12
seen_bytes = max_bytes + SCRUB_MARGIN_BYTES
-
12
scrubbed = scrub_string(string.byteslice(0, seen_bytes).to_s.scrub(""))
-
12
if string.bytesize > seen_bytes
-
6
scrubbed = StringScrubber.scrub_cut_end(scrubbed)
-
6
elsif scrubbed.bytesize <= max_bytes
-
# The scrubbers saw all of it, and their replacements made it short enough
-
1
return scrubbed
-
end
-
11
kept = scrubbed.byteslice(0, max_bytes).to_s.scrub("")
-
11
"#{kept}... (cut from #{string.bytesize} bytes)"
-
end
-
-
# Like deep_symbolize_keys, but stops at circular references and at the depth where
-
# process_values stops, leaving those values for process_values to summarize
-
3
sig { params(value: T.untyped, recursion_depth: Integer, ancestors: T::Hash[T.untyped, T::Boolean]).returns(T.untyped) }
-
2
def symbolize_keys(value, recursion_depth: 0, ancestors: {}.compare_by_identity)
-
746
return value if recursion_depth > MAX_RECURSION_DEPTH || ancestors.key?(value)
-
-
740
case value
-
when Hash
-
147
ancestors[value] = true
-
147
result = value.each_with_object({}) do |(key, nested_value), symbolized|
-
344
symbol_key = key.respond_to?(:to_sym) ? key.to_sym : key
-
344
symbolized[symbol_key] = symbolize_keys(nested_value, recursion_depth: recursion_depth + 1, ancestors: ancestors)
-
end
-
147
ancestors.delete(value)
-
147
result
-
when Array
-
12
ancestors[value] = true
-
310
result = value.map { |item| symbolize_keys(item, recursion_depth: recursion_depth + 1, ancestors: ancestors) }
-
12
ancestors.delete(value)
-
12
result
-
else
-
581
value
-
end
-
end
-
-
# Describes a value without filtering, scrubbing or serializing it, so that values which
-
# couldn't be processed are never logged raw. Hashes and arrays are not serialized because
-
# they may contain circular references.
-
3
sig { params(arg: T.untyped).returns(T.untyped) }
-
2
def summarize_unprocessed_value(arg)
-
15
case arg
-
when Hash
-
10
{_filtered: {_class: arg.class.name, _keys_count: arg.size}}
-
when Array
-
1
{_filtered: {_class: arg.class.name, _count: arg.size}}
-
when String
-
1
{_filtered: {_class: arg.class.name}}
-
when Numeric, Symbol, TrueClass, FalseClass, NilClass
-
2
arg
-
else
-
1
{_filtered: {_class: arg.class.name}}
-
end
-
end
-
-
4
sig { params(log_value: T.untyped, time: Time).returns(T::Hash[Symbol, T.untyped]) }
-
2
def log_value_to_hash(log_value, time:)
-
1760
case log_value
-
when Log::Interfaces::CommonFields
-
# Our log classes all implement a custom #serialize method that use symbol keys
-
1674
log_value.serialize
-
-
when T::Struct
-
# Default T::Struct.serialize methods returns a hash with string keys, so convert them to symbols
-
2
symbolize_keys(log_value.serialize)
-
-
when Hash
-
# Use hash as is and convert string keys to symbols
-
74
symbolize_keys(log_value)
-
-
when Exception
-
# logger.error(exception): its class, message and backtrace
-
2
Log.from_exception(Source::App, log_value, {}, time).serialize
-
-
else
-
# Create a Plain log with the message as a string and serialize it with symbol keys
-
# log_value can be literally anything: Integer, Float, Boolean, NilClass, etc.
-
8
log_message = case log_value
-
# Handle all the basic types without any further processing
-
when String, Symbol, TrueClass, FalseClass, NilClass, Array, Hash, Time, Numeric
-
5
log_value
-
else
-
# Handle the serialization of complex objects in a useful way:
-
#
-
# 1. For ActiveRecord models: Use as_json which includes attributes
-
# 2. For objects with custom as_json implementations: Use their implementation
-
# 3. For basic objects that only have ActiveSupport's as_json: Use to_s
-
begin
-
3
method_owner = log_value.method(:as_json).owner
-
-
# If it's ActiveRecord, ActiveModel, or a custom implementation, use as_json
-
2
if method_owner.to_s.include?("ActiveRecord") ||
-
method_owner.to_s.include?("ActiveModel") ||
-
method_owner.to_s.exclude?("ActiveSupport::CoreExtensions") &&
-
method_owner.to_s.exclude?("Object")
-
1
log_value.as_json
-
else
-
# For plain objects with only the default ActiveSupport as_json
-
1
log_value.to_s
-
end
-
rescue => e
-
# Handle serialization errors
-
context = {
-
1
object_class: log_value.class.name,
-
object_inspect: log_value.inspect.truncate(100)
-
}
-
1
LogStruct.handle_exception(e, source: Source::Internal, context: context)
-
-
# Fall back to the string representation to ensure we continue processing
-
1
log_value.to_s
-
end
-
end
-
-
8
Log::Plain.new(
-
message: log_message,
-
timestamp: time
-
).serialize
-
end
-
end
-
-
# Serializes Log (or string) into JSON.
-
# request_id: the request's ID (default: the one RequestContext stored for this thread).
-
# context: fields for the line's ctx, like the running job's ID and class. The line's own ctx keys win.
-
# tags: the logger's tags (logger.tagged("billing")), written as tags unless the line has its own.
-
2
sig do
-
2
params(
-
severity: T.any(String, Symbol, Integer),
-
time: Time,
-
progname: T.nilable(String),
-
log_value: T.untyped,
-
request_id: T.nilable(String),
-
context: T.nilable(T::Hash[T.untyped, T.untyped]),
-
tags: T.nilable(T::Array[T.untyped])
-
).returns(String)
-
end
-
2
def call(severity, time, progname, log_value, request_id: nil, context: nil, tags: nil)
-
1753
level_enum = Level.from_severity(severity)
-
-
1753
data = log_value_to_hash(log_value, time: time)
-
-
# Filter params, scrub sensitive values, format ActiveJob GlobalID arguments
-
1753
data = process_values(data)
-
-
# Add standard fields if not already present
-
1753
data[:src] ||= Source::App
-
1753
data[:evt] ||= Event::Log
-
1753
data[:ts] ||= time.iso8601(3)
-
1753
data[:lvl] = level_enum # Set level from severity parameter
-
1753
add_metadata(data, progname: progname, request_id: request_id || Thread.current[:logstruct_request_id], context: context, tags: tags)
-
-
begin
-
1753
generate_json(data)
-
rescue SystemStackError, StandardError => e
-
1
unwritable_line_json(data, e)
-
end
-
end
-
-
# A line that can't be written as JSON is replaced by one that says so, instead of being lost
-
3
sig { params(data: T::Hash[T.untyped, T.untyped], error: Exception).returns(String) }
-
2
def unwritable_line_json(data, error)
-
1
level = data[:lvl]
-
line = {
-
1
src: enum_or_string(data[:src]) || Source::Internal.serialize,
-
evt: enum_or_string(data[:evt]) || Event::Error.serialize,
-
1
ts: data[:ts].is_a?(String) ? data[:ts] : Time.now.iso8601(3),
-
1
lvl: level.is_a?(Level) ? level.serialize : Level::Error.serialize,
-
msg: "LogStruct couldn't write this line as JSON",
-
error_class: error.class.name
-
}
-
1
unless Thread.current[:logstruct_reporting_unwritable_line]
-
begin
-
1
Thread.current[:logstruct_reporting_unwritable_line] = true
-
1
reported = error.is_a?(StandardError) ? error : RuntimeError.new("#{error.class}: #{error.message}")
-
1
LogStruct.handle_exception(reported, source: Source::Internal, context: {src: line[:src], evt: line[:evt]})
-
ensure
-
1
Thread.current[:logstruct_reporting_unwritable_line] = nil
-
end
-
end
-
1
"#{::JSON.generate(line)}\n"
-
end
-
-
# The program name, request ID and job context, filtered and scrubbed like the rest of the line:
-
# an X-Request-Id header can carry anything
-
2
sig do
-
2
params(
-
data: T::Hash[T.untyped, T.untyped],
-
progname: T.nilable(String),
-
request_id: T.untyped,
-
context: T.nilable(T::Hash[T.untyped, T.untyped]),
-
tags: T.nilable(T::Array[T.untyped])
-
).void
-
end
-
2
def add_metadata(data, progname:, request_id:, context:, tags:)
-
1753
metadata = {}
-
1753
metadata[:prog] = progname if progname.present?
-
1753
metadata[:tags] = tags if tags.is_a?(Array) && !tags.empty? && !data.key?(:tags)
-
1753
if request_id.is_a?(String) && !request_id.empty? && !data.key?(:req_id)
-
51
metadata[:req_id] = request_id
-
end
-
1753
metadata[:ctx] = symbolize_keys(context) if context.present?
-
1753
return if metadata.empty?
-
-
1663
processed = process_values(metadata)
-
1663
job_context = processed.delete(:ctx)
-
1663
data.merge!(processed)
-
1663
return if job_context.nil?
-
-
28
line_context = data[:ctx]
-
# A logged Hash's own ctx keeps its keys and gains the job's
-
28
data[:ctx] = line_context.is_a?(Hash) ? job_context.merge(line_context) : (line_context || job_context)
-
end
-
-
3
sig { params(value: T.untyped).returns(T.nilable(String)) }
-
2
def enum_or_string(value)
-
2
if value.is_a?(Source) || value.is_a?(Event)
-
2
value.serialize.to_s
-
elsif value.is_a?(String)
-
value
-
end
-
end
-
-
# Output as JSON with a newline. We mock this method in tests so we can
-
# inspect the data right before it gets turned into a JSON string.
-
4
sig { params(data: T::Hash[T.untyped, T.untyped]).returns(String) }
-
2
def generate_json(data)
-
1753
"#{data.to_json}\n"
-
end
-
-
# An array's items keep the array's path, as in Rails (users.name filters each user's name)
-
2
sig do
-
2
params(
-
array: T::Array[T.untyped],
-
recursion_depth: Integer,
-
ancestors: T::Hash[T.untyped, T::Boolean],
-
path: T.nilable(T::Array[String]),
-
root: T.untyped
-
).returns(T::Array[T.untyped])
-
end
-
2
def process_array(array, recursion_depth:, ancestors:, path: nil, root: nil)
-
297
return [] if array.empty?
-
-
270
limits = LogStruct.config.limits
-
270
backtrace = looks_like_backtrace_array?(array)
-
270
max_items = backtrace ? limits.max_backtrace_lines : limits.max_array_items
-
270
processed = array.first(max_items).map do |value|
-
1619
process_values(value, recursion_depth: recursion_depth + 1, ancestors: ancestors, path: path, root: root)
-
end
-
270
if array.size > max_items
-
66
processed << "... and #{array.size - max_items} more #{backtrace ? "lines" : "items"}"
-
end
-
-
270
processed
-
end
-
-
# Check if an array looks like a backtrace (array of strings with file:line pattern)
-
3
sig { params(array: T::Array[T.untyped]).returns(T::Boolean) }
-
2
def looks_like_backtrace_array?(array)
-
270
backtrace_like_count = array.first(5).count do |element|
-
678
element.is_a?(String) && element.match?(/\A[^:\s]+:\d+/)
-
end
-
-
270
backtrace_like_count >= 3
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
# Module for custom handlers used throughout the library
-
2
module Handlers
-
# Type for Lograge custom options
-
2
LogrageCustomOptions = T.type_alias {
-
2
T.proc.params(
-
event: ActiveSupport::Notifications::Event,
-
options: T::Hash[Symbol, T.untyped]
-
).returns(T.untyped)
-
}
-
-
# Type for error reporting handlers
-
2
ErrorReporter = T.type_alias {
-
2
T.proc.params(
-
error: StandardError,
-
context: T.nilable(T::Hash[Symbol, T.untyped]),
-
source: Source
-
).void
-
}
-
-
# Type for string scrubbing handlers
-
4
StringScrubber = T.type_alias { T.proc.params(string: String).returns(String) }
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "openssl"
-
-
2
module LogStruct
-
# Utility module for hashing sensitive data
-
2
module HashUtils
-
# The key when there's neither a configured hash_salt nor a Rails app with a secret_key_base.
-
# It's public, so these hashes can be matched to a list of candidate values.
-
2
FALLBACK_KEY = "l0g5t0p"
-
-
2
class << self
-
2
extend T::Sig
-
-
# An HMAC-SHA256 of the value, so lines about the same email can be traced without logging it
-
3
sig { params(value: String).returns(String) }
-
2
def hash_value(value)
-
350
length = LogStruct.config.filters.hash_length
-
350
OpenSSL::HMAC.hexdigest("SHA256", hash_key, value)[0...length] || "error"
-
end
-
-
# The configured hash_salt, or a key derived from the app's secret_key_base
-
3
sig { returns(String) }
-
2
def hash_key
-
352
LogStruct.config.filters.hash_salt || derived_key || FALLBACK_KEY
-
end
-
-
# Forget the derived key (for tests that change the Rails app)
-
3
sig { void }
-
2
def reset!
-
6
@derived_key = T.let(nil, T.nilable(String))
-
end
-
-
2
private
-
-
3
sig { returns(T.nilable(String)) }
-
2
def derived_key
-
5
@derived_key ||= T.let(begin
-
3
application = ::Rails.application if defined?(::Rails) && ::Rails.respond_to?(:application)
-
# Rails derives keys like this one for cookies and messages; nil until the app has its secret
-
3
application&.key_generator&.generate_key("LogStruct hashed values", 32)
-
rescue
-
nil
-
end,
-
T.nilable(String))
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "integrations/integration_interface"
-
2
require_relative "integrations/rails_log_subscribers"
-
2
require_relative "integrations/request_context"
-
2
require_relative "integrations/active_job"
-
2
require_relative "integrations/active_record"
-
2
require_relative "integrations/rack_error_handler"
-
2
require_relative "integrations/host_authorization"
-
2
require_relative "integrations/action_mailer"
-
2
require_relative "integrations/lograge"
-
2
require_relative "integrations/shrine"
-
2
require_relative "integrations/sidekiq"
-
2
require_relative "integrations/good_job"
-
2
require_relative "integrations/active_storage"
-
2
require_relative "integrations/carrierwave"
-
2
require_relative "integrations/sorbet"
-
2
require_relative "integrations/ahoy"
-
2
require_relative "integrations/active_model_serializers"
-
2
require_relative "integrations/dotenv"
-
2
require_relative "integrations/puma"
-
-
2
module LogStruct
-
2
module Integrations
-
2
extend T::Sig
-
-
# Register generic initializers on the Railtie to keep integration
-
# wiring centralized (boot replay interception and resolution).
-
4
sig { params(railtie: T.untyped).void }
-
2
def self.setup_initializers(railtie)
-
# Intercept any boot-time replays (e.g., dotenv) before those railties run
-
2
railtie.initializer "logstruct.intercept_boot_replays", before: "dotenv" do
-
2
LogStruct::Integrations::Dotenv.intercept_logger_setter!
-
end
-
-
# Decide which set of boot logs to emit once `enabled` is final
-
2
railtie.initializer "logstruct.resolve_boot_logs", after: "logstruct.setup" do
-
2
LogStruct::Integrations::Dotenv.resolve_boot_logs!
-
end
-
end
-
-
4
sig { params(stage: Symbol).void }
-
2
def self.setup_integrations(stage: :all)
-
4
config = LogStruct.config
-
-
4
case stage
-
when :non_middleware
-
2
setup_non_middleware_integrations(config)
-
when :middleware
-
2
setup_middleware_integrations(config)
-
when :all
-
setup_non_middleware_integrations(config)
-
setup_middleware_integrations(config)
-
else
-
raise ArgumentError, "Unknown integration stage: #{stage}"
-
end
-
end
-
-
4
sig { params(config: LogStruct::Configuration).void }
-
2
def self.setup_non_middleware_integrations(config)
-
2
Integrations::Lograge.setup(config) if config.integrations.enable_lograge
-
2
Integrations::ActionMailer.setup(config) if config.integrations.enable_actionmailer
-
2
Integrations::ActiveJob.setup(config) if config.integrations.enable_activejob
-
2
Integrations::ActiveRecord.setup(config) if config.integrations.enable_sql_logging
-
2
Integrations::Sidekiq.setup(config) if config.integrations.enable_sidekiq
-
2
Integrations::GoodJob.setup(config) if config.integrations.enable_goodjob
-
2
Integrations::Ahoy.setup(config) if config.integrations.enable_ahoy
-
2
Integrations::ActiveModelSerializers.setup(config) if config.integrations.enable_active_model_serializers
-
2
Integrations::Shrine.setup(config) if config.integrations.enable_shrine
-
2
Integrations::ActiveStorage.setup(config) if config.integrations.enable_activestorage
-
2
Integrations::CarrierWave.setup(config) if config.integrations.enable_carrierwave
-
2
Integrations::Sorbet.setup(config) if config.integrations.enable_sorbet_error_handlers
-
2
if config.enabled && config.integrations.enable_dotenv
-
2
Integrations::Dotenv.setup(config)
-
end
-
2
Integrations::Puma.setup(config) if config.integrations.enable_puma
-
end
-
-
4
sig { params(config: LogStruct::Configuration).void }
-
2
def self.setup_middleware_integrations(config)
-
2
Integrations::RequestContext.setup(config)
-
2
Integrations::HostAuthorization.setup(config) if config.integrations.enable_host_authorization
-
2
Integrations::RackErrorHandler.setup(config) if config.integrations.enable_rack_error_handler
-
end
-
-
2
private_class_method :setup_non_middleware_integrations, :setup_middleware_integrations
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
begin
-
2
require "action_mailer"
-
rescue LoadError
-
# actionmailer gem is not available, integration will be skipped
-
end
-
-
2
if defined?(::ActionMailer)
-
2
require_relative "action_mailer/metadata_collection"
-
2
require_relative "action_mailer/event_logging"
-
2
require_relative "action_mailer/error_handling"
-
2
require_relative "action_mailer/fallback_error_handlers"
-
end
-
-
2
module LogStruct
-
2
module Integrations
-
# ActionMailer integration for structured logging
-
2
module ActionMailer
-
2
extend T::Sig
-
2
extend IntegrationInterface
-
-
# Set up ActionMailer structured logging
-
4
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def self.setup(config)
-
10
return nil unless defined?(::ActionMailer)
-
10
return nil unless config.enabled
-
10
return nil unless config.integrations.enable_actionmailer
-
-
# Register our custom observers and handlers on all mailers. The hook runs right away
-
# if ActionMailer::Base is already loaded.
-
9
ActiveSupport.on_load(:action_mailer) do
-
# LogStruct's delivery lines replace Rails' mail lines, which also log the whole mail at debug level.
-
# Mailers keep their logger, so an app's own logger calls in a mailer are still logged.
-
9
RailsLogSubscribers.detach(::ActionMailer::LogSubscriber, :action_mailer) if defined?(::ActionMailer::LogSubscriber)
-
-
9
prepend LogStruct::Integrations::ActionMailer::EventLogging
-
9
prepend LogStruct::Integrations::ActionMailer::ErrorHandling
-
9
prepend LogStruct::Integrations::ActionMailer::MetadataCollection
-
end
-
-
9
true
-
end
-
-
# Where LogStruct doesn't set up ActionMailer, mailers still get the error handlers they can name in
-
# rescue_from (see FallbackErrorHandlers)
-
4
sig { params(config: LogStruct::Configuration).void }
-
2
def self.setup_fallback_error_handlers(config)
-
6
return unless defined?(::ActionMailer)
-
6
return if config.enabled && config.integrations.enable_actionmailer
-
-
2
ActiveSupport.on_load(:action_mailer) do
-
next if include?(LogStruct::Integrations::ActionMailer::FallbackErrorHandlers)
-
-
include LogStruct::Integrations::ActionMailer::FallbackErrorHandlers
-
extend LogStruct::Integrations::ActionMailer::FallbackErrorHandlers::ClassMethods
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Integrations
-
2
module ActionMailer
-
# Handles error handling for ActionMailer
-
#
-
# IMPORTANT LIMITATIONS:
-
# 1. This module must be included BEFORE users define rescue_from handlers
-
# to ensure proper handler precedence (user handlers are checked first)
-
# 2. Rails rescue_from handlers don't bubble to parent class handlers after reraise
-
# 3. Handler order matters: Rails checks rescue_from handlers in reverse declaration order
-
2
module ErrorHandling
-
2
extend T::Sig
-
2
extend ActiveSupport::Concern
-
-
# ActionMailer::MailDeliveryJob passes errors to the mailer class (`handle_exception`), which
-
# looks the rescue_from handlers up on the class, so the handlers are defined there too.
-
# (ActiveSupport::Concern extends the class with ClassMethods when it's included or prepended.)
-
2
module ClassMethods
-
2
extend T::Sig
-
-
# Logs the error, and doesn't report or raise it
-
3
sig { params(ex: StandardError).void }
-
2
def log_and_ignore_error(ex)
-
4
ErrorHandling.handle_mailer_class_error(self, ex, report: false, reraise: false)
-
end
-
-
# Logs and reports the error, and doesn't raise it
-
3
sig { params(ex: StandardError).void }
-
2
def log_and_report_error(ex)
-
7
ErrorHandling.handle_mailer_class_error(self, ex, report: true, reraise: false)
-
end
-
-
# Logs the error and raises it again, reporting it too in the Report modes
-
3
sig { params(ex: StandardError).void }
-
2
def log_and_reraise_error(ex)
-
5
ErrorHandling.handle_mailer_class_error(self, ex, report: ErrorHandling.report_before_reraise?, reraise: true)
-
end
-
end
-
-
# log_and_reraise_error reports the error where standard_errors' mode reports the app's errors
-
# (Report, and ReportProduction in production). Elsewhere whatever rescues the raised error decides.
-
3
sig { returns(T::Boolean) }
-
2
def self.report_before_reraise?
-
24
mode = LogStruct.error_handling_mode_for(Source::Mailer)
-
24
mode == ErrorHandlingMode::Report || (mode == ErrorHandlingMode::ReportProduction && LogStruct.is_production?)
-
end
-
-
# An error that reaches the class handler from the delivery itself was logged (and reported)
-
# by the mailer's handler, which raised it again (log_and_reraise_error). It's raised again,
-
# without logging it twice, so the job fails as deliver_now would.
-
3
sig { params(mailer_class: T.untyped, error: StandardError, report: T::Boolean, reraise: T::Boolean).void }
-
2
def self.handle_mailer_class_error(mailer_class, error, report:, reraise:)
-
16
Kernel.raise error if LogStruct::LoggedErrors.logged?(error)
-
-
13
message = if reraise
-
2
"#{error.class}: Email delivery failed. Error message: #{error.message}"
-
else
-
11
"#{error.class}: Cannot send email. Error message: #{error.message}"
-
end
-
13
LogStruct.error(Log::ActionMailer::Error.new(
-
mailer_class: mailer_class.to_s,
-
error_class: error.class,
-
message: message,
-
backtrace: error.backtrace,
-
level: Level::Error
-
))
-
13
LogStruct.report_error(error, source: Source::Mailer, context: {mailer_class: mailer_class.to_s}) if report
-
-
13
Kernel.raise error if reraise
-
end
-
-
3
sig { returns(T.nilable(T::Boolean)) }
-
2
attr_accessor :logstruct_mail_failed
-
-
# NOTE: rescue_from handlers are checked in reverse order of declaration.
-
# We want LogStruct handlers to be checked AFTER user handlers (lower priority),
-
# so we need to add them BEFORE user handlers are declared.
-
-
# This will be called when the module is included/prepended
-
4
sig { params(base: T.untyped).void }
-
2
def self.install_handler(base)
-
# Only add the handler once per class
-
10
return if base.instance_variable_get(:@_logstruct_handler_installed)
-
-
# Add our handler FIRST so it has lower priority than user handlers
-
3
base.rescue_from StandardError, with: :log_and_reraise_error
-
-
# Mark as installed to prevent duplicates
-
3
base.instance_variable_set(:@_logstruct_handler_installed, true)
-
end
-
-
2
included do
-
LogStruct::Integrations::ActionMailer::ErrorHandling.install_handler(self)
-
end
-
-
# Also support prepended (used by tests and manual setup)
-
4
sig { params(base: T.untyped).void }
-
2
def self.prepended(base)
-
10
install_handler(base)
-
end
-
-
2
protected
-
-
# Logs the error, and doesn't report or raise it
-
3
sig { params(ex: StandardError).void }
-
2
def log_and_ignore_error(ex)
-
8
self.logstruct_mail_failed = true
-
8
log_email_delivery_error(ex, notify: false, report: false, reraise: false)
-
end
-
-
# Logs and reports the error, and doesn't raise it
-
3
sig { params(ex: StandardError).void }
-
2
def log_and_report_error(ex)
-
14
log_email_delivery_error(ex, notify: false, report: true, reraise: false)
-
end
-
-
# Logs the error and raises it again, reporting it too in the Report modes (report_before_reraise?)
-
3
sig { params(ex: StandardError).void }
-
2
def log_and_reraise_error(ex)
-
18
log_email_delivery_error(ex, notify: false, report: ErrorHandling.report_before_reraise?, reraise: true)
-
end
-
-
2
private
-
-
# Handle an error from a mailer
-
3
sig { params(mailer: T.untyped, error: StandardError, message: String).void }
-
2
def log_structured_error(mailer, error, message)
-
# Get message if available
-
40
mailer_message = mailer.respond_to?(:message) ? mailer.message : nil
-
-
# Prepare universal mailer fields
-
40
message_data = {}
-
40
MetadataCollection.add_message_metadata(mailer, message_data)
-
-
# Prepare app-specific context data for additional_data
-
40
context_data = {}
-
40
MetadataCollection.add_context_metadata(mailer, context_data)
-
-
# Extract email fields
-
40
to = mailer_message&.to
-
40
from = mailer_message&.from&.first
-
40
subject = mailer_message&.subject
-
40
message_id = extract_message_id_from_mailer(mailer)
-
-
# Create ActionMailer-specific error struct
-
40
exception_data = Log::ActionMailer::Error.new(
-
to: to,
-
from: from,
-
subject: subject,
-
message_id: message_id,
-
mailer_class: mailer.class.to_s,
-
40
mailer_action: mailer.respond_to?(:action_name) ? mailer.action_name&.to_s : nil,
-
attachment_count: message_data[:attachment_count],
-
error_class: error.class,
-
message: message,
-
backtrace: error.backtrace,
-
additional_data: context_data.presence,
-
timestamp: Time.now,
-
level: Level::Error
-
)
-
-
# Log the structured error
-
40
LogStruct.error(exception_data)
-
end
-
-
# Extract message ID from the mailer
-
3
sig { params(mailer: T.untyped).returns(T.nilable(String)) }
-
2
def extract_message_id_from_mailer(mailer)
-
40
return nil unless mailer.respond_to?(:message)
-
-
40
mail_message = mailer.message
-
40
return nil unless mail_message.respond_to?(:message_id)
-
-
40
mail_message.message_id
-
end
-
-
# Log when email delivery fails
-
3
sig { params(error: StandardError, notify: T::Boolean, report: T::Boolean, reraise: T::Boolean).void }
-
2
def log_email_delivery_error(error, notify: false, report: true, reraise: true)
-
# Generate appropriate error message
-
40
message = error_message_for(error, reraise)
-
-
# Use structured error logging
-
40
log_structured_error(self, error, message)
-
# The mailer class handler gets the error if it's raised from here (re-raised, or raised by
-
# LogStruct.handle_exception in Raise mode), and mustn't log or report it again
-
40
LogStruct::LoggedErrors.mark(error)
-
-
# Handle notifications, reporting and raising
-
40
handle_error_notifications(error, notify, report, reraise)
-
end
-
-
# Generate appropriate error message based on error handling strategy
-
3
sig { params(error: StandardError, reraise: T::Boolean).returns(String) }
-
2
def error_message_for(error, reraise)
-
40
if reraise
-
18
"#{error.class}: Email delivery failed. Recipients: #{recipients(error)}. Error message: #{error.message}"
-
else
-
22
"#{error.class}: Cannot send email to #{recipients(error)}. Error message: #{error.message}"
-
end
-
end
-
-
# Handle error notifications, reporting, and reraising
-
3
sig { params(error: StandardError, notify: T::Boolean, report: T::Boolean, reraise: T::Boolean).void }
-
2
def handle_error_notifications(error, notify, report, reraise)
-
# Log a notification event if requested
-
40
log_notification_event(error) if notify
-
-
# Report to error reporting service if requested. The structured error line is already logged.
-
40
if report
-
context = {
-
22
mailer_class: self.class.to_s,
-
22
mailer_action: respond_to?(:action_name) ? action_name : nil,
-
recipients: recipients(error)
-
}
-
22
LogStruct.report_error(error, source: Source::Mailer, context: context)
-
end
-
-
# Re-raise the error if requested
-
40
Kernel.raise error if reraise
-
end
-
-
# Log a notification event that can be picked up by external systems
-
2
sig { params(error: StandardError).void }
-
2
def log_notification_event(error)
-
context_data = {
-
mailer: self.class.to_s,
-
action: action_name&.to_s,
-
recipients: recipients(error)
-
}
-
exception_data = build_exception_data(error, Level::Info, context_data)
-
-
# Log the error at info level since it's not a critical error
-
LogStruct.info(exception_data)
-
end
-
-
2
sig { params(error: StandardError, level: Level, context_data: T::Hash[Symbol, T.untyped]).returns(Log::ActionMailer::Error) }
-
2
def build_exception_data(error, level, context_data)
-
mailer_message = respond_to?(:message) ? message : nil
-
-
message_data = {}
-
MetadataCollection.add_message_metadata(self, message_data)
-
-
MetadataCollection.add_context_metadata(self, context_data)
-
-
to = mailer_message&.to
-
from = mailer_message&.from&.first
-
subject = mailer_message&.subject
-
message_id = extract_message_id_from_mailer(self)
-
-
Log::ActionMailer::Error.new(
-
to: to,
-
from: from,
-
subject: subject,
-
message_id: message_id,
-
mailer_class: self.class.to_s,
-
mailer_action: respond_to?(:action_name) ? action_name&.to_s : nil,
-
attachment_count: message_data[:attachment_count],
-
error_class: error.class,
-
message: error.message,
-
backtrace: error.backtrace,
-
additional_data: context_data.presence,
-
timestamp: Time.now,
-
level: level
-
)
-
end
-
-
# The formatter scrubs the addresses in the line
-
3
sig { params(error: StandardError).returns(String) }
-
2
def recipients(error)
-
# Some delivery errors (Postmark's, for example) have the recipients that failed
-
62
if error.respond_to?(:recipients) && T.unsafe(error).recipients.present?
-
3
return T.unsafe(error).recipients.join(", ")
-
end
-
-
# Otherwise the mail's own: to, cc and bcc (unknown when the mailer failed before it built the mail)
-
59
mail = respond_to?(:message) ? message : nil
-
59
destinations = mail.respond_to?(:destinations) ? Array(mail.destinations).compact : []
-
59
destinations.empty? ? "unknown" : destinations.join(", ")
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Integrations
-
2
module ActionMailer
-
# Handles logging of email delivery events
-
2
module EventLogging
-
2
extend ActiveSupport::Concern
-
2
extend T::Sig
-
2
extend T::Helpers
-
2
requires_ancestor { ::ActionMailer::Base }
-
2
requires_ancestor { ErrorHandling }
-
-
2
included do
-
T.bind(self, T.class_of(::ActionMailer::Base))
-
-
# Add callbacks for delivery events
-
before_deliver :log_email_delivery
-
after_deliver :log_email_delivered
-
end
-
-
# When this module is prepended (our integration uses prepend), ensure callbacks are registered
-
2
if respond_to?(:prepended)
-
2
prepended do
-
2
T.bind(self, T.class_of(::ActionMailer::Base))
-
-
# Add callbacks for delivery events
-
2
before_deliver :log_email_delivery
-
2
after_deliver :log_email_delivered
-
end
-
end
-
-
2
protected
-
-
# Log when an email is about to be delivered
-
3
sig { void }
-
2
def log_email_delivery
-
45
log_mailer_event(Event::Delivery)
-
end
-
-
# Log when an email is delivered
-
3
sig { void }
-
2
def log_email_delivered
-
# Don't log delivered event if the delivery failed (error was handled with log_and_ignore_error)
-
7
return if logstruct_mail_failed
-
-
6
log_mailer_event(Event::Delivered)
-
end
-
-
2
private
-
-
# Log a mailer event with the given event type
-
3
sig { params(event_type: LogStruct::Event, level: Symbol, additional_data: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
-
2
def log_mailer_event(event_type, level = :info, additional_data = {})
-
# Get message (self refers to the mailer instance)
-
51
mailer_message = message if respond_to?(:message)
-
-
# Prepare universal mailer fields
-
51
message_data = {}
-
51
MetadataCollection.add_message_metadata(self, message_data)
-
-
# Prepare app-specific context data for additional_data
-
51
context_data = {}
-
51
MetadataCollection.add_context_metadata(self, context_data)
-
51
context_data.merge!(additional_data) if additional_data.present?
-
-
# Extract email fields (these will be filtered if email_addresses=true)
-
51
to = mailer_message&.to
-
51
from = mailer_message&.from&.first
-
51
subject = mailer_message&.subject
-
-
51
base_fields = Log::ActionMailer::BaseFields.new(
-
to: to,
-
from: from,
-
subject: subject,
-
message_id: extract_message_id,
-
mailer_class: self.class.to_s,
-
mailer_action: action_name.to_s,
-
attachment_count: message_data[:attachment_count]
-
)
-
-
51
log = case event_type
-
when Event::Delivery
-
45
Log::ActionMailer::Delivery.new(
-
**base_fields.to_kwargs,
-
additional_data: context_data.presence,
-
timestamp: Time.now
-
)
-
when Event::Delivered
-
6
Log::ActionMailer::Delivered.new(
-
**base_fields.to_kwargs,
-
additional_data: context_data.presence,
-
timestamp: Time.now
-
)
-
else
-
return
-
end
-
51
LogStruct.info(log)
-
51
log
-
end
-
-
# Extract message ID from the mailer
-
3
sig { returns(T.nilable(String)) }
-
2
def extract_message_id
-
51
return nil unless respond_to?(:message)
-
-
51
mail_message = message
-
51
return nil unless mail_message.respond_to?(:message_id)
-
-
51
mail_message.message_id
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Integrations
-
2
module ActionMailer
-
# The error handlers a mailer can name in rescue_from (with: :log_and_ignore_error), for processes
-
# where LogStruct doesn't set up ActionMailer (development, the console and Rake tasks by default).
-
# Without them those mailers raised NameError there. Each logs the error with the mailer's logger,
-
# then ignores it, reports it to Rails.error, or raises it again, as its name says.
-
2
module FallbackErrorHandlers
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
requires_ancestor { ::ActionMailer::Base }
-
-
2
class << self
-
2
extend T::Sig
-
-
3
sig { params(logger: T.untyped, error: StandardError, report: T::Boolean, reraise: T::Boolean).void }
-
2
def handle(logger, error, report:, reraise:)
-
3
logger&.error("#{error.class}: #{error.message}")
-
3
::Rails.error.report(error, handled: true, source: "logstruct.action_mailer") if report
-
3
Kernel.raise error if reraise
-
end
-
end
-
-
# The handlers, for a mailer and (through ClassMethods) for the mailer class, which
-
# ActionMailer::MailDeliveryJob passes an error to. Both have a logger.
-
2
module Handlers
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
abstract!
-
-
3
sig { params(ex: StandardError).void }
-
2
def log_and_ignore_error(ex)
-
1
FallbackErrorHandlers.handle(handler_logger, ex, report: false, reraise: false)
-
end
-
-
3
sig { params(ex: StandardError).void }
-
2
def log_and_report_error(ex)
-
1
FallbackErrorHandlers.handle(handler_logger, ex, report: true, reraise: false)
-
end
-
-
3
sig { params(ex: StandardError).void }
-
2
def log_and_reraise_error(ex)
-
1
FallbackErrorHandlers.handle(handler_logger, ex, report: false, reraise: true)
-
end
-
-
2
private
-
-
3
sig { abstract.returns(T.untyped) }
-
2
def handler_logger
-
end
-
end
-
-
2
include Handlers
-
-
2
module ClassMethods
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
requires_ancestor { T.class_of(::ActionMailer::Base) }
-
-
2
include Handlers
-
-
2
private
-
-
3
sig { override.returns(T.untyped) }
-
2
def handler_logger = logger
-
end
-
-
2
private
-
-
3
sig { override.returns(T.untyped) }
-
2
def handler_logger = logger
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Integrations
-
2
module ActionMailer
-
# Handles collection of metadata for email logging
-
2
module MetadataCollection
-
2
extend T::Sig
-
# Add message-specific metadata to log data
-
3
sig { params(mailer: T.untyped, log_data: T::Hash[Symbol, T.untyped]).void }
-
2
def self.add_message_metadata(mailer, log_data)
-
93
message = mailer.respond_to?(:message) ? mailer.message : nil
-
-
# Add attachment count if message is available
-
93
log_data[:attachment_count] = if message
-
92
message.attachments&.count || 0
-
else
-
1
0
-
end
-
end
-
-
# Add context metadata to log data
-
3
sig { params(mailer: T.untyped, log_data: T::Hash[Symbol, T.untyped]).void }
-
2
def self.add_context_metadata(mailer, log_data)
-
# Add account ID information if available (but not user email)
-
91
extract_ids_to_log_data(mailer, log_data)
-
end
-
-
3
sig { params(mailer: T.untyped, log_data: T::Hash[Symbol, T.untyped]).void }
-
2
def self.extract_ids_to_log_data(mailer, log_data)
-
# Use configured ID mapping from LogStruct configuration
-
91
id_mapping = LogStruct.config.integrations.actionmailer_id_mapping
-
-
91
id_mapping.each do |ivar_name, log_key|
-
184
ivar = :"@#{ivar_name}"
-
184
next unless mailer.instance_variable_defined?(ivar)
-
-
4
obj = mailer.instance_variable_get(ivar)
-
4
log_data[log_key] = obj.id if obj.respond_to?(:id)
-
end
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
begin
-
2
require "active_job"
-
2
require "active_job/log_subscriber"
-
rescue LoadError
-
# ActiveJob gem is not available, integration will be skipped
-
end
-
-
2
require_relative "active_job/log_subscriber" if defined?(::ActiveJob::LogSubscriber)
-
-
2
module LogStruct
-
2
module Integrations
-
# ActiveJob integration for structured logging
-
2
module ActiveJob
-
2
extend T::Sig
-
2
extend IntegrationInterface
-
-
# Set up ActiveJob structured logging
-
4
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def self.setup(config)
-
5
return nil unless defined?(::ActiveJob::LogSubscriber)
-
5
return nil unless config.enabled
-
5
return nil unless config.integrations.enable_activejob
-
-
4
::ActiveSupport.on_load(:active_job) do
-
# Detach the default text formatter
-
4
RailsLogSubscribers.detach(::ActiveJob::LogSubscriber, :active_job)
-
-
# Attach our structured formatter
-
4
Integrations::ActiveJob::LogSubscriber.attach_to :active_job
-
end
-
4
true
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "active_support/log_subscriber"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../log/active_job"
-
2
require_relative "../../log/error"
-
2
require_relative "../event_time"
-
-
2
module LogStruct
-
2
module Integrations
-
2
module ActiveJob
-
# Structured logging for ActiveJob
-
2
class LogSubscriber < ::ActiveSupport::LogSubscriber
-
2
extend T::Sig
-
-
3
sig { params(event: ::ActiveSupport::Notifications::Event).void }
-
2
def enqueue(event)
-
5
log_enqueue(event)
-
end
-
-
3
sig { params(event: ::ActiveSupport::Notifications::Event).void }
-
2
def enqueue_at(event)
-
1
log_enqueue(event)
-
end
-
-
# perform_all_later: a line for each job, as perform_later writes
-
3
sig { params(event: ::ActiveSupport::Notifications::Event).void }
-
2
def enqueue_all(event)
-
9
ts = EventTime.coerce_event_time(event.time)
-
9
jobs = Array(event.payload[:jobs]).grep(::ActiveJob::Base)
-
9
error = event.payload[:exception_object]
-
9
if error
-
4
log_enqueue_all_error(error, jobs, ts)
-
4
return
-
end
-
-
# Adapters with their own enqueue_all (Sidekiq's) say only how many jobs they enqueued
-
14
if jobs.none? { |job| job.successfully_enqueued? || job.enqueue_error }
-
4
log_enqueued_count(jobs, event.payload[:enqueued_count], ts)
-
4
return
-
end
-
-
1
jobs.each do |job|
-
3
if job.successfully_enqueued?
-
2
log_enqueued(job, ts)
-
1
elsif job.enqueue_error
-
1
log_exception(job.enqueue_error, job)
-
end
-
end
-
end
-
-
3
sig { params(event: ::ActiveSupport::Notifications::Event).void }
-
2
def perform(event)
-
22
job = T.cast(event.payload[:job], ::ActiveJob::Base)
-
22
exception = event.payload[:exception_object]
-
-
22
if exception
-
# Log the exception with the job context
-
6
log_exception(exception, job)
-
# An aborted job didn't run: a before_perform callback stopped it
-
16
elsif !event.payload[:aborted]
-
15
finished_at = EventTime.coerce_event_time(event.end)
-
15
base_fields = build_base_fields(job)
-
15
logger.info(Log::ActiveJob::Finish.new(
-
**base_fields.to_kwargs,
-
duration_ms: event.duration.to_f,
-
finished_at: finished_at,
-
timestamp: finished_at
-
))
-
end
-
end
-
-
3
sig { params(event: ::ActiveSupport::Notifications::Event).void }
-
2
def perform_start(event)
-
14
job = T.cast(event.payload[:job], ::ActiveJob::Base)
-
14
ts = EventTime.coerce_event_time(event.time)
-
14
started_at = ts
-
14
attempt = job.executions
-
14
base_fields = build_base_fields(job)
-
14
logger.info(Log::ActiveJob::Start.new(
-
**base_fields.to_kwargs,
-
started_at: started_at,
-
attempt: attempt,
-
timestamp: ts
-
))
-
end
-
-
# retry_on caught the exception and enqueued the job again
-
3
sig { params(event: ::ActiveSupport::Notifications::Event).void }
-
2
def enqueue_retry(event)
-
1
job = T.cast(event.payload[:job], ::ActiveJob::Base)
-
# retry_job can be called without an exception or a wait
-
1
error = event.payload[:error]
-
1
wait = event.payload[:wait]
-
1
logger.info(Log::ActiveJob::Retry.new(
-
**build_base_fields(job).to_kwargs,
-
error_class: error&.class,
-
message: error&.message,
-
1
wait_ms: wait.nil? ? nil : wait.to_f * 1000,
-
timestamp: EventTime.coerce_event_time(event.time)
-
))
-
end
-
-
# retry_on ran out of attempts. The job then raises the exception (an error line), unless
-
# retry_on was given a block.
-
3
sig { params(event: ::ActiveSupport::Notifications::Event).void }
-
2
def retry_stopped(event)
-
1
job = T.cast(event.payload[:job], ::ActiveJob::Base)
-
1
error = T.cast(event.payload[:error], Exception)
-
1
logger.error(Log::ActiveJob::RetryStopped.new(
-
**build_base_fields(job).to_kwargs,
-
error_class: error.class,
-
message: error.message,
-
timestamp: EventTime.coerce_event_time(event.time)
-
))
-
end
-
-
3
sig { params(event: ::ActiveSupport::Notifications::Event).void }
-
2
def discard(event)
-
1
job = T.cast(event.payload[:job], ::ActiveJob::Base)
-
1
error = T.cast(event.payload[:error], Exception)
-
1
logger.error(Log::ActiveJob::Discard.new(
-
**build_base_fields(job).to_kwargs,
-
error_class: error.class,
-
message: error.message,
-
timestamp: EventTime.coerce_event_time(event.time)
-
))
-
end
-
-
2
private
-
-
# Like Rails' own enqueue lines: a job the queue didn't take writes an error line, and one a
-
# before_enqueue callback stopped writes none
-
3
sig { params(event: ::ActiveSupport::Notifications::Event).void }
-
2
def log_enqueue(event)
-
6
job = T.cast(event.payload[:job], ::ActiveJob::Base)
-
# perform_later rescues ActiveJob::EnqueueError and keeps it on the job
-
6
error = event.payload[:exception_object] || job.enqueue_error
-
6
if error
-
# With the inline adapter, a job that fails raises out of perform_later, and its perform line has
-
# already logged the error
-
3
log_exception(error, job) unless LogStruct::LoggedErrors.logged?(error)
-
3
elsif !event.payload[:aborted]
-
2
log_enqueued(job, EventTime.coerce_event_time(event.time))
-
end
-
end
-
-
3
sig { params(job: ::ActiveJob::Base, ts: Time).void }
-
2
def log_enqueued(job, ts)
-
12
base_fields = build_base_fields(job)
-
12
scheduled_at = job.scheduled_at
-
12
if scheduled_at
-
1
logger.info(Log::ActiveJob::Schedule.new(**base_fields.to_kwargs, scheduled_at: scheduled_at, timestamp: ts))
-
else
-
11
logger.info(Log::ActiveJob::Enqueue.new(**base_fields.to_kwargs, timestamp: ts))
-
end
-
end
-
-
# perform_all_later raised: the adapter did (Redis down, say), or with the inline adapter a job did
-
3
sig { params(error: Exception, jobs: T::Array[::ActiveJob::Base], ts: Time).void }
-
2
def log_enqueue_all_error(error, jobs, ts)
-
# Rails' own enqueue loop (for adapters without enqueue_all) marks each job it tries, keeps an
-
# ActiveJob::EnqueueError on its job and goes on, and stops at a job that raises anything else
-
15
tried = jobs.reject { |job| job.successfully_enqueued?.nil? }
-
4
tried.each do |job|
-
7
enqueue_error = job.enqueue_error
-
7
if job.successfully_enqueued?
-
4
log_enqueued(job, ts)
-
3
elsif enqueue_error
-
1
log_exception(enqueue_error, job)
-
end
-
end
-
# With the inline adapter, the error is a job's own, which its perform line has logged
-
4
return if LogStruct::LoggedErrors.logged?(error)
-
-
4
raised_by = tried.last
-
4
if raised_by && !raised_by.successfully_enqueued? && raised_by.enqueue_error.nil?
-
2
log_exception(error, raised_by)
-
2
return
-
end
-
-
# Which job it was isn't known: an adapter's own enqueue_all (Sidekiq's) doesn't say which jobs it
-
# enqueued before it raised
-
6
classes = jobs.map { |job| job.class.to_s }.uniq.join(", ")
-
2
message = "perform_all_later of #{jobs.size} jobs (#{classes}) failed"
-
2
message += "; the queue adapter doesn't say which it enqueued" if tried.empty?
-
2
logger.error(Log::Error.new(
-
source: Source::Job,
-
error_class: error.class,
-
message: message,
-
error_message: error.message,
-
backtrace: error.backtrace,
-
level: Level::Error,
-
timestamp: ts
-
))
-
2
LogStruct::LoggedErrors.mark(error)
-
end
-
-
# Every job's line when the adapter enqueued them all; when it didn't, it doesn't say which it enqueued
-
3
sig { params(jobs: T::Array[::ActiveJob::Base], enqueued_count: T.untyped, ts: Time).void }
-
2
def log_enqueued_count(jobs, enqueued_count, ts)
-
4
return if jobs.empty?
-
-
4
if enqueued_count == jobs.size
-
6
jobs.each { |job| log_enqueued(job, ts) }
-
else
-
6
classes = jobs.map { |job| job.class.to_s }.uniq.join(", ")
-
2
logger.warn("perform_all_later enqueued #{enqueued_count.to_i} of #{jobs.size} jobs (#{classes}); the queue adapter doesn't say which")
-
end
-
end
-
-
3
sig { params(job: ::ActiveJob::Base).returns(Log::ActiveJob::BaseFields) }
-
2
def build_base_fields(job)
-
56
Log::ActiveJob::BaseFields.new(
-
job_id: job.job_id,
-
job_class: job.class.to_s,
-
queue_name: job.queue_name&.to_sym,
-
executions: job.executions,
-
# Backends use Strings or Integers (SolidQueue uses its job row ID)
-
provider_job_id: job.provider_job_id&.to_s,
-
arguments: logged_arguments(job)
-
)
-
end
-
-
# ActionMailer's delivery job carries the mail's own arguments, which can be secrets: Devise passes its
-
# confirmation, password reset and invitation tokens as plain strings, which no key-based filter can find.
-
# For a mail, log the mailer, the mail and the delivery method only.
-
3
sig { params(job: ::ActiveJob::Base).returns(T.nilable(T::Array[T.untyped])) }
-
2
def logged_arguments(job)
-
56
return nil unless job.class.respond_to?(:log_arguments?) && job.class.log_arguments?
-
54
return job.arguments.first(3) if defined?(::ActionMailer::MailDeliveryJob) && job.is_a?(::ActionMailer::MailDeliveryJob)
-
-
29
job.arguments
-
end
-
-
# perform_now lets any Exception through, not only StandardErrors
-
3
sig { params(exception: Exception, job: ::ActiveJob::Base).void }
-
2
def log_exception(exception, job)
-
12
log_data = Log::ActiveJob::Error.new(
-
**build_base_fields(job).to_kwargs,
-
error_class: exception.class,
-
message: exception.message,
-
backtrace: exception.backtrace
-
)
-
-
12
logger.error(log_data)
-
# So a backend that reports the same exception (GoodJob's on_thread_error) doesn't log it again
-
12
LogStruct::LoggedErrors.mark(exception)
-
end
-
-
3
sig { returns(T.untyped) }
-
2
def logger
-
147
::ActiveJob::Base.logger
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "active_support/notifications"
-
-
2
module LogStruct
-
2
module Integrations
-
# ActiveModelSerializers integration. Subscribes to AMS notifications and
-
# emits structured logs with serializer/adapter/duration details.
-
2
module ActiveModelSerializers
-
2
extend T::Sig
-
-
4
sig { params(config: LogStruct::Configuration).returns(T.nilable(TrueClass)) }
-
2
def self.setup(config)
-
4
return nil unless defined?(::ActiveSupport::Notifications)
-
-
# Only activate if AMS appears to be present
-
4
return nil unless defined?(::ActiveModelSerializers)
-
-
# AMS logs each render as a line of text too
-
3
if defined?(::ActiveModelSerializers::Logging::LogSubscriber)
-
1
::ActiveModelSerializers::Logging::LogSubscriber.detach_from(:active_model_serializers)
-
end
-
-
# Subscribe to common AMS notification names; keep broad but specific
-
3
pattern = /\.active_model_serializers\z/
-
-
3
::ActiveSupport::Notifications.subscribe(pattern) do |_name, started, finished, _unique_id, payload|
-
# started/finished are Time; convert to ms
-
2
duration_ms = ((finished - started) * 1000.0).round(3)
-
-
2
serializer = payload[:serializer] || payload[:serializer_class]
-
2
adapter = payload[:adapter]
-
2
resource = payload[:resource] || payload[:object]
-
-
2
LogStruct.info(
-
LogStruct::Log::ActiveModelSerializers.new(
-
message: "ams.render",
-
serializer: serializer&.name,
-
adapter: adapter&.class&.name,
-
resource_class: resource&.class&.name,
-
duration_ms: duration_ms,
-
timestamp: started
-
)
-
)
-
rescue => e
-
# A problem with the log line is LogStruct's own, and never fails the render in production
-
1
LogStruct.handle_exception(e, source: LogStruct::Source::Internal, context: {integration: :active_model_serializers})
-
end
-
-
3
true
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "active_support/notifications"
-
-
2
module LogStruct
-
2
module Integrations
-
# ActiveRecord Integration for SQL Query Logging
-
#
-
# This integration captures and structures all SQL queries executed through ActiveRecord,
-
# providing detailed performance and debugging information in a structured format.
-
#
-
# ## Features:
-
# - Captures all SQL queries with execution time
-
# - Safely filters sensitive data from bind parameters
-
# - Extracts database operation metadata
-
# - Provides connection pool monitoring information
-
# - Identifies query types and table names
-
#
-
# ## Performance Considerations:
-
# - Minimal overhead on query execution
-
# - Async logging prevents I/O blocking
-
# - Configurable to disable in production if needed
-
# - Smart filtering reduces log volume for repetitive queries
-
#
-
# ## Security:
-
# - Bind parameters are filtered by their column names, like request params, and values that
-
# look like tokens or hashes are filtered too
-
# - The SQL is scrubbed like any string (emails, card numbers), but it isn't filtered: it can
-
# hold literal values (string conditions, or every value when prepared_statements is off)
-
#
-
# ## Configuration:
-
# ```ruby
-
# LogStruct.configure do |config|
-
# config.integrations.enable_sql_logging = true
-
# config.integrations.sql_slow_query_threshold = 100.0 # ms
-
# config.integrations.sql_log_bind_params = false # disable in production
-
# end
-
# ```
-
2
module ActiveRecord
-
2
extend T::Sig
-
2
extend IntegrationInterface
-
-
# Track subscription state keyed to the current Notifications.notifier instance
-
2
State = ::Struct.new(:subscribed, :notifier_id)
-
2
STATE = T.let(State.new(false, nil), State)
-
-
# Set up SQL query logging integration
-
3
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def self.setup(config)
-
21
return nil unless config.integrations.enable_sql_logging
-
20
return nil unless defined?(::ActiveRecord::Base)
-
-
# Detach Rails' default ActiveRecord log subscriber to prevent
-
# duplicate/unstructured SQL debug output when LogStruct SQL logging
-
# is enabled. We still receive notifications via ActiveSupport.
-
19
if defined?(::ActiveRecord::LogSubscriber)
-
begin
-
RailsLogSubscribers.detach(::ActiveRecord::LogSubscriber, :active_record)
-
rescue => e
-
LogStruct.handle_exception(e, source: LogStruct::Source::Internal)
-
end
-
end
-
-
# Disable verbose query logs ("↳ caller") since LogStruct provides
-
# structured context and these lines are noisy/unstructured.
-
# (A setting on the ActiveRecord module, so ActiveRecord::Base isn't loaded early.)
-
19
::ActiveRecord.verbose_query_logs = false
-
-
19
subscribe_to_sql_notifications
-
19
true
-
end
-
-
2
private_class_method
-
-
# Subscribe to ActiveRecord's sql.active_record notifications
-
3
sig { void }
-
2
def self.subscribe_to_sql_notifications
-
# Avoid duplicate subscriptions; re-subscribe if the notifier was reset
-
19
notifier = ::ActiveSupport::Notifications.notifier
-
19
current_id = notifier&.object_id
-
19
if STATE.subscribed && STATE.notifier_id == current_id
-
return
-
end
-
-
19
::ActiveSupport::Notifications.subscribe("sql.active_record") do |name, start, finish, id, payload|
-
9
handle_sql_event(name, start, finish, id, payload)
-
rescue => error
-
1
LogStruct.handle_exception(error, source: LogStruct::Source::Internal)
-
end
-
19
STATE.subscribed = true
-
19
STATE.notifier_id = current_id
-
end
-
-
# Process SQL notification event and create structured log
-
3
sig { params(name: String, start: T.untyped, finish: T.untyped, id: String, payload: T::Hash[Symbol, T.untyped]).void }
-
2
def self.handle_sql_event(name, start, finish, id, payload)
-
# Skip schema queries and Rails internal queries
-
38
return if skip_query?(payload)
-
-
31
duration_ms = ((finish - start) * 1000.0).round(2)
-
-
# Skip fast queries if threshold is configured
-
31
config = LogStruct.config
-
31
if config.integrations.sql_slow_query_threshold&.positive?
-
4
return if duration_ms < config.integrations.sql_slow_query_threshold
-
end
-
-
29
sql_log = Log::SQL.new(
-
message: format_sql_message(payload),
-
source: Source::App,
-
event: Event::Database,
-
sql: payload[:sql]&.strip || "",
-
name: payload[:name] || "SQL Query",
-
duration_ms: duration_ms,
-
row_count: extract_row_count(payload),
-
adapter: extract_adapter_name(payload),
-
bind_params: extract_and_filter_binds(payload),
-
database_name: extract_database_name(payload),
-
connection_pool_size: extract_pool_size(payload),
-
active_connections: extract_active_connections(payload),
-
operation_type: extract_operation_type(payload),
-
table_names: extract_table_names(payload)
-
)
-
-
28
LogStruct.info(sql_log)
-
end
-
-
# Determine if query should be skipped from logging
-
3
sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
-
2
def self.skip_query?(payload)
-
38
query_name = payload[:name]
-
38
sql = payload[:sql]
-
-
# Skip Rails schema queries
-
38
return true if query_name&.include?("SCHEMA")
-
37
return true if query_name&.include?("CACHE")
-
-
# Skip common Rails internal queries
-
36
return true if sql&.include?("schema_migrations")
-
35
return true if sql&.include?("ar_internal_metadata")
-
-
# Skip SHOW/DESCRIBE queries
-
34
return true if sql&.match?(/\A\s*(SHOW|DESCRIBE|EXPLAIN)\s/i)
-
-
31
false
-
end
-
-
# Format a readable message for the SQL log
-
3
sig { params(payload: T::Hash[Symbol, T.untyped]).returns(String) }
-
2
def self.format_sql_message(payload)
-
29
operation_name = payload[:name] || "SQL Query"
-
29
"#{operation_name} executed"
-
end
-
-
# Extract row count from payload
-
3
sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(Integer)) }
-
2
def self.extract_row_count(payload)
-
29
row_count = payload[:row_count]
-
29
row_count.is_a?(Integer) ? row_count : nil
-
end
-
-
# Extract database adapter name
-
3
sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
-
2
def self.extract_adapter_name(payload)
-
29
connection = payload[:connection]
-
29
return nil unless connection
-
-
27
adapter_name = connection.class.name
-
27
adapter_name&.split("::")&.last
-
end
-
-
# Extract and filter bind parameters
-
3
sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Array[T.untyped])) }
-
2
def self.extract_and_filter_binds(payload)
-
29
return nil unless LogStruct.config.integrations.sql_log_bind_params
-
-
27
binds = payload[:binds]
-
# The values as the database gets them, which Rails passes as a lambda since 6.0
-
27
casted_binds = payload[:type_casted_binds]
-
27
casted_binds = casted_binds.call if casted_binds.respond_to?(:call)
-
27
return nil unless binds.is_a?(Array) || casted_binds.is_a?(Array)
-
-
7
attributes = binds.is_a?(Array) ? binds : []
-
7
values = casted_binds.is_a?(Array) ? casted_binds : attributes
-
7
values.each_with_index.map do |value, index|
-
19
filter_bind(attributes[index], value)
-
end
-
end
-
-
# A bind's value, filtered by its column name like Rails' own SQL log lines
-
# (ActiveRecord::LogSubscriber#render_bind)
-
3
sig { params(attribute: T.untyped, value: T.untyped).returns(T.untyped) }
-
2
def self.filter_bind(attribute, value)
-
21
name = if attribute.is_a?(Array)
-
attribute.first&.name
-
21
elsif attribute.respond_to?(:name)
-
8
attribute.name
-
end
-
21
value = attribute.value_for_database if value.equal?(attribute) && attribute.respond_to?(:value_for_database)
-
21
if attribute.respond_to?(:type) && attribute.type.respond_to?(:binary?) && attribute.type.binary? && !value.nil?
-
2
return "<#{value.to_s.bytesize} bytes of binary data>"
-
end
-
19
return "[FILTERED]" if name && ParamFilters.should_filter_key?(name.to_s, value)
-
-
16
filter_bind_parameter(value)
-
end
-
-
# Extract database name from connection
-
3
sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
-
2
def self.extract_database_name(payload)
-
29
connection = payload[:connection]
-
29
return nil unless connection
-
-
27
if connection.respond_to?(:current_database)
-
27
connection.current_database
-
elsif connection.respond_to?(:database)
-
connection.database
-
end
-
rescue
-
nil
-
end
-
-
# Extract connection pool size
-
3
sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(Integer)) }
-
2
def self.extract_pool_size(payload)
-
29
connection = payload[:connection]
-
29
return nil unless connection
-
-
27
pool = connection.pool if connection.respond_to?(:pool)
-
27
pool&.size
-
rescue
-
nil
-
end
-
-
# Extract active connection count
-
3
sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(Integer)) }
-
2
def self.extract_active_connections(payload)
-
29
connection = payload[:connection]
-
29
return nil unless connection
-
-
27
pool = connection.pool if connection.respond_to?(:pool)
-
27
pool&.stat&.[](:busy)
-
rescue
-
nil
-
end
-
-
# Extract SQL operation type (SELECT, INSERT, etc.)
-
3
sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
-
2
def self.extract_operation_type(payload)
-
29
sql = payload[:sql]
-
29
return nil unless sql
-
-
# Extract first word of SQL query
-
29
match = sql.strip.match(/\A\s*(\w+)/i)
-
29
match&.captures&.first&.upcase
-
end
-
-
# Extract table names from SQL query
-
3
sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Array[String])) }
-
2
def self.extract_table_names(payload)
-
29
sql = payload[:sql]
-
29
return nil unless sql
-
-
# Simple regex to extract table names (basic implementation)
-
# This covers most common cases but could be enhanced
-
29
tables = []
-
-
# Match FROM, JOIN, UPDATE, INSERT INTO, DELETE FROM patterns
-
29
sql.scan(/(?:FROM|JOIN|UPDATE|INTO|DELETE\s+FROM)\s+["`]?(\w+)["`]?/i) do |match|
-
29
table_name = match[0]
-
29
tables << table_name unless tables.include?(table_name)
-
end
-
-
29
tables.empty? ? nil : tables
-
end
-
-
# Filter individual bind parameter values to remove sensitive data
-
3
sig { params(value: T.untyped).returns(T.untyped) }
-
2
def self.filter_bind_parameter(value)
-
26
case value
-
when String
-
# Filter strings that look like passwords, tokens, secrets, etc.
-
23
if looks_sensitive?(value)
-
8
"[FILTERED]"
-
else
-
15
value
-
end
-
else
-
3
value
-
end
-
end
-
-
# Whether a String value is shaped like a secret: a token, a digest or a Bearer header. Values are
-
# filtered by their column names first; this catches binds without one. Text that mentions a
-
# password or a key isn't a secret.
-
3
sig { params(value: String).returns(T::Boolean) }
-
2
def self.looks_sensitive?(value)
-
# A long value with no spaces, which might be a token
-
23
return true if value.length > 50 && !value.match?(/\s/)
-
-
# Filter strings that look like hashed passwords, API keys, tokens
-
21
return true if value.match?(/\A[a-f0-9]{32,}\z/i) # MD5, SHA, etc.
-
19
return true if value.match?(/\A[A-Za-z0-9+\/]{20,}={0,2}\z/) # Base64
-
-
# Filter JWT tokens (header.payload.signature format, starts with "ey")
-
18
return true if value.match?(/\Aey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\z/)
-
-
# Filter Bearer tokens
-
17
return true if value.match?(/\ABearer\s+/i)
-
-
15
false
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "../enums/source"
-
2
require_relative "../enums/event"
-
2
require_relative "../log/active_storage"
-
-
2
module LogStruct
-
2
module Integrations
-
# Integration for ActiveStorage structured logging
-
2
module ActiveStorage
-
2
extend T::Sig
-
2
extend IntegrationInterface
-
-
# Set up ActiveStorage structured logging
-
4
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def self.setup(config)
-
2
return nil unless defined?(::ActiveStorage)
-
1
return nil unless config.enabled
-
1
return nil unless config.integrations.enable_activestorage
-
-
1
detach_rails_log_subscriber
-
-
# Subscribe to all ActiveStorage service events
-
1
::ActiveSupport::Notifications.subscribe(/service_.*\.active_storage/) do |*args|
-
process_active_storage_event(::ActiveSupport::Notifications::Event.new(*args), config)
-
rescue => e
-
# A problem with a line mustn't fail the upload or download it describes
-
LogStruct.handle_exception(e, source: Source::Internal, context: {integration: :active_storage})
-
end
-
-
1
true
-
end
-
-
# LogStruct's lines replace Rails' own ActiveStorage lines. Rails loads its log subscriber with
-
# the storage service, after LogStruct is set up, so it's loaded here to detach it.
-
3
sig { void }
-
2
def self.detach_rails_log_subscriber
-
1
require "active_storage/log_subscriber"
-
1
RailsLogSubscribers.detach(::ActiveStorage::LogSubscriber, :active_storage)
-
rescue LoadError
-
nil
-
end
-
-
2
private_class_method
-
-
# Process ActiveStorage events and create structured logs
-
2
sig { params(event: ActiveSupport::Notifications::Event, config: LogStruct::Configuration).void }
-
2
def self.process_active_storage_event(event, config)
-
return unless config.enabled
-
return unless config.integrations.enable_activestorage
-
-
# Extract key information from the event
-
event_name = event.name.sub(/\.active_storage$/, "")
-
service_name = event.payload[:service]
-
duration_ms = event.duration
-
-
# Map service events to log event types
-
event_type = case event_name
-
when "service_upload"
-
Event::Upload
-
when "service_download"
-
Event::Download
-
when "service_delete"
-
Event::Delete
-
when "service_delete_prefixed"
-
Event::Delete
-
when "service_exist"
-
Event::Exist
-
when "service_url"
-
Event::Url
-
when "service_download_chunk", "service_streaming_download"
-
Event::Download
-
when "service_update_metadata"
-
Event::Metadata
-
else
-
# Events with no line of their own, such as service_mirror
-
return
-
end
-
-
# Create structured log event using generated classes
-
log_data = case event_type
-
when Event::Upload
-
Log::ActiveStorage::Upload.new(
-
storage: service_name.to_sym,
-
file_id: event.payload[:key]&.to_s,
-
checksum: event.payload[:checksum]&.to_s,
-
duration_ms: duration_ms
-
)
-
when Event::Download
-
Log::ActiveStorage::Download.new(
-
storage: service_name.to_sym,
-
file_id: event.payload[:key]&.to_s,
-
range: event.payload[:range],
-
duration_ms: duration_ms
-
)
-
when Event::Delete
-
# delete_prefixed has a prefix instead of a key
-
Log::ActiveStorage::Delete.new(
-
storage: service_name.to_sym,
-
file_id: event.payload[:key]&.to_s,
-
prefix: event.payload[:prefix]&.to_s
-
)
-
when Event::Metadata
-
Log::ActiveStorage::Metadata.new(
-
storage: service_name.to_sym,
-
file_id: event.payload[:key]&.to_s,
-
mime_type: event.payload[:content_type]&.to_s
-
)
-
when Event::Exist
-
Log::ActiveStorage::Exist.new(
-
storage: service_name.to_sym,
-
file_id: event.payload[:key]&.to_s,
-
exist: event.payload[:exist]
-
)
-
when Event::Url
-
# Without the URL: it's signed, and anyone who has it can read the file until it expires
-
Log::ActiveStorage::Url.new(
-
storage: service_name.to_sym,
-
file_id: event.payload[:key]&.to_s
-
)
-
end
-
-
# Rails logs existence checks and URLs at debug level: an app generates a URL for each file it shows
-
if event_type == Event::Exist || event_type == Event::Url
-
LogStruct.debug(log_data)
-
else
-
LogStruct.info(log_data)
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Integrations
-
# Ahoy analytics integration. If Ahoy is present, prepend a small hook to
-
# Ahoy::Tracker#track to emit a structured log for analytics events.
-
2
module Ahoy
-
2
extend T::Sig
-
-
4
sig { params(config: LogStruct::Configuration).returns(T.nilable(TrueClass)) }
-
2
def self.setup(config)
-
6
return nil unless defined?(::Ahoy)
-
-
4
if defined?(::Ahoy::Tracker)
-
4
mod = Module.new do
-
4
extend T::Sig
-
-
# Takes whatever Ahoy's track takes and passes it on as given, so Ahoy's defaults still apply
-
8
sig { params(name: T.untyped, args: T.untyped).returns(T.untyped) }
-
4
def track(name, *args)
-
4
result = super
-
begin
-
4
LogStruct.info(
-
LogStruct::Log::Ahoy.new(
-
message: "ahoy.track",
-
ahoy_event: name.to_s,
-
properties: Integrations::Ahoy.properties_for_log(args.first)
-
)
-
)
-
rescue => e
-
# A problem with the log line is LogStruct's, and never fails the app's tracking call in production
-
1
LogStruct.handle_exception(e, source: LogStruct::Source::Internal, context: {integration: :ahoy})
-
end
-
4
result
-
end
-
end
-
-
4
::Ahoy::Tracker.prepend(mod)
-
end
-
-
4
true
-
end
-
-
# Event properties can come from the browser, so they can be anything; a non-Hash is logged as its class
-
3
sig { params(properties: T.untyped).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
-
2
def self.properties_for_log(properties)
-
4
properties = properties.to_unsafe_h if properties.respond_to?(:to_unsafe_h)
-
4
case properties
-
2
when nil then nil
-
3
when Hash then properties.to_h { |key, value| [key.to_s.to_sym, value] }
-
1
else {_class: properties.class.name}
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
begin
-
2
require "carrierwave"
-
rescue LoadError
-
# CarrierWave gem is not available, integration will be skipped
-
end
-
-
2
module LogStruct
-
2
module Integrations
-
# CarrierWave integration for structured logging
-
2
module CarrierWave
-
2
extend T::Sig
-
2
extend IntegrationInterface
-
-
# Set up CarrierWave structured logging
-
3
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def self.setup(config)
-
8
return nil unless defined?(::CarrierWave)
-
8
return nil unless config.enabled
-
8
return nil unless config.integrations.enable_carrierwave
-
-
# Patch CarrierWave to add logging
-
8
::CarrierWave::Uploader::Base.prepend(LoggingMethods)
-
-
8
true
-
end
-
-
# Logs the operations that store, delete or fetch a file: store! (an upload, when there's a
-
# cached file to store), remove! (a delete) and download! (a remote URL fetched into the cache).
-
# retrieve_from_store! isn't logged: it only points the uploader at a stored file, and runs
-
# each time a record's mounted file is read.
-
2
module LoggingMethods
-
2
extend T::Sig
-
2
extend T::Helpers
-
2
requires_ancestor { ::CarrierWave::Uploader::Base }
-
-
3
sig { params(args: T.untyped).returns(T.untyped) }
-
2
def store!(*args)
-
# CarrierWave only stores a cached file; a record's save calls store! on every uploader
-
6
storing = !cache_only && (cached? || !args.first.nil?)
-
6
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
6
result = super
-
6
return result unless storing && !cached?
-
-
5
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0
-
5
log_file_operation do
-
# file_fields is a Hash Sorbet can't check against the struct's keyword arguments
-
5
Log::CarrierWave::Upload.new(
-
**T.unsafe(file_fields),
-
filename: file&.filename,
-
mime_type: file&.content_type,
-
size: file&.size,
-
duration_ms: duration_ms
-
)
-
end
-
5
result
-
end
-
-
3
sig { returns(T.untyped) }
-
2
def remove!
-
2
fields = file ? file_fields : nil
-
2
result = super
-
3
log_file_operation { Log::CarrierWave::Delete.new(**T.unsafe(fields)) } if fields
-
2
result
-
end
-
-
3
sig { params(uri: T.untyped, remote_headers: T.untyped).returns(T.untyped) }
-
2
def download!(uri, remote_headers = {})
-
1
result = super
-
1
log_file_operation do
-
1
Log::CarrierWave::Download.new(
-
**T.unsafe(file_fields),
-
filename: file&.filename,
-
mime_type: file&.content_type,
-
size: file&.size
-
)
-
end
-
1
result
-
end
-
-
2
private
-
-
# The fields every CarrierWave line has
-
3
sig { returns(T::Hash[Symbol, T.untyped]) }
-
2
def file_fields
-
{
-
7
storage: storage.class.name.to_s.split("::").last.to_s.downcase.to_sym,
-
file_id: identifier.to_s,
-
uploader: self.class.name,
-
model: model&.class&.name,
-
mount_point: mounted_as&.to_s,
-
version: version_name&.to_s,
-
store_path: store_path,
-
extension: file&.extension
-
}
-
end
-
-
# A problem with a log line mustn't stop the file operation that has already happened
-
3
sig { params(block: T.proc.returns(Log::Interfaces::CommonFields)).void }
-
2
def log_file_operation(&block)
-
7
LogStruct.info(yield)
-
rescue => e
-
1
LogStruct.handle_exception(e, source: Source::Internal, context: {integration: :carrierwave})
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# rubocop:disable Sorbet/ConstantsFromStrings
-
2
require_relative "../boot_buffer"
-
2
require "pathname"
-
-
begin
-
2
require "dotenv-rails"
-
rescue LoadError
-
# Dotenv-rails gem is not available, integration will be skipped
-
end
-
-
2
module LogStruct
-
2
module Integrations
-
# Dotenv integration: emits structured logs for load/update/save/restore events
-
2
module Dotenv
-
2
extend T::Sig
-
2
extend IntegrationInterface
-
2
@original_logger_setter = T.let(nil, T.nilable(UnboundMethod))
-
-
# Internal state holder to avoid duplicate subscriptions in a Sorbet-friendly way
-
2
State = ::Struct.new(:subscribed)
-
2
STATE = T.let(State.new(false), State)
-
-
4
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def self.setup(config)
-
# Subscribe regardless of dotenv gem presence so instrumentation via
-
# ActiveSupport::Notifications can be captured during tests and runtime.
-
2
subscribe!
-
2
true
-
end
-
-
2
class << self
-
2
extend T::Sig
-
-
4
sig { params(env: T.untyped).returns(String) }
-
2
def relative_env_path(env)
-
4
abs = env.filename
-
begin
-
1
if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
-
1
Pathname.new(abs).relative_path_from(Pathname.new(::Rails.root.to_s)).to_s
-
else
-
abs
-
end
-
rescue
-
abs
-
end
-
end
-
-
# A problem with a Dotenv log line is LogStruct's own: raised in development and test, logged in production
-
3
sig { params(error: StandardError).void }
-
2
def handle_log_error(error)
-
3
LogStruct.handle_exception(error, source: LogStruct::Source::Internal, context: {integration: :dotenv})
-
end
-
-
4
sig { void }
-
2
def subscribe!
-
# Guard against double subscription
-
5
return if STATE.subscribed
-
-
3
instrumenter = defined?(::ActiveSupport::Notifications) ? ::ActiveSupport::Notifications : nil
-
3
return unless instrumenter
-
-
3
instrumenter.subscribe("load.dotenv") do |*args|
-
# Allow tests to stub Log::Dotenv.new to force an error path
-
2
LogStruct::Log::Dotenv.new
-
2
event = ::ActiveSupport::Notifications::Event.new(*args)
-
2
env = event.payload[:env]
-
2
file = relative_env_path(env)
-
-
ts = event.time ? Time.at(event.time) : Time.now
-
LogStruct.info(Log::Dotenv::Load.new(file: file, timestamp: ts))
-
rescue => e
-
2
handle_log_error(e)
-
end
-
-
3
instrumenter.subscribe("update.dotenv") do |*args|
-
LogStruct::Log::Dotenv.new
-
event = ::ActiveSupport::Notifications::Event.new(*args)
-
diff = event.payload[:diff]
-
vars = diff.env.keys.map(&:to_s)
-
-
ts = event.time ? Time.at(event.time) : Time.now
-
LogStruct.debug(Log::Dotenv::Update.new(vars: vars, timestamp: ts))
-
rescue => e
-
handle_log_error(e)
-
end
-
-
3
instrumenter.subscribe("save.dotenv") do |*args|
-
1
LogStruct::Log::Dotenv.new
-
1
event = ::ActiveSupport::Notifications::Event.new(*args)
-
1
ts = event.time ? Time.at(event.time) : Time.now
-
1
LogStruct.info(Log::Dotenv::Save.new(snapshot: true, timestamp: ts))
-
rescue => e
-
handle_log_error(e)
-
end
-
-
3
instrumenter.subscribe("restore.dotenv") do |*args|
-
LogStruct::Log::Dotenv.new
-
event = ::ActiveSupport::Notifications::Event.new(*args)
-
diff = event.payload[:diff]
-
vars = diff.env.keys.map(&:to_s)
-
-
ts = event.time ? Time.at(event.time) : Time.now
-
LogStruct.info(Log::Dotenv::Restore.new(vars: vars, timestamp: ts))
-
rescue => e
-
handle_log_error(e)
-
end
-
-
3
STATE.subscribed = true
-
end
-
end
-
-
# Early boot subscription to buffer structured logs until logger is ready
-
2
@@boot_subscribed = T.let(false, T::Boolean)
-
4
sig { void }
-
2
def self.setup_boot
-
2
return if @@boot_subscribed
-
2
return unless defined?(::ActiveSupport::Notifications)
-
-
2
instrumenter = if Object.const_defined?(:Dotenv)
-
2
dm = Object.const_get(:Dotenv)
-
2
dm.respond_to?(:instrumenter) ? dm.instrumenter : ::ActiveSupport::Notifications
-
else
-
::ActiveSupport::Notifications
-
end
-
-
2
instrumenter.subscribe("load.dotenv") do |*args|
-
2
event = ::ActiveSupport::Notifications::Event.new(*args)
-
2
env = event.payload[:env]
-
2
file = relative_env_path(env)
-
1
ts = event.time ? Time.at(event.time) : Time.now
-
1
LogStruct::BootBuffer.add(Log::Dotenv::Load.new(file: file, timestamp: ts))
-
rescue => e
-
1
handle_log_error(e)
-
end
-
-
2
instrumenter.subscribe("update.dotenv") do |*args|
-
1
event = ::ActiveSupport::Notifications::Event.new(*args)
-
1
diff = event.payload[:diff]
-
1
vars = diff.env.keys.map(&:to_s)
-
1
ts = event.time ? Time.at(event.time) : Time.now
-
1
LogStruct::BootBuffer.add(Log::Dotenv::Update.new(vars: vars, timestamp: ts))
-
rescue => e
-
handle_log_error(e)
-
end
-
-
2
instrumenter.subscribe("save.dotenv") do |*args|
-
1
event = ::ActiveSupport::Notifications::Event.new(*args)
-
1
ts = event.time ? Time.at(event.time) : Time.now
-
1
LogStruct::BootBuffer.add(Log::Dotenv::Save.new(snapshot: true, timestamp: ts))
-
rescue => e
-
handle_log_error(e)
-
end
-
-
2
instrumenter.subscribe("restore.dotenv") do |*args|
-
event = ::ActiveSupport::Notifications::Event.new(*args)
-
diff = event.payload[:diff]
-
vars = diff.env.keys.map(&:to_s)
-
ts = event.time ? Time.at(event.time) : Time.now
-
LogStruct::BootBuffer.add(Log::Dotenv::Restore.new(vars: vars, timestamp: ts))
-
rescue => e
-
handle_log_error(e)
-
end
-
-
2
@@boot_subscribed = true
-
end
-
-
# Intercept Dotenv::Rails#logger= to defer replay until we resolve policy
-
4
sig { void }
-
2
def self.intercept_logger_setter!
-
2
return unless Object.const_defined?(:Dotenv)
-
# Do not intercept when LogStruct is disabled; allow original dotenv replay
-
2
return unless LogStruct.enabled?
-
2
dotenv_mod = Object.const_get(:Dotenv)
-
2
return unless dotenv_mod.const_defined?(:Rails)
-
2
klass = dotenv_mod.const_get(:Rails)
-
2
return if klass.instance_variable_defined?(:@_logstruct_replay_patched)
-
-
2
original = klass.instance_method(:logger=)
-
2
@original_logger_setter = original
-
-
2
mod = Module.new do
-
2
define_method :logger= do |new_logger|
-
# Defer replay: store desired logger, keep ReplayLogger as current
-
2
instance_variable_set(:@logstruct_pending_dotenv_logger, new_logger)
-
2
new_logger
-
end
-
-
2
define_method :logstruct_pending_dotenv_logger do
-
2
instance_variable_get(:@logstruct_pending_dotenv_logger)
-
end
-
end
-
-
2
klass.prepend(mod)
-
2
klass.instance_variable_set(:@_logstruct_replay_patched, true)
-
end
-
-
# Decide which boot logs to emit after user initializers
-
4
sig { void }
-
2
def self.resolve_boot_logs!
-
# If LogStruct is disabled, do not alter dotenv behavior at all
-
2
return unless LogStruct.enabled?
-
2
dotenv_mod = Object.const_defined?(:Dotenv) ? Object.const_get(:Dotenv) : nil
-
2
klass = dotenv_mod&.const_defined?(:Rails) ? dotenv_mod.const_get(:Rails) : nil
-
-
2
pending_logger = nil
-
2
railtie_instance = nil
-
2
if klass&.respond_to?(:instance)
-
2
railtie_instance = klass.instance
-
2
if railtie_instance.respond_to?(:logstruct_pending_dotenv_logger)
-
2
pending_logger = railtie_instance.logstruct_pending_dotenv_logger
-
end
-
end
-
-
2
if LogStruct.enabled? && LogStruct.config.integrations.enable_dotenv
-
# Structured path
-
2
if pending_logger && railtie_instance
-
# Clear any buffered original logs
-
2
current_logger = railtie_instance.logger if railtie_instance.respond_to?(:logger)
-
2
if current_logger && current_logger.class.name.end_with?("ReplayLogger")
-
begin
-
2
logs = current_logger.instance_variable_get(:@logs)
-
2
logs.clear if logs.respond_to?(:clear)
-
rescue
-
# best effort
-
end
-
end
-
2
railtie_instance.config.dotenv.logger = pending_logger
-
end
-
-
# Detach original subscriber and subscribe runtime structured
-
2
if dotenv_mod&.const_defined?(:LogSubscriber)
-
2
dotenv_mod.const_get(:LogSubscriber).detach_from(:dotenv)
-
end
-
2
LogStruct::Integrations::Dotenv.subscribe!
-
-
2
require_relative "../boot_buffer"
-
2
LogStruct::BootBuffer.flush
-
else
-
# Original path: replay dotenv lines, drop structured buffer
-
if railtie_instance && @original_logger_setter
-
setter = @original_logger_setter
-
new_logger = pending_logger
-
if new_logger.nil? && ENV["RAILS_LOG_TO_STDOUT"].to_s.strip != ""
-
require "logger"
-
require "active_support/tagged_logging"
-
new_logger = ActiveSupport::TaggedLogging.new(::Logger.new($stdout)).tagged("dotenv")
-
end
-
setter.bind_call(railtie_instance, new_logger) if new_logger
-
end
-
require_relative "../boot_buffer"
-
LogStruct::BootBuffer.clear
-
end
-
end
-
end
-
end
-
end
-
-
# Subscribe immediately to capture earliest dotenv events into BootBuffer
-
2
LogStruct::Integrations::Dotenv.setup_boot
-
-
# rubocop:enable Sorbet/ConstantsFromStrings
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Integrations
-
2
module EventTime
-
2
extend T::Sig
-
-
3
sig { params(value: T.untyped).returns(Time) }
-
2
def self.coerce_event_time(value)
-
43
return value if value.is_a?(Time)
-
43
return Time.now unless value.is_a?(Numeric)
-
-
43
monotonic_now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
-
43
Time.now - (monotonic_now - value)
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
begin
-
2
require "good_job"
-
rescue LoadError
-
# GoodJob gem is not available, integration will be skipped
-
end
-
-
2
require_relative "good_job/logger" if defined?(::GoodJob)
-
-
2
module LogStruct
-
2
module Integrations
-
# GoodJob integration for structured logging
-
#
-
# GoodJob is a PostgreSQL-based ActiveJob backend. Its jobs are logged by the ActiveJob
-
# integration (enqueued, started, finished, failed), so this integration covers GoodJob itself:
-
#
-
# - Replaces GoodJob.logger with a LogStruct logger, so GoodJob's own messages are JSON lines
-
# - Logs each thread error as a structured error line, then calls the app's `on_thread_error`
-
# - Stops GoodJob's own log subscriber logging those errors again with the whole backtrace
-
#
-
# ## Configuration:
-
# The integration is automatically enabled when GoodJob is detected and
-
# LogStruct configuration allows it. It can be disabled by setting:
-
#
-
# ```ruby
-
# config.integrations.enable_goodjob = false
-
# ```
-
2
module GoodJob
-
2
extend T::Sig
-
2
extend IntegrationInterface
-
-
# Set up GoodJob structured logging
-
#
-
# This method replaces GoodJob's logger and logs its thread errors as structured lines.
-
#
-
# @param config [LogStruct::Configuration] The LogStruct configuration
-
# @return [Boolean, nil] Returns true if setup was successful, nil if skipped
-
4
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def self.setup(config)
-
12
return nil unless defined?(::GoodJob)
-
6
return nil unless config.enabled
-
6
return nil unless config.integrations.enable_goodjob
-
-
# Replace GoodJob's logger with our structured logger
-
6
configure_logger
-
-
6
compact_error_lines
-
-
6
true
-
end
-
-
# GoodJob::LogSubscriber logs errors as text with the whole backtrace in the message (over 10KB).
-
# GoodJob passes every error its job tasks raise, every systemd watchdog error and every notifier
-
# error except connection errors to `on_thread_error`, which LogThreadErrors logs as a structured
-
# line, so GoodJob's own lines for those are off. The notifier passes connection errors on only
-
# once they repeat, so its line for them is kept, without the backtrace.
-
2
module CompactErrorLines
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
requires_ancestor { ::GoodJob::LogSubscriber }
-
-
3
sig { params(_event: T.untyped).void }
-
2
def finished_job_task(_event)
-
end
-
-
3
sig { params(event: T.untyped).void }
-
2
def notifier_notify_error(event)
-
2
exception = event.payload[:error]
-
2
return unless exception
-
2
return unless LogStruct::Integrations::GoodJob.notifier_connection_error?(exception)
-
-
2
error { "Notifier errored: #{exception.class}: #{exception}" }
-
end
-
-
3
sig { params(_event: T.untyped).void }
-
2
def systemd_watchdog_error(_event)
-
end
-
end
-
-
# Logs each thread error as a structured line, then calls the handler the app set as
-
# `on_thread_error` (GoodJob's README sets one to report errors to an error tracker). Every
-
# GoodJob component passes its thread errors to `GoodJob._on_thread_error`, so this works
-
# whenever the app sets its handler: GoodJob's engine applies config.good_job.on_thread_error
-
# after LogStruct's setup. GoodJob passes any Exception its handled_exceptions setting lets
-
# through, not only StandardErrors.
-
2
module LogThreadErrors
-
2
extend T::Sig
-
-
3
sig { params(exception: Exception).void }
-
2
def _on_thread_error(exception)
-
6
LogStruct::Integrations::GoodJob.log_thread_error(exception)
-
rescue => e
-
1
LogStruct.handle_exception(e, source: LogStruct::Source::Internal, context: {integration: :good_job})
-
ensure
-
# The app's handler runs even if LogStruct's line failed
-
6
super
-
end
-
end
-
-
# Configure GoodJob to use LogStruct's structured logger
-
3
sig { void }
-
2
def self.configure_logger
-
10
return unless defined?(::GoodJob)
-
-
# Not GoodJob, which here is this module
-
10
goodjob_module = ::GoodJob
-
-
# Replace GoodJob.logger with our structured logger if GoodJob is available
-
10
if goodjob_module.respond_to?(:logger=)
-
10
goodjob_module.logger = LogStruct::Integrations::GoodJob::Logger.new("GoodJob")
-
-
# GoodJob's executable adds a plain text STDOUT logger unless GoodJob.logger already writes
-
# to STDOUT, which it can't tell for this logger
-
10
if defined?(::GoodJob::CLI) && ::GoodJob::CLI.respond_to?(:log_to_stdout=)
-
10
::GoodJob::CLI.log_to_stdout = false
-
end
-
end
-
-
10
configure_thread_error_handler
-
end
-
-
3
sig { void }
-
2
def self.configure_thread_error_handler
-
10
return unless defined?(::GoodJob)
-
-
# Not GoodJob, which here is this module
-
10
goodjob_singleton = ::GoodJob.singleton_class
-
10
return unless goodjob_singleton.method_defined?(:_on_thread_error)
-
-
10
goodjob_singleton.prepend(LogThreadErrors) unless goodjob_singleton.ancestors.include?(LogThreadErrors)
-
end
-
-
# A failed job's exception was already logged by the ActiveJob integration, with the job's
-
# ID and class, so it isn't logged again here
-
3
sig { params(exception: Exception).void }
-
2
def self.log_thread_error(exception)
-
5
return if LogStruct::LoggedErrors.logged?(exception)
-
-
4
::GoodJob.logger.error(
-
LogStruct::Log::GoodJob::Error.new(
-
error_class: exception.class.name || exception.class.to_s,
-
error_message: exception.message,
-
backtrace: exception.backtrace,
-
level: LogStruct::Level::Error,
-
process_id: ::Process.pid,
-
thread_id: Thread.current.object_id.to_s(36)
-
)
-
)
-
end
-
-
# The errors GoodJob's notifier holds back until they repeat (GoodJob::Notifier::CONNECTION_ERRORS)
-
3
sig { params(exception: Exception).returns(T::Boolean) }
-
2
def self.notifier_connection_error?(exception)
-
2
return true unless defined?(::GoodJob::Notifier::CONNECTION_ERRORS)
-
-
2
::GoodJob::Notifier::CONNECTION_ERRORS.any? do |error_name|
-
2
error_class = error_name.safe_constantize
-
2
error_class.is_a?(Class) && exception.is_a?(error_class)
-
end
-
end
-
-
# Only when LogThreadErrors logs thread errors instead
-
3
sig { void }
-
2
def self.compact_error_lines
-
6
return unless defined?(::GoodJob::LogSubscriber)
-
2
return unless ::GoodJob.singleton_class.ancestors.include?(LogThreadErrors)
-
-
2
log_subscriber = ::GoodJob::LogSubscriber
-
2
log_subscriber.prepend(CompactErrorLines) unless log_subscriber.ancestors.include?(CompactErrorLines)
-
end
-
-
2
private_class_method :configure_logger
-
2
private_class_method :compact_error_lines
-
2
private_class_method :configure_thread_error_handler
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
1
require_relative "../../semantic_logger/logger"
-
1
require_relative "../../log/good_job"
-
1
require_relative "../../enums/source"
-
-
1
module LogStruct
-
1
module Integrations
-
1
module GoodJob
-
# Custom Logger for GoodJob that creates LogStruct::Log::GoodJob entries
-
#
-
# GoodJob's own messages are logged as Log::GoodJob::Log lines with the process and thread,
-
# and, during a job, the job's ID, class, queue, execution count, scheduled time and priority.
-
#
-
# ## Usage:
-
# This logger is automatically configured when the GoodJob integration
-
# is enabled. It replaces GoodJob.logger to provide structured logging
-
# for all GoodJob operations.
-
1
class Logger < LogStruct::SemanticLogger::Logger
-
1
extend T::Sig
-
-
# Override log methods to create GoodJob-specific log structs
-
1
%i[debug info warn error fatal].each do |level|
-
5
define_method(level) do |message = nil, payload = nil, exception = nil, &block|
-
# LogStruct log entries and Hashes (like the thread error handler's) are logged as they are, and a
-
# message with an exception is an error line with both
-
18
if !(message.nil? || message.is_a?(String)) || exception || payload.is_a?(Exception)
-
5
next super(message, payload, exception, &block)
-
end
-
-
13
job_context = Logger.current_job_context
-
-
13
log_struct = Log::GoodJob::Log.new(
-
1
message: message || (block ? block.call : ""),
-
process_id: ::Process.pid,
-
thread_id: Thread.current.object_id.to_s(36),
-
job_id: job_context[:job_id],
-
job_class: job_context[:job_class],
-
queue_name: job_context[:queue_name],
-
executions: job_context[:executions],
-
scheduled_at: job_context[:scheduled_at],
-
priority: job_context[:priority]
-
)
-
-
13
super(log_struct, payload, &nil)
-
end
-
end
-
-
# The job GoodJob is running on this thread: CurrentThread.job (a GoodJob::Job) in GoodJob 4,
-
# CurrentThread.execution in GoodJob 3
-
2
sig { returns(T::Hash[Symbol, T.untyped]) }
-
1
def self.current_job_context
-
13
return {} unless defined?(::GoodJob::CurrentThread)
-
-
3
current = ::GoodJob::CurrentThread
-
3
job = (current.job if current.respond_to?(:job)) || (current.execution if current.respond_to?(:execution))
-
3
return {} unless job
-
-
2
job_class = job.try(:job_class) || job.try(:serialized_params)&.dig("job_class")
-
{
-
2
job_id: job.try(:active_job_id)&.to_s,
-
job_class: job_class&.to_s,
-
queue_name: job.try(:queue_name)&.to_sym,
-
executions: job.try(:executions_count) || job.try(:executions),
-
scheduled_at: job.try(:scheduled_at),
-
priority: job.try(:priority)
-
}
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "action_dispatch/middleware/host_authorization"
-
2
require_relative "../enums/event"
-
2
require_relative "../log/security/blocked_host"
-
-
2
module LogStruct
-
2
module Integrations
-
# Host Authorization integration for structured logging of blocked hosts
-
2
module HostAuthorization
-
2
extend T::Sig
-
2
extend IntegrationInterface
-
-
2
RESPONSE_HTML = T.let(
-
"<html><head><title>Blocked Host</title></head><body>" \
-
"<h1>Blocked Host</h1>" \
-
"<p>This host is not permitted to access this application.</p>" \
-
"<p>If you are the administrator, check your configuration.</p>" \
-
"</body></html>",
-
String
-
)
-
2
RESPONSE_HEADERS = T.let(
-
{
-
"Content-Type" => "text/html",
-
"Content-Length" => RESPONSE_HTML.bytesize.to_s
-
}.freeze,
-
T::Hash[String, String]
-
)
-
2
FORBIDDEN_STATUS = T.let(403, Integer)
-
-
# Set up host authorization logging
-
4
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def self.setup(config)
-
7
return nil unless config.enabled
-
7
return nil unless config.integrations.enable_host_authorization
-
-
# Merge our response_app into existing host_authorization config to preserve excludes
-
7
existing = Rails.application.config.host_authorization
-
7
unless existing.is_a?(Hash)
-
existing = {}
-
end
-
7
existing = existing.dup
-
7
app_response_app = existing[:response_app]
-
# Set up once: don't wrap our own response app again
-
7
app_response_app = app_response_app.app_response_app if app_response_app.is_a?(ResponseApp)
-
7
existing[:response_app] = ResponseApp.new(app_response_app)
-
7
Rails.application.config.host_authorization = existing
-
-
7
true
-
end
-
-
# Logs the blocked host, then responds with the app's own response_app if it set one
-
2
class ResponseApp
-
2
extend T::Sig
-
-
3
sig { returns(T.untyped) }
-
2
attr_reader :app_response_app
-
-
4
sig { params(app_response_app: T.untyped).void }
-
2
def initialize(app_response_app)
-
7
@app_response_app = app_response_app
-
end
-
-
3
sig { params(env: T::Hash[String, T.untyped]).returns(T.untyped) }
-
2
def call(env)
-
begin
-
4
log_blocked_host(env)
-
rescue => e
-
1
LogStruct.handle_exception(e, source: LogStruct::Source::Internal, context: {integration: :host_authorization})
-
end
-
4
return @app_response_app.call(env) if @app_response_app
-
-
# Dup the headers so they can be modified by downstream middleware
-
3
[FORBIDDEN_STATUS, RESPONSE_HEADERS.dup, [RESPONSE_HTML]]
-
end
-
-
2
private
-
-
3
sig { params(env: T::Hash[String, T.untyped]).void }
-
2
def log_blocked_host(env)
-
4
request = ::ActionDispatch::Request.new(env)
-
# The hosts Rails blocked: the Host header, and X-Forwarded-Host when it's set
-
4
blocked_hosts = Array(env["action_dispatch.blocked_hosts"]).map(&:to_s)
-
-
4
log_entry = LogStruct::Log::Security::BlockedHost.new(
-
message: "Blocked host detected: #{request.host}",
-
blocked_host: request.host,
-
4
blocked_hosts: blocked_hosts.empty? ? nil : blocked_hosts,
-
path: request.path,
-
http_method: request.method,
-
source_ip: request.ip,
-
user_agent: request.user_agent,
-
referer: StringScrubber.without_query(request.referer),
-
x_forwarded_for: request.x_forwarded_for
-
)
-
-
4
LogStruct.warn(log_entry)
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Integrations
-
# Interface that all integrations must implement
-
# This ensures consistent behavior across all integration modules
-
2
module IntegrationInterface
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
# This is an interface that should be implemented by all integration modules
-
2
interface!
-
-
# All integrations must implement this method to set up their functionality
-
# @return [Boolean, nil] Returns true if setup was successful, nil if skipped
-
4
sig { abstract.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def setup(config); end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
begin
-
2
require "lograge"
-
rescue LoadError
-
# Lograge gem is not available, integration will be skipped
-
end
-
-
2
module LogStruct
-
2
module Integrations
-
# Lograge integration for structured request logging
-
2
module Lograge
-
2
extend IntegrationInterface
-
-
2
LOGRAGE_KNOWN_KEYS = T.let(
-
[
-
:method,
-
:path,
-
:format,
-
:controller,
-
:action,
-
:status,
-
:duration,
-
:view,
-
:db,
-
:params,
-
:request_id,
-
:source_ip,
-
:user_agent,
-
:referer,
-
:host,
-
:content_type,
-
:accept
-
].freeze,
-
T::Array[Symbol]
-
)
-
-
# With config.exceptions_app = routes, Rails renders an error page by sending the request through the app again
-
# as GET /<status>, and keeps the request's own method and path in these headers
-
2
ORIGINAL_METHOD_HEADER = "action_dispatch.original_request_method"
-
2
ORIGINAL_PATH_HEADER = "action_dispatch.original_path"
-
# Set when the request's action logged a line with its error, so the error page for it logs none
-
2
ACTION_ERROR_LOGGED_HEADER = "logstruct.action_error_logged"
-
-
# A Proc, not a Method: sorbet-runtime replaces a method with sig checks on its first call, so a Method taken
-
# before then isn't equal to one taken after, and setup couldn't tell it had added the test already
-
2
IGNORE_REPEATED_ERROR_PAGE = T.let(
-
->(event) { Lograge.repeated_error_page?(event) },
-
T.proc.params(event: ActiveSupport::Notifications::Event).returns(T::Boolean)
-
)
-
-
2
class << self
-
2
extend T::Sig
-
-
# Set up lograge for structured request logging
-
4
sig { override.params(logstruct_config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def setup(logstruct_config)
-
3
return nil unless defined?(::Lograge)
-
3
return nil unless logstruct_config.enabled
-
3
return nil unless logstruct_config.integrations.enable_lograge
-
-
3
configure_lograge(logstruct_config)
-
-
3
true
-
end
-
-
2
private_class_method
-
-
4
sig { params(logstruct_config: LogStruct::Configuration).void }
-
2
def configure_lograge(logstruct_config)
-
3
::Rails.application.configure do
-
3
config.lograge.enabled = true
-
# Use a raw formatter that just returns the log struct.
-
# The struct is converted to JSON by our Formatter (after filtering, etc.)
-
3
config.lograge.formatter = T.let(
-
lambda do |data|
-
4
LogStruct::Integrations::Lograge.build_request_log(data)
-
end,
-
T.proc.params(hash: T::Hash[T.any(Symbol, String), T.untyped]).returns(Log::Request)
-
)
-
-
# Add custom options to lograge
-
3
config.lograge.custom_options = lambda do |event|
-
Integrations::Lograge.lograge_default_options(event)
-
end
-
-
# Lograge detaches Rails' own request lines from ActiveSupport::Notifications. From Rails 8.2 they come
-
# from Rails.event, so LogStruct detaches them there.
-
3
config.after_initialize do |app|
-
3
Integrations::Lograge.unsubscribe_rails_request_lines unless app.config.lograge.keep_original_rails_log
-
end
-
end
-
-
# Lograge.setup adds the app's own ignore tests to the same list
-
3
::Lograge.ignore(IGNORE_REPEATED_ERROR_PAGE) unless ::Lograge.ignore_tests.include?(IGNORE_REPEATED_ERROR_PAGE)
-
end
-
-
# Rails' "Processing by", "Completed 200 OK" and "Rendered" lines, which the request line replaces
-
4
sig { void }
-
2
def unsubscribe_rails_request_lines
-
# Earlier Rails versions' subscribers are lograge's to detach
-
3
return unless defined?(::ActiveSupport::EventReporter::LogSubscriber)
-
-
1
RailsLogSubscribers.unsubscribe_from_events(::ActionController::LogSubscriber) if defined?(::ActionController::LogSubscriber)
-
1
RailsLogSubscribers.unsubscribe_from_events(::ActionView::LogSubscriber) if defined?(::ActionView::LogSubscriber)
-
end
-
-
3
sig { params(event: ActiveSupport::Notifications::Event).returns(T::Hash[Symbol, T.untyped]) }
-
2
def lograge_default_options(event)
-
# Extract essential fields from the payload
-
12
options = event.payload.slice(
-
:request_id,
-
:host,
-
:source_ip
-
).compact
-
12
add_request_address(event.payload[:request], options)
-
12
add_error_page_request(event, options)
-
-
12
if event.payload[:params].present?
-
options[:params] = event.payload[:params].except("controller", "action")
-
end
-
-
# Process headers if available
-
12
process_headers(event, options)
-
-
# Apply custom options from application if provided
-
12
apply_custom_options(event, options)
-
-
12
options
-
end
-
-
# Rails' payload has the request but not its address, which Rails' own "Started GET" line logs.
-
# A payload's own :source_ip and :host (from append_info_to_payload) take precedence.
-
3
sig { params(request: T.untyped, options: T::Hash[Symbol, T.untyped]).void }
-
2
def add_request_address(request, options)
-
12
return unless request.is_a?(::ActionDispatch::Request)
-
-
6
options[:host] ||= request.host
-
6
options[:source_ip] ||= begin
-
5
request.remote_ip
-
rescue ::ActionDispatch::RemoteIp::IpSpoofAttackError
-
# The forwarding headers disagree, so the address the connection came from
-
1
request.get_header("REMOTE_ADDR")
-
end
-
end
-
-
# An error page's line has the method and path of the request it's for, not GET /500
-
3
sig { params(event: ActiveSupport::Notifications::Event, options: T::Hash[Symbol, T.untyped]).void }
-
2
def add_error_page_request(event, options)
-
12
request = event.payload[:request]
-
12
return unless request.is_a?(::ActionDispatch::Request)
-
-
6
if request.has_header?(ORIGINAL_PATH_HEADER)
-
1
options[:method] = request.get_header(ORIGINAL_METHOD_HEADER)
-
1
options[:path] = "#{request.script_name}#{request.get_header(ORIGINAL_PATH_HEADER)}"
-
5
elsif event.payload[:exception]
-
1
request.set_header(ACTION_ERROR_LOGGED_HEADER, true)
-
end
-
end
-
-
# The error page for an error its action's line logged would log the request a second time. A request whose
-
# error came before any action (a routing error) or after it (in a middleware) keeps its error page's line.
-
3
sig { params(event: ActiveSupport::Notifications::Event).returns(T::Boolean) }
-
2
def repeated_error_page?(event)
-
4
request = event.payload[:request]
-
4
request.is_a?(::ActionDispatch::Request) && request.has_header?(ORIGINAL_PATH_HEADER) &&
-
request.get_header(ACTION_ERROR_LOGGED_HEADER) == true
-
end
-
-
# Process headers from the event payload
-
3
sig { params(event: ActiveSupport::Notifications::Event, options: T::Hash[Symbol, T.untyped]).void }
-
2
def process_headers(event, options)
-
12
headers = event.payload[:headers]
-
12
return if headers.blank?
-
-
# Rails' ActionDispatch::RequestId middleware stores request_id in headers
-
# Only set if not already present in payload (payload takes precedence)
-
3
if options[:request_id].blank? && headers["action_dispatch.request_id"].present?
-
1
options[:request_id] = headers["action_dispatch.request_id"]
-
end
-
-
3
options[:user_agent] = headers["HTTP_USER_AGENT"]
-
3
options[:referer] = headers["HTTP_REFERER"]
-
3
options[:content_type] = headers["CONTENT_TYPE"]
-
3
options[:accept] = headers["HTTP_ACCEPT"]
-
end
-
-
# Apply custom options from the application's configuration
-
3
sig { params(event: ActiveSupport::Notifications::Event, options: T::Hash[Symbol, T.untyped]).void }
-
2
def apply_custom_options(event, options)
-
12
custom_options_proc = LogStruct.config.integrations.lograge_custom_options
-
12
return unless custom_options_proc&.respond_to?(:call)
-
-
# The proc can change options, or return a Hash of fields to add, as Lograge's custom_options do
-
3
result = custom_options_proc.call(event, options)
-
3
return if !result.is_a?(Hash) || result.equal?(options)
-
-
6
options.merge!(result.to_h { |key, value| [key.to_s.to_sym, value] })
-
end
-
-
3
sig { params(data: T::Hash[T.any(Symbol, String), T.untyped]).returns(Log::Request) }
-
2
def build_request_log(data)
-
6
normalized_data = normalize_lograge_data(data)
-
-
# Coerce common fields to expected types
-
6
status = ((s = normalized_data[:status]) && s.respond_to?(:to_i)) ? s.to_i : s
-
6
duration_ms = ((d = normalized_data[:duration]) && d.respond_to?(:to_f)) ? d.to_f : d
-
6
view = ((v = normalized_data[:view]) && v.respond_to?(:to_f)) ? v.to_f : v
-
6
db = ((b = normalized_data[:db]) && b.respond_to?(:to_f)) ? b.to_f : b
-
-
6
params = normalized_data[:params]
-
6
params = params.deep_symbolize_keys if params&.respond_to?(:deep_symbolize_keys)
-
-
6
additional_data = extract_additional_data(normalized_data)
-
-
6
Log::Request.new(
-
http_method: normalized_data[:method]&.to_s,
-
path: normalized_data[:path]&.to_s,
-
format: normalized_data[:format]&.to_sym,
-
controller: normalized_data[:controller]&.to_s,
-
action: normalized_data[:action]&.to_s,
-
status: status,
-
duration_ms: duration_ms,
-
view: view,
-
database: db,
-
params: params,
-
source_ip: normalized_data[:source_ip]&.to_s,
-
user_agent: normalized_data[:user_agent]&.to_s,
-
# Without its query, which can carry reset tokens and OAuth codes
-
referer: StringScrubber.without_query(normalized_data[:referer]&.to_s),
-
host: normalized_data[:host]&.to_s,
-
content_type: normalized_data[:content_type]&.to_s,
-
accept: normalized_data[:accept]&.to_s,
-
additional_data: additional_data,
-
timestamp: Time.now
-
)
-
end
-
-
3
sig { params(data: T::Hash[T.any(Symbol, String), T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
-
2
def normalize_lograge_data(data)
-
6
data.each_with_object({}) do |(key, value), normalized|
-
47
normalized[key.to_s.to_sym] = value
-
end
-
end
-
-
3
sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
-
2
def extract_additional_data(data)
-
6
extras = T.let({}, T::Hash[Symbol, T.untyped])
-
6
data.each do |key, value|
-
47
next if LOGRAGE_KNOWN_KEYS.include?(key)
-
8
next if value.nil?
-
-
6
extras[key] = value
-
end
-
-
6
return nil if extras.empty?
-
-
3
extras
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Integrations
-
2
module Puma
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
STATE = T.let(
-
{
-
installed: false,
-
boot_emitted: false,
-
shutdown_emitted: false,
-
handler_pending_started: false,
-
signals_trapped: false,
-
start_info: {
-
mode: nil,
-
puma_version: nil,
-
puma_codename: nil,
-
ruby_version: nil,
-
min_threads: nil,
-
max_threads: nil,
-
environment: nil,
-
pid: nil,
-
listening: []
-
}
-
},
-
T::Hash[Symbol, T.untyped]
-
)
-
-
2
class << self
-
2
extend T::Sig
-
-
4
sig { params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def setup(config)
-
2
return nil unless config.integrations.enable_puma
-
-
# No stdout wrapping here.
-
-
# Ensure Puma is loaded so we can patch its classes
-
begin
-
2
require "puma"
-
rescue LoadError
-
# If Puma isn't available, skip setup
-
1
return nil
-
end
-
-
1
install_patches!
-
-
1
if ARGV.include?("server")
-
# Emit deterministic boot/started events based on CLI args
-
begin
-
port = port_from_argv(ARGV)
-
si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
-
si[:pid] ||= Process.pid
-
si[:environment] ||= ((defined?(::Rails) && ::Rails.respond_to?(:env)) ? ::Rails.env : nil)
-
si[:mode] ||= "single"
-
if port && !T.cast(si[:listening], T::Array[T.untyped]).any? { |a| a.to_s.include?(":" + port.to_s) }
-
si[:listening] = ["tcp://127.0.0.1:#{port}"]
-
end
-
emit_boot_if_needed!
-
unless STATE[:started_emitted]
-
emit_started!
-
STATE[:started_emitted] = true
-
end
-
rescue => e
-
handle_integration_error(e)
-
end
-
begin
-
unless STATE[:signals_trapped]
-
STATE[:signals_trapped] = true
-
%w[TERM INT].each { |signal| trap_shutdown_signal(signal) }
-
end
-
rescue => e
-
handle_integration_error(e)
-
end
-
at_exit do
-
emit_shutdown!("Exiting")
-
rescue => e
-
handle_integration_error(e)
-
end
-
-
# Connection-based readiness: emit started once port is accepting connections
-
# No background threads or sockets; rely solely on parsing Puma output
-
end
-
1
true
-
end
-
-
4
sig { void }
-
2
def install_patches!
-
2
return if STATE[:installed]
-
2
STATE[:installed] = true
-
-
2
state_reset!
-
-
begin
-
begin
-
2
require "puma"
-
rescue LoadError
-
# Puma isn't installed (another server, or a command with "server" in its arguments)
-
1
STATE[:installed] = false
-
1
return
-
end
-
1
puma_mod = ::Object.const_defined?(:Puma) ? ::Object.const_get(:Puma) : nil # rubocop:disable Sorbet/ConstantsFromStrings
-
# rubocop:disable Sorbet/ConstantsFromStrings
-
1
if puma_mod&.const_defined?(:LogWriter)
-
1
::Object.const_get("Puma::LogWriter").prepend(LogWriterPatch)
-
end
-
1
if puma_mod&.const_defined?(:ErrorLogger)
-
1
::Object.const_get("Puma::ErrorLogger").prepend(ErrorLoggerPatch)
-
end
-
1
if puma_mod&.const_defined?(:Events)
-
ev = ::Object.const_get("Puma::Events")
-
ev.prepend(EventsPatch)
-
end
-
# Patch Rack::Handler::Puma.run to emit lifecycle logs using options
-
1
if ::Object.const_defined?(:Rack)
-
1
rack_mod = ::Object.const_get(:Rack)
-
1
if rack_mod.const_defined?(:Handler)
-
handler_mod = rack_mod.const_get(:Handler)
-
if handler_mod.const_defined?(:Puma)
-
handler = handler_mod.const_get(:Puma)
-
handler.singleton_class.prepend(RackHandlerPatch)
-
end
-
end
-
end
-
# Avoid patching CLI/Server; rely on log parsing
-
# Avoid patching CLI to minimize version-specific risks
-
# rubocop:enable Sorbet/ConstantsFromStrings
-
rescue => e
-
handle_integration_error(e)
-
end
-
-
# Rely on Puma patches to observe lines
-
end
-
-
3
sig { params(e: StandardError).void }
-
2
def handle_integration_error(e)
-
1
server_mode = ::LogStruct.server_mode?
-
1
if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env.test? && !server_mode
-
raise e
-
else
-
# LogStruct's own error: logged in production, so Puma keeps running
-
1
LogStruct.handle_exception(e, source: Source::Internal, context: {integration: :puma})
-
end
-
end
-
-
# No stdout interception
-
-
4
sig { void }
-
2
def state_reset!
-
28
STATE[:boot_emitted] = false
-
28
STATE[:shutdown_emitted] = false
-
28
STATE[:started_emitted] = false
-
28
STATE[:handler_pending_started] = false
-
28
STATE[:start_info] = {
-
mode: nil,
-
puma_version: nil,
-
puma_codename: nil,
-
ruby_version: nil,
-
min_threads: nil,
-
max_threads: nil,
-
environment: nil,
-
pid: nil,
-
listening: []
-
}
-
end
-
-
3
sig { params(argv: T::Array[String]).returns(T.nilable(String)) }
-
2
def port_from_argv(argv)
-
1
argv.each_with_index do |arg, idx|
-
2
if arg == "-p" || arg == "--port"
-
1
return argv[idx + 1]
-
1
elsif arg.start_with?("--port=")
-
return arg.split("=", 2)[1]
-
end
-
end
-
nil
-
end
-
-
3
sig { params(line: String).returns(T::Boolean) }
-
2
def process_line(line)
-
13
l = line.to_s.strip
-
13
return false if l.empty?
-
-
# Suppress non-JSON rails banners
-
13
return true if l.start_with?("=> ")
-
-
# Ignore boot line
-
12
return true if l.start_with?("=> Booting Puma")
-
-
12
if l.start_with?("Puma starting in ")
-
# Example: Puma starting in single mode...
-
1
T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:mode] = l.sub("Puma starting in ", "").sub(" mode...", "")
-
1
return true
-
end
-
-
11
if (m = l.match(/^(?:\*\s*)?Puma version: (\S+)(?:.*"([^\"]+)")?/))
-
1
T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:puma_version] = m[1]
-
1
if m[2]
-
1
T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:puma_codename] = m[2]
-
end
-
1
return true
-
end
-
-
10
if (m = l.match(/^\* Ruby version: (.+)$/))
-
1
T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:ruby_version] = m[1]
-
1
return true
-
end
-
-
9
if (m = l.match(/^(?:\*\s*)?Min threads: (\d+)/))
-
1
T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:min_threads] = m[1].to_i
-
1
return true
-
end
-
-
8
if (m = l.match(/^(?:\*\s*)?Max threads: (\d+)/))
-
1
T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:max_threads] = m[1].to_i
-
1
return true
-
end
-
-
7
if (m = l.match(/^(?:\*\s*)?Environment: (\S+)/))
-
1
T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:environment] = m[1]
-
1
return true
-
end
-
-
6
if (m = l.match(/^(?:\*\s*)?PID:\s+(\d+)/))
-
1
T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:pid] = m[1].to_i
-
1
return true
-
end
-
-
5
if (m = l.match(/^\*?\s*Listening on (.+)$/))
-
1
si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
-
1
list = T.cast(si[:listening], T::Array[T.untyped])
-
1
address = m[1].to_s
-
1
list << address unless list.include?(address)
-
# Emit started when we see the first listening address
-
1
if !STATE[:started_emitted]
-
1
emit_started!
-
1
STATE[:started_emitted] = true
-
end
-
1
return true
-
end
-
-
4
if l == "Use Ctrl-C to stop"
-
1
si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
-
# Fallback: if no listening address captured yet, infer from ARGV
-
1
if T.cast(si[:listening], T::Array[T.untyped]).empty?
-
begin
-
1
port = port_from_argv(ARGV)
-
1
if port
-
1
si[:listening] << "tcp://127.0.0.1:#{port}"
-
end
-
rescue => e
-
handle_integration_error(e)
-
end
-
end
-
1
if !STATE[:started_emitted]
-
1
emit_started!
-
1
STATE[:started_emitted] = true
-
end
-
1
return false
-
end
-
-
3
if l.start_with?("- Gracefully stopping")
-
1
emit_shutdown!(l)
-
1
return true
-
end
-
-
2
if l.start_with?("=== puma shutdown:")
-
emit_shutdown!(l)
-
return true
-
end
-
-
2
if l == "- Goodbye!"
-
# Swallow
-
1
return true
-
end
-
-
1
if l == "Exiting"
-
emit_shutdown!(l)
-
return true
-
end
-
-
1
false
-
end
-
-
2
sig { void }
-
2
def emit_boot_if_needed!
-
# Intentionally no-op: we no longer emit a boot log
-
STATE[:boot_emitted] = true
-
end
-
-
# No server hooks; rely on parsing only
-
-
3
sig { void }
-
2
def emit_started!
-
2
si = T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])
-
2
log = Log::Puma::Start.new(
-
mode: T.cast(si[:mode], T.nilable(String)),
-
puma_version: T.cast(si[:puma_version], T.nilable(String)),
-
puma_codename: T.cast(si[:puma_codename], T.nilable(String)),
-
ruby_version: T.cast(si[:ruby_version], T.nilable(String)),
-
min_threads: T.cast(si[:min_threads], T.nilable(Integer)),
-
max_threads: T.cast(si[:max_threads], T.nilable(Integer)),
-
environment: T.cast(si[:environment], T.nilable(String)),
-
process_id: T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:pid] || Process.pid,
-
listening_addresses: T.cast(T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:listening], T::Array[String]),
-
level: Level::Info,
-
timestamp: Time.now
-
)
-
2
LogStruct.info(log)
-
2
STATE[:handler_pending_started] = false
-
# Only use LogStruct; SemanticLogger routes to STDOUT in test
-
end
-
-
# Logs the shutdown when the signal arrives, then runs the handler that was there before
-
# (Puma's, which stops the server gracefully), or does what the signal does by default
-
3
sig { params(signal: String).void }
-
2
def trap_shutdown_signal(signal)
-
1
previous = T.let(nil, T.untyped)
-
1
previous = Signal.trap(signal) do |signal_number|
-
1
emit_shutdown!(signal)
-
1
run_previous_signal_handler(previous, signal, signal_number)
-
end
-
end
-
-
3
sig { params(previous: T.untyped, signal: String, signal_number: T.untyped).void }
-
2
def run_previous_signal_handler(previous, signal, signal_number)
-
1
if previous.respond_to?(:call)
-
1
(previous.respond_to?(:arity) && previous.arity == 0) ? previous.call : previous.call(signal_number)
-
elsif previous == "EXIT"
-
Kernel.exit
-
elsif previous.nil? || previous == "DEFAULT" || previous == "SYSTEM_DEFAULT"
-
# Put back the default and send the signal again, so the process ends as it would have
-
Signal.trap(signal, previous || "DEFAULT")
-
Process.kill(signal, Process.pid)
-
end
-
# "IGNORE": nothing more to do
-
end
-
-
3
sig { params(_message: String).void }
-
2
def emit_shutdown!(_message)
-
1
return if STATE[:shutdown_emitted]
-
1
STATE[:shutdown_emitted] = true
-
1
log = Log::Puma::Shutdown.new(
-
process_id: T.cast(STATE[:start_info], T::Hash[Symbol, T.untyped])[:pid] || Process.pid,
-
level: Level::Info,
-
timestamp: Time.now
-
)
-
1
LogStruct.info(log)
-
# Only use LogStruct; SemanticLogger routes to STDOUT in test
-
# Let SemanticLogger appender write to STDOUT
-
end
-
end
-
-
# STDOUT interception is handled globally via StdoutFilter; keep Puma patches minimal
-
-
# Patch Puma::LogWriter to intercept log writes
-
2
module LogWriterPatch
-
2
extend T::Sig
-
-
2
sig { params(msg: String).returns(T.untyped) }
-
2
def log(msg)
-
consumed = ::LogStruct::Integrations::Puma.process_line(msg)
-
super unless consumed
-
end
-
-
2
sig { params(msg: String).returns(T.untyped) }
-
2
def write(msg)
-
any_consumed = T.let(false, T::Boolean)
-
msg.to_s.each_line do |l|
-
any_consumed = true if ::LogStruct::Integrations::Puma.process_line(l)
-
end
-
super unless any_consumed
-
end
-
-
2
sig { params(msg: String).returns(T.untyped) }
-
2
def <<(msg)
-
any_consumed = T.let(false, T::Boolean)
-
msg.to_s.each_line do |l|
-
any_consumed = true if ::LogStruct::Integrations::Puma.process_line(l)
-
end
-
super unless any_consumed
-
end
-
-
2
sig { params(msg: String).returns(T.untyped) }
-
2
def puts(msg)
-
consumed = ::LogStruct::Integrations::Puma.process_line(msg)
-
if consumed
-
# attempt to suppress; only forward if not consumed
-
return nil
-
end
-
if ::Kernel.instance_variables.include?(:@stdout)
-
io = ::Kernel.instance_variable_get(:@stdout)
-
return io.puts(msg)
-
end
-
super
-
end
-
-
2
sig { params(msg: String).returns(T.untyped) }
-
2
def info(msg)
-
consumed = ::LogStruct::Integrations::Puma.process_line(msg)
-
super unless consumed
-
end
-
end
-
-
# Puma::ErrorLogger writes request errors (an exception the app didn't handle, a malformed
-
# request, an SSL error) as plain text to STDERR, with the request's query string. Log them
-
# as structured lines, with the method and path but not the query string, which LogStruct
-
# can't filter there.
-
2
module ErrorLoggerPatch
-
2
extend T::Sig
-
-
3
sig { params(options: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
-
2
def info(options = {})
-
2
error = options[:error]
-
2
return super unless error.is_a?(Exception)
-
-
2
env = options[:req]&.env
-
2
::LogStruct.error(::LogStruct::Log::Puma::Error.new(
-
error_class: error.class,
-
message: error.message,
-
backtrace: error.backtrace,
-
error_context: options[:text]&.to_s,
-
http_method: env&.dig("REQUEST_METHOD"),
-
2
path: env && (env["REQUEST_PATH"] || env["PATH_INFO"])
-
))
-
2
nil
-
rescue => e
-
::LogStruct::Integrations::Puma.handle_integration_error(e)
-
super
-
end
-
end
-
-
# Patch Puma::Events as a fallback for some versions where Events handles output
-
2
module EventsPatch
-
2
extend T::Sig
-
-
2
sig { params(str: String).returns(T.untyped) }
-
2
def log(str)
-
consumed = ::LogStruct::Integrations::Puma.process_line(str)
-
super unless consumed
-
end
-
end
-
-
# Hook Rack::Handler::Puma.run to emit structured started/shutdown
-
2
module RackHandlerPatch
-
2
extend T::Sig
-
-
2
sig do
-
params(
-
app: T.untyped,
-
args: T.untyped,
-
block: T.nilable(T.proc.returns(T.untyped))
-
).returns(T.untyped)
-
end
-
2
def run(app, *args, &block)
-
rest = args
-
options = T.let({}, T::Hash[T.untyped, T.untyped])
-
rest.each do |value|
-
next unless value.is_a?(Hash)
-
options.merge!(value)
-
end
-
-
begin
-
si = T.cast(::LogStruct::Integrations::Puma::STATE[:start_info], T::Hash[Symbol, T.untyped])
-
si[:mode] ||= "single"
-
si[:environment] ||= ((defined?(::Rails) && ::Rails.respond_to?(:env)) ? ::Rails.env : nil)
-
si[:pid] ||= Process.pid
-
si[:listening] ||= []
-
port = T.let(nil, T.untyped)
-
host = T.let(nil, T.untyped)
-
if options.respond_to?(:[])
-
port = options[:Port] || options["Port"] || options[:port] || options["port"]
-
host = options[:Host] || options["Host"] || options[:host] || options["host"]
-
end
-
if port
-
list = T.cast(si[:listening], T::Array[T.untyped])
-
list.clear
-
h = (host && host != "0.0.0.0") ? host : "127.0.0.1"
-
list << "tcp://#{h}:#{port}"
-
end
-
state = ::LogStruct::Integrations::Puma::STATE
-
state[:handler_pending_started] = true unless state[:started_emitted]
-
rescue => e
-
::LogStruct::Integrations::Puma.handle_integration_error(e)
-
end
-
-
begin
-
Kernel.at_exit do
-
unless ::LogStruct::Integrations::Puma::STATE[:shutdown_emitted]
-
::LogStruct::Integrations::Puma.emit_shutdown!("Exiting")
-
::LogStruct::Integrations::Puma::STATE[:shutdown_emitted] = true
-
end
-
rescue => e
-
::LogStruct::Integrations::Puma.handle_integration_error(e)
-
end
-
rescue => e
-
::LogStruct::Integrations::Puma.handle_integration_error(e)
-
end
-
-
begin
-
result = super(app, **options, &block)
-
ensure
-
state = ::LogStruct::Integrations::Puma::STATE
-
if state[:handler_pending_started] && !state[:started_emitted]
-
begin
-
::LogStruct::Integrations::Puma.emit_started!
-
state[:started_emitted] = true
-
rescue => e
-
::LogStruct::Integrations::Puma.handle_integration_error(e)
-
ensure
-
state[:handler_pending_started] = false
-
end
-
end
-
end
-
-
result
-
end
-
end
-
-
# (No Launcher patch)
-
-
# No Server patch
-
-
# No InterceptorIO
-
-
# Removed EventsInitPatch and CLIPatch to avoid version-specific conflicts
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "rack"
-
2
require "action_dispatch/middleware/debug_exceptions"
-
2
require_relative "rack_error_handler/middleware"
-
2
require_relative "rack_error_handler/debug_exceptions_logging"
-
2
require_relative "rack_error_handler/parse_error_logging"
-
2
require_relative "rack_setup"
-
-
2
module LogStruct
-
2
module Integrations
-
# Rack middleware integration for structured logging
-
2
module RackErrorHandler
-
2
extend T::Sig
-
2
extend IntegrationInterface
-
-
2
@interceptor_registered = T.let(false, T::Boolean)
-
-
# Set up structured lines for CSRF and IP spoofing errors, errors the app didn't handle, and
-
# errors that get past ShowExceptions
-
4
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def self.setup(config)
-
2
return nil unless RackSetup.enabled?(config)
-
-
# Errors from the middleware outside ShowExceptions, and any that get past it
-
2
RackSetup.insert_before(
-
::ActionDispatch::RemoteIp,
-
Integrations::RackErrorHandler::Middleware
-
)
-
# DebugExceptions calls its interceptors with each error from the app (in every environment),
-
# before ShowExceptions turns it into Rails' usual response: 422 for a CSRF error
-
2
unless @interceptor_registered
-
2
::ActionDispatch::DebugExceptions.register_interceptor(ErrorLines.method(:intercept))
-
2
@interceptor_registered = true
-
end
-
# An error line instead of the text Rails logs for an error the app didn't handle
-
2
unless ::ActionDispatch::DebugExceptions <= DebugExceptionsLogging
-
2
::ActionDispatch::DebugExceptions.prepend(DebugExceptionsLogging)
-
end
-
# The size of a body Rails can't parse, instead of the body
-
2
::ActionDispatch::Request.prepend(ParseErrorLogging) unless ::ActionDispatch::Request <= ParseErrorLogging
-
-
2
true
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "error_lines"
-
-
2
module LogStruct
-
2
module Integrations
-
2
module RackErrorHandler
-
# Prepended to ActionDispatch::DebugExceptions. For an error the app didn't handle, Rails logs a
-
# block of text (the error, its source code and its trace) before ShowExceptions renders the
-
# error page. LogStruct writes an error line instead.
-
2
module DebugExceptionsLogging
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
requires_ancestor { ::ActionDispatch::DebugExceptions }
-
-
2
private
-
-
2
sig { params(request: ::ActionDispatch::Request, wrapper: T.untyped).returns(T.untyped) }
-
2
def log_error(request, wrapper)
-
return super unless ErrorLines.enabled?
-
# As Rails does: config.action_dispatch.log_rescued_responses = false leaves out the errors
-
# Rails turns into a response of their own (404 for a routing error, 422 for CSRF)
-
return if !request.get_header("action_dispatch.log_rescued_responses") && wrapper.rescue_response?
-
-
ErrorLines.log_unhandled(request, wrapper)
-
rescue => e
-
LogStruct.handle_exception(e, source: Source::Internal, context: {integration: :rack_error_handler})
-
super
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "../../params_errors"
-
-
2
module LogStruct
-
2
module Integrations
-
2
module RackErrorHandler
-
# The structured lines for errors raised while Rails handles a request. Each error is logged once:
-
# security errors from the app by DebugExceptions' interceptor, anything else that gets past
-
# ShowExceptions by the middleware. Rails still turns each error into its usual response.
-
2
module ErrorLines
-
2
extend T::Sig
-
-
2
CSRF_ERROR_NAMES = T.let(
-
["ActionController::InvalidAuthenticityToken", "ActionController::InvalidCrossOriginRequest"].freeze,
-
T::Array[String]
-
)
-
-
3
sig { returns(T::Boolean) }
-
2
def self.enabled?
-
6
LogStruct.enabled? && LogStruct.config.integrations.enable_rack_error_handler
-
end
-
-
# For DebugExceptions.register_interceptor: CSRF and IP spoofing errors from the app, which
-
# ShowExceptions would otherwise turn into a response before any middleware saw them.
-
# Other errors are left to Rails' own logging and error reporting.
-
3
sig { params(request: ::ActionDispatch::Request, error: Exception).void }
-
2
def self.intercept(request, error)
-
2
return unless enabled?
-
2
return unless security_error?(error)
-
2
return if LoggedErrors.logged?(error)
-
-
2
log_security_error(request, error)
-
end
-
-
# An error that got past ShowExceptions (or came from a middleware outside it)
-
3
sig { params(request: ::ActionDispatch::Request, error: StandardError).void }
-
2
def self.log_escaped(request, error)
-
3
return unless enabled?
-
3
return if LoggedErrors.logged?(error)
-
2
return log_security_error(request, error) if security_error?(error)
-
-
2
LoggedErrors.mark(error)
-
2
LogStruct.error(
-
Log::Error.new(
-
source: Source::Rails,
-
error_class: error.class,
-
message: ParamsErrors.messages(error).first,
-
backtrace: error.backtrace,
-
path: request.path,
-
http_method: request.method,
-
source_ip: source_ip(request),
-
user_agent: request.user_agent,
-
referer: StringScrubber.without_query(request.referer)
-
)
-
)
-
end
-
-
# An error the app didn't handle, which DebugExceptions logs before ShowExceptions renders Rails'
-
# error page: at config.action_dispatch.debug_exception_log_level (fatal by default), with the
-
# trace Rails would log and the response's status. A security error already has its own line.
-
3
sig { params(request: ::ActionDispatch::Request, wrapper: T.untyped).void }
-
2
def self.log_unhandled(request, wrapper)
-
4
error = T.cast(wrapper.exception, Exception)
-
4
return if LoggedErrors.logged?(error)
-
-
4
LoggedErrors.mark(error)
-
# Without a backtrace cleaner, Rails' trace is Thread::Backtrace::Locations
-
4
trace = Array(wrapper.exception_trace).map(&:to_s)
-
4
message, cause_message = ParamsErrors.messages(error)
-
4
line = Log::Error.new(
-
source: Source::Rails,
-
error_class: error.class,
-
message: message,
-
4
backtrace: trace.empty? ? error.backtrace : trace,
-
path: request.path,
-
http_method: request.method,
-
source_ip: source_ip(request),
-
user_agent: request.user_agent,
-
referer: StringScrubber.without_query(request.referer),
-
status: wrapper.status_code,
-
cause_class: error.cause&.class&.name,
-
cause_message: cause_message
-
)
-
4
case request.get_header("action_dispatch.debug_exception_log_level")
-
when ::Logger::DEBUG then LogStruct.debug(line)
-
when ::Logger::INFO then LogStruct.info(line)
-
when ::Logger::WARN then LogStruct.warn(line)
-
when ::Logger::ERROR then LogStruct.error(line)
-
4
else LogStruct.fatal(line)
-
end
-
end
-
-
# A security line, and the error handled in the security_errors mode (reported by default)
-
3
sig { params(request: ::ActionDispatch::Request, error: Exception).void }
-
2
def self.log_security_error(request, error)
-
2
LoggedErrors.mark(error)
-
2
if ip_spoof_error?(error)
-
LogStruct.warn(
-
Log::Security::IPSpoof.new(
-
path: request.path,
-
http_method: request.method,
-
user_agent: request.user_agent,
-
referer: StringScrubber.without_query(request.referer),
-
message: error.message,
-
source_ip: request.ip,
-
client_ip: request.get_header("HTTP_CLIENT_IP"),
-
x_forwarded_for: request.x_forwarded_for,
-
timestamp: Time.now
-
)
-
)
-
else
-
2
LogStruct.error(
-
Log::Security::CSRFViolation.new(
-
path: request.path,
-
http_method: request.method,
-
source_ip: source_ip(request),
-
user_agent: request.user_agent,
-
referer: StringScrubber.without_query(request.referer),
-
message: error.message,
-
timestamp: Time.now
-
)
-
)
-
end
-
2
reported = error.is_a?(StandardError) ? error : RuntimeError.new(error.message)
-
2
LogStruct.handle_exception(reported, source: Source::Security, context: request_context(request), log: false)
-
end
-
-
3
sig { params(request: ::ActionDispatch::Request).returns(T::Hash[Symbol, T.untyped]) }
-
2
def self.request_context(request)
-
{
-
2
path: request.path,
-
method: request.method,
-
user_agent: request.user_agent,
-
referer: StringScrubber.without_query(request.referer)
-
}
-
end
-
-
# The client's IP, as Rails works it out, or the connection's when Rails finds a spoofed one
-
3
sig { params(request: ::ActionDispatch::Request).returns(T.nilable(String)) }
-
2
def self.source_ip(request)
-
8
request.remote_ip
-
rescue ::ActionDispatch::RemoteIp::IpSpoofAttackError
-
request.ip
-
end
-
-
3
sig { params(error: Exception).returns(T::Boolean) }
-
2
def self.security_error?(error)
-
8
ip_spoof_error?(error) || CSRF_ERROR_NAMES.include?(error.class.name)
-
end
-
-
3
sig { params(error: Exception).returns(T::Boolean) }
-
2
def self.ip_spoof_error?(error)
-
10
error.is_a?(::ActionDispatch::RemoteIp::IpSpoofAttackError)
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "../rack_middleware"
-
2
require_relative "error_lines"
-
-
2
module LogStruct
-
2
module Integrations
-
2
module RackErrorHandler
-
# Logs the errors that get past ShowExceptions, or come from the middleware outside it (an IP
-
# spoofing error from a middleware that reads remote_ip, for example), then raises them again
-
# for the server to handle as it would without LogStruct
-
2
class Middleware < RackMiddleware
-
3
sig { override.params(env: T.untyped).returns(T.untyped) }
-
2
def call(env)
-
2
@app.call(env)
-
rescue => error
-
begin
-
2
ErrorLines.log_escaped(::ActionDispatch::Request.new(env), error)
-
rescue => logging_error
-
LogStruct.handle_exception(logging_error, source: Source::Internal)
-
end
-
2
raise error
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Integrations
-
2
module RackErrorHandler
-
# Prepended to ActionDispatch::Request. When Rails can't parse a request's body (malformed JSON), it logs the
-
# whole body at debug level, where nothing filters it. LogStruct logs the body's size and type instead. The
-
# request line and the ParseError's error line are written as usual.
-
2
module ParseErrorLogging
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
requires_ancestor { ::ActionDispatch::Request }
-
-
2
private
-
-
3
sig { returns(T.untyped) }
-
2
def log_parse_error_once
-
1
return super unless ErrorLines.enabled?
-
-
1
@logstruct_parse_error_logged ||= T.let(
-
begin
-
1
(logger || ::ActiveSupport::Logger.new($stderr)).debug(
-
"Error occurred while parsing request parameters. LogStruct leaves out the body " \
-
"(#{raw_post.to_s.bytesize} bytes of #{media_type}), which it can't filter."
-
)
-
1
true
-
end,
-
T.nilable(TrueClass)
-
)
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Integrations
-
# A Rack middleware around the app: subclasses define call, and pass the request on to @app
-
2
class RackMiddleware
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
abstract!
-
-
4
sig { params(app: T.untyped).void }
-
2
def initialize(app)
-
5
@app = app
-
end
-
-
3
sig { abstract.params(env: T.untyped).returns(T.untyped) }
-
2
def call(env)
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Integrations
-
2
module RackSetup
-
2
extend T::Sig
-
-
4
sig { params(config: LogStruct::Configuration).returns(T::Boolean) }
-
2
def self.enabled?(config)
-
2
return false unless config.enabled
-
2
return false unless config.integrations.enable_rack_error_handler
-
-
2
true
-
end
-
-
2
sig { params(anchor: T.untyped, middleware: T.untyped).void }
-
2
def self.insert_after(anchor, middleware)
-
::Rails.application.middleware.insert_after(anchor, middleware)
-
end
-
-
4
sig { params(anchor: T.untyped, middleware: T.untyped).void }
-
2
def self.insert_before(anchor, middleware)
-
2
::Rails.application.middleware.insert_before(anchor, middleware)
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Integrations
-
# Rails' own log subscribers, which LogStruct's lines replace
-
2
module RailsLogSubscribers
-
2
extend T::Sig
-
-
# Detaches a framework's log subscriber (ActiveStorage::LogSubscriber, :active_storage). Up to Rails 8.1 it's an
-
# ActiveSupport::LogSubscriber attached to the framework's notifications; from Rails 8.2 it's an
-
# ActiveSupport::EventReporter::LogSubscriber, subscribed to Rails.event when its file loads.
-
4
sig { params(subscriber_class: T.untyped, namespace: Symbol).void }
-
2
def self.detach(subscriber_class, namespace)
-
14
if subscriber_class.respond_to?(:detach_from)
-
10
subscriber_class.detach_from(namespace)
-
else
-
4
unsubscribe_from_events(subscriber_class)
-
end
-
end
-
-
# Unsubscribes a Rails 8.2 log subscriber from Rails.event. Does nothing for one that isn't subscribed there.
-
4
sig { params(subscriber_class: T.untyped).void }
-
2
def self.unsubscribe_from_events(subscriber_class)
-
6
return unless ::ActiveSupport.respond_to?(:event_reporter)
-
-
6
reporter = ::ActiveSupport.event_reporter
-
6
reporter.unsubscribe(subscriber_class) if reporter.respond_to?(:unsubscribe)
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "request_context/middleware"
-
-
2
module LogStruct
-
2
module Integrations
-
# Request context integration that captures request_id for all logs
-
2
module RequestContext
-
2
extend T::Sig
-
2
extend IntegrationInterface
-
-
4
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def self.setup(config)
-
2
return nil unless config.enabled
-
-
# Insert after RequestId middleware so request_id is available
-
2
::Rails.application.middleware.insert_after(
-
::ActionDispatch::RequestId,
-
Integrations::RequestContext::Middleware
-
)
-
-
2
true
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "../rack_middleware"
-
-
2
module LogStruct
-
2
module Integrations
-
2
module RequestContext
-
# Middleware that captures request_id and stores it in SemanticLogger's
-
# named_tags so all logs during the request include the request_id.
-
2
class Middleware < RackMiddleware
-
2
sig { override.params(env: T.untyped).returns(T.untyped) }
-
2
def call(env)
-
request = ::ActionDispatch::Request.new(env)
-
request_id = request.request_id
-
Thread.current[:logstruct_request_id] = request_id
-
::SemanticLogger.push_named_tags(request_id: request_id)
-
@app.call(env)
-
ensure
-
::SemanticLogger.pop_named_tags
-
Thread.current[:logstruct_request_id] = nil
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
begin
-
2
require "shrine"
-
rescue LoadError
-
# Shrine gem is not available, integration will be skipped
-
end
-
-
2
module LogStruct
-
2
module Integrations
-
# Shrine integration for structured logging
-
2
module Shrine
-
2
extend T::Sig
-
2
extend IntegrationInterface
-
-
2
SHRINE_EVENTS = T.let(%i[upload exists download delete metadata open].freeze, T::Array[Symbol])
-
-
# The file name in a Content-Disposition (attachment; filename="..."; filename*=UTF-8''...). A quoted name
-
# can hold a semicolon, and whatever follows a quote (malformed or not) goes too.
-
2
DISPOSITION_FILENAME_REGEX = /(\bfilename\*?\s*=\s*)(?:"(?:[^"\\]|\\.)*"?)?[^;]*/i
-
-
# Set up Shrine structured logging
-
4
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def self.setup(config)
-
2
return nil unless defined?(::Shrine)
-
2
return nil unless config.enabled
-
2
return nil unless config.integrations.enable_shrine
-
-
# Create a structured log subscriber for Shrine
-
# ActiveSupport::Notifications::Event has name, time, end, transaction_id, payload, and duration
-
2
shrine_log_subscriber = lambda do |event|
-
payload = event.payload.except(:io, :name).dup
-
-
# Map event name to Event type
-
event_type = case event.name
-
when :upload then Event::Upload
-
when :download then Event::Download
-
when :open then Event::Download
-
when :delete then Event::Delete
-
when :exists then Event::Exist
-
else Event::Metadata
-
end
-
-
# Create structured log data
-
# Ensure storage is always a symbol
-
storage_sym = payload[:storage].to_sym
-
-
log_data = case event_type
-
when Event::Upload
-
metadata = payload[:metadata].is_a?(Hash) ? payload[:metadata] : {}
-
size = metadata["size"]
-
Log::Shrine::Upload.new(
-
storage: storage_sym,
-
location: payload[:location],
-
mime_type: metadata["mime_type"]&.to_s,
-
size: size.is_a?(Integer) ? size : nil,
-
uploader: payload[:uploader]&.to_s,
-
upload_options: without_file_names(payload[:upload_options]),
-
options: payload[:options],
-
duration_ms: event.duration.to_f
-
)
-
when Event::Download
-
Log::Shrine::Download.new(
-
storage: storage_sym,
-
location: payload[:location],
-
download_options: payload[:download_options]
-
)
-
when Event::Delete
-
Log::Shrine::Delete.new(
-
storage: storage_sym,
-
location: payload[:location]
-
)
-
when Event::Exist
-
Log::Shrine::Exist.new(
-
storage: storage_sym,
-
location: payload[:location]
-
)
-
else
-
# metadata, the only other event in SHRINE_EVENTS
-
Log::Shrine::Metadata.new(storage: storage_sym, duration_ms: event.duration.to_f)
-
end
-
-
# Log directly through SemanticLogger, NOT through Shrine.logger
-
# Shrine.logger is a basic Logger that would just call .to_s on the struct
-
::SemanticLogger[::Shrine].info(log_data)
-
end
-
-
# Load the instrumentation plugin without Shrine's default log subscriber, which logs
-
# each event as a line of text (a no-op if the app has loaded it already). Shrine
-
# ignores a subscriber that's already subscribed.
-
2
::Shrine.plugin :instrumentation, log_subscriber: nil
-
2
SHRINE_EVENTS.each do |event_name|
-
12
::Shrine.subscribe(event_name, &shrine_log_subscriber)
-
end
-
-
# The app may load the plugin with the default log subscriber too, before or after this
-
2
::Shrine::Plugins::Instrumentation::LogSubscriber.singleton_class.prepend(SkipLoggedEvents)
-
-
# Other plugins' log subscribers and Shrine's warnings write text to Shrine.logger
-
2
::Shrine.logger = ::SemanticLogger[::Shrine]
-
-
2
true
-
end
-
-
# Upload options with the file names taken out of a Content-Disposition (upload_options:
-
# {content_disposition: ...}, which the upload_options plugin often sets): they can name a person
-
2
sig { params(options: T.untyped).returns(T.untyped) }
-
2
def self.without_file_names(options)
-
return options unless options.is_a?(Hash)
-
-
options.to_h do |key, value|
-
if value.is_a?(String) && KeyWords.normalize(key).include?("disposition")
-
[key, value.gsub(DISPOSITION_FILENAME_REGEX, '\1[FILENAME]')]
-
else
-
[key, value]
-
end
-
end
-
end
-
-
# Prepended to Shrine's default instrumentation log subscriber, so it doesn't log the
-
# events that LogStruct logs
-
2
module SkipLoggedEvents
-
2
extend T::Sig
-
-
2
sig { params(event: T.untyped).returns(T.untyped) }
-
2
def call(event)
-
return if SHRINE_EVENTS.include?(event.name)
-
-
super
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
begin
-
2
require "sidekiq"
-
rescue LoadError
-
# Sidekiq gem is not available, integration will be skipped
-
end
-
2
if defined?(::Sidekiq)
-
1
require_relative "sidekiq/logger"
-
1
require_relative "sidekiq/job_context_middleware"
-
end
-
-
2
module LogStruct
-
2
module Integrations
-
# Sidekiq integration for structured logging
-
2
module Sidekiq
-
2
extend T::Sig
-
2
extend IntegrationInterface
-
-
2
@original_logger = T.let(nil, T.untyped)
-
-
# Set up Sidekiq structured logging
-
3
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def self.setup(config)
-
2
return nil unless defined?(::Sidekiq)
-
2
return nil unless config.enabled
-
2
return nil unless config.integrations.enable_sidekiq
-
-
1
configure_logger
-
-
1
::Sidekiq.configure_server do |sidekiq_config|
-
# First in the chain, so lines logged by other middleware include the job too
-
sidekiq_config.server_middleware do |chain|
-
chain.prepend LogStruct::Integrations::Sidekiq::JobContextMiddleware
-
end
-
-
replace_default_error_handler(sidekiq_config)
-
end
-
-
1
true
-
end
-
-
# Use LogStruct's logger for Sidekiq's own lines. The Railtie calls this as soon as it replaces
-
# Rails.logger, before config/initializers run, because an initializer that configures Sidekiq
-
# (connecting to Redis, setting up a fetcher) logs with Sidekiq's logger straight away.
-
4
sig { void }
-
2
def self.configure_logger
-
5
return unless defined?(::Sidekiq)
-
-
4
current = sidekiq_logger_config.logger
-
4
return if current.is_a?(LogStruct::Integrations::Sidekiq::Logger)
-
-
3
@original_logger = current
-
3
name = ::Sidekiq.server? ? "Sidekiq-Server" : "Sidekiq-Client"
-
3
sidekiq_logger_config.logger = LogStruct::Integrations::Sidekiq::Logger.new(name)
-
end
-
-
# Put Sidekiq's own logger back, when an initializer turned LogStruct or this integration off
-
# after configure_logger ran
-
4
sig { void }
-
2
def self.restore_logger
-
2
return unless defined?(::Sidekiq)
-
1
return if @original_logger.nil?
-
-
1
sidekiq_logger_config.logger = @original_logger
-
1
@original_logger = nil
-
end
-
-
# Sidekiq 7 keeps the logger on its default configuration, Sidekiq 6 on the module
-
3
sig { returns(T.untyped) }
-
2
def self.sidekiq_logger_config
-
8
::Sidekiq.respond_to?(:default_configuration) ? ::Sidekiq.default_configuration : ::Sidekiq
-
end
-
-
# Sidekiq's default error handler logs three lines for each failed job: the job (with its
-
# arguments) as a JSON string, the error, and the backtrace. Log one structured line instead,
-
# without the arguments, which LogStruct can't filter inside a JSON string.
-
3
sig { params(sidekiq_config: T.untyped).void }
-
2
def self.replace_default_error_handler(sidekiq_config)
-
4
return unless defined?(::Sidekiq::Config::ERROR_HANDLER)
-
-
4
handlers = sidekiq_config.error_handlers
-
4
return unless handlers.delete(::Sidekiq::Config::ERROR_HANDLER)
-
-
1
handlers.unshift(JOB_ERROR_HANDLER)
-
end
-
-
# The line has the job's ID and class in ctx, like the lines logged while the job ran (Sidekiq
-
# calls its error handlers after the job's middleware has finished).
-
3
sig { params(error: Exception, context: T::Hash[T.untyped, T.untyped]).void }
-
2
def self.log_job_error(error, context)
-
# LogStruct's ActiveJob integration logs the errors it sees an ActiveJob job raise. An
-
# error raised before ActiveJob ran the job (a job class a deploy removed, say) is logged
-
# here.
-
3
return if LogStruct::LoggedErrors.logged?(error)
-
-
2
job = context[:job]
-
2
job = nil unless job.is_a?(Hash)
-
2
error_context = context[:context]
-
-
2
line = Log::Sidekiq::Error.new(
-
error_class: error.class,
-
message: error.message,
-
backtrace: error.backtrace,
-
error_context: error_context&.to_s,
-
queue_name: job&.dig("queue")&.to_s,
-
retry_count: job&.dig("retry_count")
-
)
-
2
if job.nil?
-
::Rails.logger.warn(line)
-
return
-
end
-
-
job_context = {
-
2
jid: job["jid"],
-
class: job["display_class"] || job["wrapped"] || job["class"]
-
}.compact
-
4
::SemanticLogger.named_tagged(sidekiq_context: job_context) { ::Rails.logger.warn(line) }
-
end
-
-
2
JOB_ERROR_HANDLER = T.let(
-
3
->(error, context, _config = nil) { log_job_error(error, context) },
-
T.proc.params(error: Exception, context: T::Hash[T.untyped, T.untyped], config: T.untyped).void
-
)
-
-
2
private_class_method :sidekiq_logger_config, :replace_default_error_handler, :log_job_error
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
1
module LogStruct
-
1
module Integrations
-
1
module Sidekiq
-
# Sidekiq server middleware that adds the running job's ID and class to every line
-
# logged while it runs, in the same `ctx` field as Sidekiq's own log lines. A query
-
# on ctx.jid then returns every line for one job run.
-
1
class JobContextMiddleware
-
1
extend T::Sig
-
-
1
sig do
-
1
params(
-
job_instance: T.untyped,
-
job_hash: T::Hash[String, T.untyped],
-
queue: String,
-
block: T.proc.returns(T.untyped)
-
).returns(T.untyped)
-
end
-
1
def call(job_instance, job_hash, queue, &block)
-
context = {
-
2
jid: job_hash["jid"],
-
# Same precedence as Sidekiq's own context, so ActiveJob jobs show their class
-
class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"]
-
}
-
2
::SemanticLogger.named_tagged(sidekiq_context: context, &block)
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
1
require_relative "../../semantic_logger/logger"
-
1
require_relative "../../log/sidekiq"
-
1
require_relative "../../enums/source"
-
-
1
module LogStruct
-
1
module Integrations
-
1
module Sidekiq
-
1
extend T::Sig
-
-
# Get thread ID for Sidekiq logging
-
2
sig { returns(String) }
-
1
def self.tid
-
3
Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
-
end
-
-
# Custom Logger for Sidekiq that creates LogStruct::Log::Sidekiq::Log entries
-
1
class Logger < LogStruct::SemanticLogger::Logger
-
1
extend T::Sig
-
-
# Override log methods to create Sidekiq log structs
-
1
%i[debug info warn error fatal].each do |level|
-
5
define_method(level) do |message = nil, payload = nil, &block|
-
# Hashes, LogStruct log entries and exceptions are logged as they are, so their keys are
-
# filtered (Sidekiq logs some settings as a Hash)
-
5
unless message.nil? || message.is_a?(String)
-
1
next super(message, payload, &block)
-
end
-
# A message with an exception is an error line with both
-
4
next super(message, payload, &block) if payload.is_a?(Exception)
-
-
3
msg = message || (block ? block.call.to_s : "")
-
# Lines logged outside a job have no context
-
3
context = ::Sidekiq::Context.current
-
-
# Create a Sidekiq log struct with the message
-
3
log_struct = Log::Sidekiq::Log.new(
-
level: LogStruct::Level.from_severity(level.to_s.upcase),
-
event: Event::Log,
-
message: msg,
-
process_id: ::Process.pid,
-
thread_id: Sidekiq.tid,
-
# Log a copy: SemanticLogger may still be serializing the struct on its appender
-
# thread when Sidekiq adds :elapsed to the live context after the job finishes.
-
3
context: context.blank? ? nil : context.dup,
-
3
additional_data: payload.is_a?(Hash) ? payload : nil
-
)
-
-
# Pass the struct to SemanticLogger
-
3
super(log_struct, nil, &nil)
-
end
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "sorbet-runtime"
-
-
2
module LogStruct
-
2
module Integrations
-
# Integration for Sorbet runtime type checking error handlers
-
# This module installs error handlers that report type errors through LogStruct
-
# These handlers can be enabled/disabled using configuration
-
2
module Sorbet
-
2
extend T::Sig
-
2
extend IntegrationInterface
-
-
# Set up Sorbet error handlers to report errors through LogStruct
-
4
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
-
2
def self.setup(config)
-
12
return nil unless config.integrations.enable_sorbet_error_handlers
-
-
# Install inline type error handler
-
# Called when T.let, T.cast, T.must, etc. fail
-
11
T::Configuration.inline_type_error_handler = lambda do |error, _opts|
-
2
add_backtrace(error)
-
2
LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
-
end
-
-
# Install call validation error handler
-
# Called when method signature validation fails
-
11
T::Configuration.call_validation_error_handler = lambda do |_signature, opts|
-
10
error = TypeError.new(opts[:pretty_message])
-
10
add_backtrace(error)
-
10
LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
-
end
-
-
# Install sig builder error handler
-
# Called when there's a problem with a signature definition
-
11
T::Configuration.sig_builder_error_handler = lambda do |error, _location|
-
1
add_backtrace(error)
-
1
LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
-
end
-
-
# Install sig validation error handler
-
# Called when there's a problem with a signature validation
-
11
T::Configuration.sig_validation_error_handler = lambda do |error, _opts|
-
1
add_backtrace(error)
-
1
LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
-
end
-
-
11
true
-
end
-
-
# Starts the error's backtrace at the code that failed, below sorbet-runtime's frames (and this file's).
-
# sorbet-runtime passes some errors without raising them, which have no backtrace at all.
-
3
sig { params(error: Exception).void }
-
2
def self.add_backtrace(error)
-
14
frames = error.backtrace || caller
-
131
error.set_backtrace(frames.drop_while { |frame| frame.include?(__FILE__) || frame.include?("sorbet-runtime") })
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# Common enums and shared interfaces
-
2
require_relative "enums/source"
-
2
require_relative "enums/event"
-
2
require_relative "enums/level"
-
2
require_relative "enums/log_field"
-
2
require_relative "log/interfaces/public_common_fields"
-
2
require_relative "shared/serialize_common_public"
-
2
require_relative "params_errors"
-
-
# Dynamically require all top-level log structs under log/*
-
# Nested per-event files are required by their parent files.
-
2
Dir[File.join(__dir__, "log", "*.rb")].sort.each do |file|
-
32
require file
-
end
-
-
2
module LogStruct
-
2
module Log
-
2
extend T::Sig
-
-
# Build an Error log from an exception with optional context and timestamp
-
2
sig do
-
1
params(
-
source: Source,
-
ex: Exception,
-
additional_data: T::Hash[T.any(String, Symbol), T.untyped],
-
timestamp: Time
-
).returns(LogStruct::Log::Error)
-
end
-
2
def self.from_exception(source, ex, additional_data = {}, timestamp = Time.now)
-
21
LogStruct::Log::Error.new(
-
source: source,
-
error_class: ex.class,
-
# Without the input a params parser quoted, for a request's params error that the app logs itself
-
message: ParamsErrors.messages(ex).first,
-
backtrace: ex.backtrace,
-
additional_data: additional_data,
-
timestamp: timestamp
-
)
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/source_parent.rb.erb
-
-
2
require_relative "action_mailer/delivery"
-
2
require_relative "action_mailer/delivered"
-
2
require_relative "action_mailer/error"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActionMailer
-
2
class BaseFields < T::Struct
-
2
extend T::Sig
-
2
const :to, T.nilable(T::Array[String]), default: nil
-
2
const :from, T.nilable(String), default: nil
-
2
const :subject, T.nilable(String), default: nil
-
2
const :message_id, T.nilable(String), default: nil
-
2
const :mailer_class, T.nilable(String), default: nil
-
2
const :mailer_action, T.nilable(String), default: nil
-
2
const :attachment_count, T.nilable(Integer), default: nil
-
-
2
Kwargs = T.type_alias do
-
{
-
1
to: T.nilable(T::Array[String]),
-
from: T.nilable(String),
-
subject: T.nilable(String),
-
message_id: T.nilable(String),
-
mailer_class: T.nilable(String),
-
mailer_action: T.nilable(String),
-
attachment_count: T.nilable(Integer)
-
}
-
end
-
-
3
sig { returns(Kwargs) }
-
2
def to_kwargs
-
{
-
51
to: to,
-
from: from,
-
subject: subject,
-
message_id: message_id,
-
mailer_class: mailer_class,
-
mailer_action: mailer_action,
-
attachment_count: attachment_count
-
}
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActionMailer
-
2
class Delivered < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Mailer, default: Source::Mailer
-
2
const :event, Event, default: Event::Delivered
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :to, T.nilable(T::Array[String]), default: nil
-
2
const :from, T.nilable(String), default: nil
-
2
const :subject, T.nilable(String), default: nil
-
2
const :message_id, T.nilable(String), default: nil
-
2
const :mailer_class, T.nilable(String), default: nil
-
2
const :mailer_action, T.nilable(String), default: nil
-
2
const :attachment_count, T.nilable(Integer), default: nil
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
4
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
4
h[LogField::To] = to unless to.nil?
-
4
h[LogField::From] = from unless from.nil?
-
4
h[LogField::Subject] = subject unless subject.nil?
-
4
h[LogField::MessageId] = message_id unless message_id.nil?
-
4
h[LogField::MailerClass] = mailer_class unless mailer_class.nil?
-
4
h[LogField::MailerAction] = mailer_action unless mailer_action.nil?
-
4
h[LogField::AttachmentCount] = attachment_count unless attachment_count.nil?
-
4
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActionMailer
-
2
class Delivery < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Mailer, default: Source::Mailer
-
2
const :event, Event, default: Event::Delivery
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :to, T.nilable(T::Array[String]), default: nil
-
2
const :from, T.nilable(String), default: nil
-
2
const :subject, T.nilable(String), default: nil
-
2
const :message_id, T.nilable(String), default: nil
-
2
const :mailer_class, T.nilable(String), default: nil
-
2
const :mailer_action, T.nilable(String), default: nil
-
2
const :attachment_count, T.nilable(Integer), default: nil
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
42
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
42
h[LogField::To] = to unless to.nil?
-
42
h[LogField::From] = from unless from.nil?
-
42
h[LogField::Subject] = subject unless subject.nil?
-
42
h[LogField::MessageId] = message_id unless message_id.nil?
-
42
h[LogField::MailerClass] = mailer_class unless mailer_class.nil?
-
42
h[LogField::MailerAction] = mailer_action unless mailer_action.nil?
-
42
h[LogField::AttachmentCount] = attachment_count unless attachment_count.nil?
-
42
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActionMailer
-
2
class Error < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Mailer, default: Source::Mailer
-
2
const :event, Event, default: Event::Error
-
18
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :to, T.nilable(T::Array[String]), default: nil
-
2
const :from, T.nilable(String), default: nil
-
2
const :subject, T.nilable(String), default: nil
-
2
const :message_id, T.nilable(String), default: nil
-
2
const :mailer_class, T.nilable(String), default: nil
-
2
const :mailer_action, T.nilable(String), default: nil
-
2
const :attachment_count, T.nilable(Integer), default: nil
-
-
# Event-specific fields
-
2
const :error_class, T.class_of(StandardError)
-
2
const :message, String
-
2
const :backtrace, T.nilable(T::Array[String]), default: nil
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
54
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
54
h[LogField::To] = to unless to.nil?
-
54
h[LogField::From] = from unless from.nil?
-
54
h[LogField::Subject] = subject unless subject.nil?
-
54
h[LogField::MessageId] = message_id unless message_id.nil?
-
54
h[LogField::MailerClass] = mailer_class unless mailer_class.nil?
-
54
h[LogField::MailerAction] = mailer_action unless mailer_action.nil?
-
54
h[LogField::AttachmentCount] = attachment_count unless attachment_count.nil?
-
54
h[LogField::ErrorClass] = error_class
-
54
h[LogField::Message] = message
-
54
h[LogField::Backtrace] = backtrace unless backtrace.nil?
-
54
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/source_parent.rb.erb
-
-
2
require_relative "active_job/enqueue"
-
2
require_relative "active_job/schedule"
-
2
require_relative "active_job/start"
-
2
require_relative "active_job/finish"
-
2
require_relative "active_job/retry"
-
2
require_relative "active_job/retry_stopped"
-
2
require_relative "active_job/discard"
-
2
require_relative "active_job/error"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveJob
-
2
class BaseFields < T::Struct
-
2
extend T::Sig
-
2
const :job_id, String
-
2
const :job_class, String
-
2
const :queue_name, T.nilable(Symbol), default: nil
-
2
const :arguments, T.nilable(T::Array[T.untyped]), default: nil
-
2
const :executions, T.nilable(Integer), default: nil
-
2
const :provider_job_id, T.nilable(String), default: nil
-
-
2
Kwargs = T.type_alias do
-
{
-
1
job_id: String,
-
job_class: String,
-
queue_name: T.nilable(Symbol),
-
arguments: T.nilable(T::Array[T.untyped]),
-
executions: T.nilable(Integer),
-
provider_job_id: T.nilable(String)
-
}
-
end
-
-
3
sig { returns(Kwargs) }
-
2
def to_kwargs
-
{
-
56
job_id: job_id,
-
job_class: job_class,
-
queue_name: queue_name,
-
arguments: arguments,
-
executions: executions,
-
provider_job_id: provider_job_id
-
}
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveJob
-
2
class Discard < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Job, default: Source::Job
-
2
const :event, Event, default: Event::Discard
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :job_id, String
-
2
const :job_class, String
-
2
const :queue_name, T.nilable(Symbol), default: nil
-
2
const :arguments, T.nilable(T::Array[T.untyped]), default: nil
-
2
const :executions, T.nilable(Integer), default: nil
-
2
const :provider_job_id, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :error_class, T.class_of(Exception)
-
2
const :message, String
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
3
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
3
h[LogField::JobId] = job_id
-
3
h[LogField::JobClass] = job_class
-
3
h[LogField::QueueName] = queue_name unless queue_name.nil?
-
3
h[LogField::Arguments] = arguments unless arguments.nil?
-
3
h[LogField::Executions] = executions unless executions.nil?
-
3
h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
-
3
h[LogField::ErrorClass] = error_class
-
3
h[LogField::Message] = message
-
3
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveJob
-
2
class Enqueue < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Job, default: Source::Job
-
2
const :event, Event, default: Event::Enqueue
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :job_id, String
-
2
const :job_class, String
-
2
const :queue_name, T.nilable(Symbol), default: nil
-
2
const :arguments, T.nilable(T::Array[T.untyped]), default: nil
-
2
const :executions, T.nilable(Integer), default: nil
-
2
const :provider_job_id, T.nilable(String), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
13
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
13
h[LogField::JobId] = job_id
-
13
h[LogField::JobClass] = job_class
-
13
h[LogField::QueueName] = queue_name unless queue_name.nil?
-
13
h[LogField::Arguments] = arguments unless arguments.nil?
-
13
h[LogField::Executions] = executions unless executions.nil?
-
13
h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
-
13
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveJob
-
2
class Error < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Job, default: Source::Job
-
2
const :event, Event, default: Event::Error
-
16
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :job_id, String
-
2
const :job_class, String
-
2
const :queue_name, T.nilable(Symbol), default: nil
-
2
const :arguments, T.nilable(T::Array[T.untyped]), default: nil
-
2
const :executions, T.nilable(Integer), default: nil
-
2
const :provider_job_id, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :error_class, T.class_of(Exception)
-
2
const :message, String
-
2
const :backtrace, T.nilable(T::Array[String]), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
14
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
14
h[LogField::JobId] = job_id
-
14
h[LogField::JobClass] = job_class
-
14
h[LogField::QueueName] = queue_name unless queue_name.nil?
-
14
h[LogField::Arguments] = arguments unless arguments.nil?
-
14
h[LogField::Executions] = executions unless executions.nil?
-
14
h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
-
14
h[LogField::ErrorClass] = error_class
-
14
h[LogField::Message] = message
-
14
h[LogField::Backtrace] = backtrace unless backtrace.nil?
-
14
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveJob
-
2
class Finish < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Job, default: Source::Job
-
2
const :event, Event, default: Event::Finish
-
2
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :job_id, String
-
2
const :job_class, String
-
2
const :queue_name, T.nilable(Symbol), default: nil
-
2
const :arguments, T.nilable(T::Array[T.untyped]), default: nil
-
2
const :executions, T.nilable(Integer), default: nil
-
2
const :provider_job_id, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :duration_ms, Float
-
2
const :finished_at, Time
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
17
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
17
h[LogField::JobId] = job_id
-
17
h[LogField::JobClass] = job_class
-
17
h[LogField::QueueName] = queue_name unless queue_name.nil?
-
17
h[LogField::Arguments] = arguments unless arguments.nil?
-
17
h[LogField::Executions] = executions unless executions.nil?
-
17
h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
-
17
h[LogField::DurationMs] = duration_ms
-
17
h[LogField::FinishedAt] = finished_at
-
17
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveJob
-
2
class Retry < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Job, default: Source::Job
-
2
const :event, Event, default: Event::Retry
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :job_id, String
-
2
const :job_class, String
-
2
const :queue_name, T.nilable(Symbol), default: nil
-
2
const :arguments, T.nilable(T::Array[T.untyped]), default: nil
-
2
const :executions, T.nilable(Integer), default: nil
-
2
const :provider_job_id, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :error_class, T.nilable(T.class_of(Exception)), default: nil
-
2
const :message, T.nilable(String), default: nil
-
2
const :wait_ms, T.nilable(Float), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
3
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
3
h[LogField::JobId] = job_id
-
3
h[LogField::JobClass] = job_class
-
3
h[LogField::QueueName] = queue_name unless queue_name.nil?
-
3
h[LogField::Arguments] = arguments unless arguments.nil?
-
3
h[LogField::Executions] = executions unless executions.nil?
-
3
h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
-
3
h[LogField::ErrorClass] = error_class unless error_class.nil?
-
3
h[LogField::Message] = message unless message.nil?
-
3
h[LogField::WaitMs] = wait_ms unless wait_ms.nil?
-
3
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveJob
-
2
class RetryStopped < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Job, default: Source::Job
-
2
const :event, Event, default: Event::RetryStopped
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :job_id, String
-
2
const :job_class, String
-
2
const :queue_name, T.nilable(Symbol), default: nil
-
2
const :arguments, T.nilable(T::Array[T.untyped]), default: nil
-
2
const :executions, T.nilable(Integer), default: nil
-
2
const :provider_job_id, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :error_class, T.class_of(Exception)
-
2
const :message, String
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
3
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
3
h[LogField::JobId] = job_id
-
3
h[LogField::JobClass] = job_class
-
3
h[LogField::QueueName] = queue_name unless queue_name.nil?
-
3
h[LogField::Arguments] = arguments unless arguments.nil?
-
3
h[LogField::Executions] = executions unless executions.nil?
-
3
h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
-
3
h[LogField::ErrorClass] = error_class
-
3
h[LogField::Message] = message
-
3
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveJob
-
2
class Schedule < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Job, default: Source::Job
-
2
const :event, Event, default: Event::Schedule
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :job_id, String
-
2
const :job_class, String
-
2
const :queue_name, T.nilable(Symbol), default: nil
-
2
const :arguments, T.nilable(T::Array[T.untyped]), default: nil
-
2
const :executions, T.nilable(Integer), default: nil
-
2
const :provider_job_id, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :scheduled_at, Time
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
3
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
3
h[LogField::JobId] = job_id
-
3
h[LogField::JobClass] = job_class
-
3
h[LogField::QueueName] = queue_name unless queue_name.nil?
-
3
h[LogField::Arguments] = arguments unless arguments.nil?
-
3
h[LogField::Executions] = executions unless executions.nil?
-
3
h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
-
3
h[LogField::ScheduledAt] = scheduled_at
-
3
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveJob
-
2
class Start < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Job, default: Source::Job
-
2
const :event, Event, default: Event::Start
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :job_id, String
-
2
const :job_class, String
-
2
const :queue_name, T.nilable(Symbol), default: nil
-
2
const :arguments, T.nilable(T::Array[T.untyped]), default: nil
-
2
const :executions, T.nilable(Integer), default: nil
-
2
const :provider_job_id, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :started_at, Time
-
2
const :attempt, T.nilable(Integer), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
16
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
16
h[LogField::JobId] = job_id
-
16
h[LogField::JobClass] = job_class
-
16
h[LogField::QueueName] = queue_name unless queue_name.nil?
-
16
h[LogField::Arguments] = arguments unless arguments.nil?
-
16
h[LogField::Executions] = executions unless executions.nil?
-
16
h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
-
16
h[LogField::StartedAt] = started_at
-
16
h[LogField::Attempt] = attempt unless attempt.nil?
-
16
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../enums/source"
-
2
require_relative "../enums/event"
-
2
require_relative "../enums/level"
-
2
require_relative "../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveModelSerializers < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Rails, default: Source::Rails
-
2
const :event, Event, default: Event::Generate
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :message, String
-
2
const :serializer, T.nilable(String), default: nil
-
2
const :adapter, T.nilable(String), default: nil
-
2
const :resource_class, T.nilable(String), default: nil
-
2
const :duration_ms, Float
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
3
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
3
h[LogField::Message] = message
-
3
h[LogField::Serializer] = serializer unless serializer.nil?
-
3
h[LogField::Adapter] = adapter unless adapter.nil?
-
3
h[LogField::ResourceClass] = resource_class unless resource_class.nil?
-
3
h[LogField::DurationMs] = duration_ms
-
3
h
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/source_parent.rb.erb
-
-
2
require_relative "active_storage/upload"
-
2
require_relative "active_storage/download"
-
2
require_relative "active_storage/delete"
-
2
require_relative "active_storage/metadata"
-
2
require_relative "active_storage/exist"
-
2
require_relative "active_storage/url"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveStorage
-
2
class BaseFields < T::Struct
-
2
extend T::Sig
-
2
const :storage, Symbol
-
2
const :file_id, T.nilable(String), default: nil
-
-
2
Kwargs = T.type_alias do
-
{
-
storage: Symbol,
-
file_id: T.nilable(String)
-
}
-
end
-
-
2
sig { returns(Kwargs) }
-
2
def to_kwargs
-
{
-
storage: storage,
-
file_id: file_id
-
}
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveStorage
-
2
class Delete < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Storage, default: Source::Storage
-
2
const :event, Event, default: Event::Delete
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :storage, Symbol
-
2
const :file_id, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :prefix, T.nilable(String), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
2
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
2
h[LogField::Storage] = storage
-
2
h[LogField::FileId] = file_id unless file_id.nil?
-
2
h[LogField::Prefix] = prefix unless prefix.nil?
-
2
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveStorage
-
2
class Download < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Storage, default: Source::Storage
-
2
const :event, Event, default: Event::Download
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :storage, Symbol
-
2
const :file_id, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :range, T.nilable(String), default: nil
-
2
const :duration_ms, T.nilable(Float), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
2
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
2
h[LogField::Storage] = storage
-
2
h[LogField::FileId] = file_id unless file_id.nil?
-
2
h[LogField::Range] = range unless range.nil?
-
2
h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
-
2
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveStorage
-
2
class Exist < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Storage, default: Source::Storage
-
2
const :event, Event, default: Event::Exist
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :storage, Symbol
-
2
const :file_id, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :exist, T.nilable(T::Boolean), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
2
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
2
h[LogField::Storage] = storage
-
2
h[LogField::FileId] = file_id unless file_id.nil?
-
2
h[LogField::Exist] = exist unless exist.nil?
-
2
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveStorage
-
2
class Metadata < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Storage, default: Source::Storage
-
2
const :event, Event, default: Event::Metadata
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :storage, Symbol
-
2
const :file_id, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :mime_type, T.nilable(String), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
2
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
2
h[LogField::Storage] = storage
-
2
h[LogField::FileId] = file_id unless file_id.nil?
-
2
h[LogField::MimeType] = mime_type unless mime_type.nil?
-
2
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveStorage
-
2
class Upload < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Storage, default: Source::Storage
-
2
const :event, Event, default: Event::Upload
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :storage, Symbol
-
2
const :file_id, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :duration_ms, T.nilable(Float), default: nil
-
2
const :checksum, T.nilable(String), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
2
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
2
h[LogField::Storage] = storage
-
2
h[LogField::FileId] = file_id unless file_id.nil?
-
2
h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
-
2
h[LogField::Checksum] = checksum unless checksum.nil?
-
2
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class ActiveStorage
-
2
class Url < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Storage, default: Source::Storage
-
2
const :event, Event, default: Event::Url
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :storage, Symbol
-
2
const :file_id, T.nilable(String), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
2
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
2
h[LogField::Storage] = storage
-
2
h[LogField::FileId] = file_id unless file_id.nil?
-
2
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../enums/source"
-
2
require_relative "../enums/event"
-
2
require_relative "../enums/level"
-
2
require_relative "../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Ahoy < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::App, default: Source::App
-
2
const :event, Event, default: Event::Log
-
8
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :message, String
-
2
const :ahoy_event, String
-
2
const :properties, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
4
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
4
h[LogField::Message] = message
-
4
h[LogField::AhoyEvent] = ahoy_event
-
4
h[LogField::Properties] = properties unless properties.nil?
-
4
h
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/source_parent.rb.erb
-
-
2
require_relative "carrierwave/upload"
-
2
require_relative "carrierwave/delete"
-
2
require_relative "carrierwave/download"
-
-
2
module LogStruct
-
2
module Log
-
2
class CarrierWave
-
2
class BaseFields < T::Struct
-
2
extend T::Sig
-
2
const :storage, Symbol
-
2
const :file_id, String
-
2
const :uploader, T.nilable(String), default: nil
-
2
const :model, T.nilable(String), default: nil
-
2
const :mount_point, T.nilable(String), default: nil
-
2
const :version, T.nilable(String), default: nil
-
2
const :store_path, T.nilable(String), default: nil
-
2
const :extension, T.nilable(String), default: nil
-
-
2
Kwargs = T.type_alias do
-
{
-
storage: Symbol,
-
file_id: String,
-
uploader: T.nilable(String),
-
model: T.nilable(String),
-
mount_point: T.nilable(String),
-
version: T.nilable(String),
-
store_path: T.nilable(String),
-
extension: T.nilable(String)
-
}
-
end
-
-
2
sig { returns(Kwargs) }
-
2
def to_kwargs
-
{
-
storage: storage,
-
file_id: file_id,
-
uploader: uploader,
-
model: model,
-
mount_point: mount_point,
-
version: version,
-
store_path: store_path,
-
extension: extension
-
}
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class CarrierWave
-
2
class Delete < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::CarrierWave, default: Source::CarrierWave
-
2
const :event, Event, default: Event::Delete
-
5
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :storage, Symbol
-
2
const :file_id, String
-
2
const :uploader, T.nilable(String), default: nil
-
2
const :model, T.nilable(String), default: nil
-
2
const :mount_point, T.nilable(String), default: nil
-
2
const :version, T.nilable(String), default: nil
-
2
const :store_path, T.nilable(String), default: nil
-
2
const :extension, T.nilable(String), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
3
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
3
h[LogField::Storage] = storage
-
3
h[LogField::FileId] = file_id
-
3
h[LogField::Uploader] = uploader unless uploader.nil?
-
3
h[LogField::Model] = model unless model.nil?
-
3
h[LogField::MountPoint] = mount_point unless mount_point.nil?
-
3
h[LogField::Version] = version unless version.nil?
-
3
h[LogField::StorePath] = store_path unless store_path.nil?
-
3
h[LogField::Extension] = extension unless extension.nil?
-
3
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class CarrierWave
-
2
class Download < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::CarrierWave, default: Source::CarrierWave
-
2
const :event, Event, default: Event::Download
-
5
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :storage, Symbol
-
2
const :file_id, String
-
2
const :uploader, T.nilable(String), default: nil
-
2
const :model, T.nilable(String), default: nil
-
2
const :mount_point, T.nilable(String), default: nil
-
2
const :version, T.nilable(String), default: nil
-
2
const :store_path, T.nilable(String), default: nil
-
2
const :extension, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :filename, T.nilable(String), default: nil
-
2
const :mime_type, T.nilable(String), default: nil
-
2
const :size, T.nilable(Integer), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
3
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
3
h[LogField::Storage] = storage
-
3
h[LogField::FileId] = file_id
-
3
h[LogField::Uploader] = uploader unless uploader.nil?
-
3
h[LogField::Model] = model unless model.nil?
-
3
h[LogField::MountPoint] = mount_point unless mount_point.nil?
-
3
h[LogField::Version] = version unless version.nil?
-
3
h[LogField::StorePath] = store_path unless store_path.nil?
-
3
h[LogField::Extension] = extension unless extension.nil?
-
3
h[LogField::Filename] = filename unless filename.nil?
-
3
h[LogField::MimeType] = mime_type unless mime_type.nil?
-
3
h[LogField::Size] = size unless size.nil?
-
3
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class CarrierWave
-
2
class Upload < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::CarrierWave, default: Source::CarrierWave
-
2
const :event, Event, default: Event::Upload
-
8
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :storage, Symbol
-
2
const :file_id, String
-
2
const :uploader, T.nilable(String), default: nil
-
2
const :model, T.nilable(String), default: nil
-
2
const :mount_point, T.nilable(String), default: nil
-
2
const :version, T.nilable(String), default: nil
-
2
const :store_path, T.nilable(String), default: nil
-
2
const :extension, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :filename, T.nilable(String), default: nil
-
2
const :mime_type, T.nilable(String), default: nil
-
2
const :size, T.nilable(Integer), default: nil
-
2
const :metadata, T.nilable(T::Hash[String, T.untyped]), default: nil
-
2
const :duration_ms, T.nilable(Float), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
6
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
6
h[LogField::Storage] = storage
-
6
h[LogField::FileId] = file_id
-
6
h[LogField::Uploader] = uploader unless uploader.nil?
-
6
h[LogField::Model] = model unless model.nil?
-
6
h[LogField::MountPoint] = mount_point unless mount_point.nil?
-
6
h[LogField::Version] = version unless version.nil?
-
6
h[LogField::StorePath] = store_path unless store_path.nil?
-
6
h[LogField::Extension] = extension unless extension.nil?
-
6
h[LogField::Filename] = filename unless filename.nil?
-
6
h[LogField::MimeType] = mime_type unless mime_type.nil?
-
6
h[LogField::Size] = size unless size.nil?
-
6
h[LogField::Metadata] = metadata unless metadata.nil?
-
6
h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
-
6
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/source_parent.rb.erb
-
-
2
require_relative "dotenv/load"
-
2
require_relative "dotenv/update"
-
2
require_relative "dotenv/save"
-
2
require_relative "dotenv/restore"
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Dotenv
-
2
class Load < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Dotenv, default: Source::Dotenv
-
2
const :event, Event, default: Event::Load
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :file, String
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
4
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
4
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
4
h[LogField::File] = file
-
4
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Dotenv
-
2
class Restore < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Dotenv, default: Source::Dotenv
-
2
const :event, Event, default: Event::Restore
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :vars, T::Array[String]
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
2
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
2
h[LogField::Vars] = vars
-
2
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Dotenv
-
2
class Save < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Dotenv, default: Source::Dotenv
-
2
const :event, Event, default: Event::Save
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :snapshot, T::Boolean
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
4
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
4
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
4
h[LogField::Snapshot] = snapshot
-
4
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Dotenv
-
2
class Update < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Dotenv, default: Source::Dotenv
-
2
const :event, Event, default: Event::Update
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :vars, T::Array[String]
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
4
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
4
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
4
h[LogField::Vars] = vars
-
4
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../enums/source"
-
2
require_relative "../enums/event"
-
2
require_relative "../enums/level"
-
2
require_relative "../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Error < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source
-
2
const :event, Event, default: Event::Error
-
20
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :path, T.nilable(String), default: nil
-
2
const :http_method, T.nilable(String), default: nil
-
2
const :source_ip, T.nilable(String), default: nil
-
2
const :user_agent, T.nilable(String), default: nil
-
2
const :referer, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :error_class, T.class_of(Exception)
-
2
const :message, String
-
2
const :error_message, T.nilable(String), default: nil
-
2
const :backtrace, T.nilable(T::Array[String]), default: nil
-
2
const :status, T.nilable(Integer), default: nil
-
2
const :cause_class, T.nilable(String), default: nil
-
2
const :cause_message, T.nilable(String), default: nil
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Request fields (optional)
-
2
include LogStruct::Log::Interfaces::RequestFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
2
include LogStruct::Log::Shared::AddRequestFields
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
36
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
36
h[LogField::Path] = path unless path.nil?
-
36
h[LogField::HttpMethod] = http_method unless http_method.nil?
-
36
h[LogField::SourceIp] = source_ip unless source_ip.nil?
-
36
h[LogField::UserAgent] = user_agent unless user_agent.nil?
-
36
h[LogField::Referer] = referer unless referer.nil?
-
36
h[LogField::ErrorClass] = error_class
-
36
h[LogField::Message] = message
-
36
h[LogField::ErrorMessage] = error_message unless error_message.nil?
-
36
h[LogField::Backtrace] = backtrace unless backtrace.nil?
-
36
h[LogField::Status] = status unless status.nil?
-
36
h[LogField::CauseClass] = cause_class unless cause_class.nil?
-
36
h[LogField::CauseMessage] = cause_message unless cause_message.nil?
-
36
h
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/source_parent.rb.erb
-
-
2
require_relative "good_job/log"
-
2
require_relative "good_job/error"
-
-
2
module LogStruct
-
2
module Log
-
2
class GoodJob
-
2
class BaseFields < T::Struct
-
2
extend T::Sig
-
2
const :job_id, T.nilable(String), default: nil
-
2
const :job_class, T.nilable(String), default: nil
-
2
const :queue_name, T.nilable(Symbol), default: nil
-
2
const :arguments, T.nilable(T::Array[T.untyped]), default: nil
-
2
const :executions, T.nilable(Integer), default: nil
-
-
2
Kwargs = T.type_alias do
-
{
-
job_id: T.nilable(String),
-
job_class: T.nilable(String),
-
queue_name: T.nilable(Symbol),
-
arguments: T.nilable(T::Array[T.untyped]),
-
executions: T.nilable(Integer)
-
}
-
end
-
-
2
sig { returns(Kwargs) }
-
2
def to_kwargs
-
{
-
job_id: job_id,
-
job_class: job_class,
-
queue_name: queue_name,
-
arguments: arguments,
-
executions: executions
-
}
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class GoodJob
-
2
class Error < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Job, default: Source::Job
-
2
const :event, Event, default: Event::Error
-
12
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :job_id, T.nilable(String), default: nil
-
2
const :job_class, T.nilable(String), default: nil
-
2
const :queue_name, T.nilable(Symbol), default: nil
-
2
const :arguments, T.nilable(T::Array[T.untyped]), default: nil
-
2
const :executions, T.nilable(Integer), default: nil
-
-
# Event-specific fields
-
2
const :error_class, String
-
2
const :error_message, String
-
2
const :duration_ms, T.nilable(Float), default: nil
-
2
const :process_id, Integer
-
2
const :thread_id, String
-
2
const :exception_executions, T.nilable(Integer), default: nil
-
2
const :backtrace, T.nilable(T::Array[String]), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
5
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
5
h[LogField::JobId] = job_id unless job_id.nil?
-
5
h[LogField::JobClass] = job_class unless job_class.nil?
-
5
h[LogField::QueueName] = queue_name unless queue_name.nil?
-
5
h[LogField::Arguments] = arguments unless arguments.nil?
-
5
h[LogField::Executions] = executions unless executions.nil?
-
5
h[LogField::ErrorClass] = error_class
-
5
h[LogField::ErrorMessage] = error_message
-
5
h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
-
5
h[LogField::ProcessId] = process_id
-
5
h[LogField::ThreadId] = thread_id
-
5
h[LogField::ExceptionExecutions] = exception_executions unless exception_executions.nil?
-
5
h[LogField::Backtrace] = backtrace unless backtrace.nil?
-
5
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class GoodJob
-
2
class Log < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Job, default: Source::Job
-
2
const :event, Event, default: Event::Log
-
24
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :job_id, T.nilable(String), default: nil
-
2
const :job_class, T.nilable(String), default: nil
-
2
const :queue_name, T.nilable(Symbol), default: nil
-
2
const :arguments, T.nilable(T::Array[T.untyped]), default: nil
-
2
const :executions, T.nilable(Integer), default: nil
-
-
# Event-specific fields
-
2
const :message, String
-
2
const :process_id, Integer
-
2
const :thread_id, String
-
2
const :scheduled_at, T.nilable(Time), default: nil
-
2
const :priority, T.nilable(Integer), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
17
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
17
h[LogField::JobId] = job_id unless job_id.nil?
-
17
h[LogField::JobClass] = job_class unless job_class.nil?
-
17
h[LogField::QueueName] = queue_name unless queue_name.nil?
-
17
h[LogField::Arguments] = arguments unless arguments.nil?
-
17
h[LogField::Executions] = executions unless executions.nil?
-
17
h[LogField::Message] = message
-
17
h[LogField::ProcessId] = process_id
-
17
h[LogField::ThreadId] = thread_id
-
17
h[LogField::ScheduledAt] = scheduled_at unless scheduled_at.nil?
-
17
h[LogField::Priority] = priority unless priority.nil?
-
17
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "log_struct/shared/interfaces/public_common_fields"
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../enums/source"
-
2
require_relative "../enums/event"
-
2
require_relative "../enums/level"
-
2
require_relative "../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Plain < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::App, default: Source::App
-
2
const :event, Event, default: Event::Log
-
27
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :message, T.untyped
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
4
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
1385
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
1385
h[LogField::Message] = message
-
1385
h
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/source_parent.rb.erb
-
-
2
require_relative "puma/start"
-
2
require_relative "puma/shutdown"
-
2
require_relative "puma/error"
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Puma
-
2
class Error < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Puma, default: Source::Puma
-
2
const :event, Event, default: Event::Error
-
6
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :error_class, T.class_of(Exception)
-
2
const :message, String
-
2
const :backtrace, T.nilable(T::Array[String]), default: nil
-
2
const :error_context, T.nilable(String), default: nil
-
2
const :http_method, T.nilable(String), default: nil
-
2
const :path, T.nilable(String), default: nil
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
4
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
4
h[LogField::ErrorClass] = error_class
-
4
h[LogField::Message] = message
-
4
h[LogField::Backtrace] = backtrace unless backtrace.nil?
-
4
h[LogField::ErrorContext] = error_context unless error_context.nil?
-
4
h[LogField::HttpMethod] = http_method unless http_method.nil?
-
4
h[LogField::Path] = path unless path.nil?
-
4
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Puma
-
2
class Shutdown < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Puma, default: Source::Puma
-
2
const :event, Event, default: Event::Shutdown
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :process_id, T.nilable(Integer), default: nil
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
2
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
2
h[LogField::ProcessId] = process_id unless process_id.nil?
-
2
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Puma
-
2
class Start < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Puma, default: Source::Puma
-
2
const :event, Event, default: Event::Start
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :mode, T.nilable(String), default: nil
-
2
const :puma_version, T.nilable(String), default: nil
-
2
const :puma_codename, T.nilable(String), default: nil
-
2
const :ruby_version, T.nilable(String), default: nil
-
2
const :min_threads, T.nilable(Integer), default: nil
-
2
const :max_threads, T.nilable(Integer), default: nil
-
2
const :environment, T.nilable(String), default: nil
-
2
const :process_id, T.nilable(Integer), default: nil
-
2
const :listening_addresses, T.nilable(T::Array[String]), default: nil
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
2
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
2
h[LogField::Mode] = mode unless mode.nil?
-
2
h[LogField::PumaVersion] = puma_version unless puma_version.nil?
-
2
h[LogField::PumaCodename] = puma_codename unless puma_codename.nil?
-
2
h[LogField::RubyVersion] = ruby_version unless ruby_version.nil?
-
2
h[LogField::MinThreads] = min_threads unless min_threads.nil?
-
2
h[LogField::MaxThreads] = max_threads unless max_threads.nil?
-
2
h[LogField::Environment] = environment unless environment.nil?
-
2
h[LogField::ProcessId] = process_id unless process_id.nil?
-
2
h[LogField::ListeningAddresses] = listening_addresses unless listening_addresses.nil?
-
2
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../enums/source"
-
2
require_relative "../enums/event"
-
2
require_relative "../enums/level"
-
2
require_relative "../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Request < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Rails, default: Source::Rails
-
2
const :event, Event, default: Event::Request
-
7
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :path, T.nilable(String), default: nil
-
2
const :http_method, T.nilable(String), default: nil
-
2
const :source_ip, T.nilable(String), default: nil
-
2
const :user_agent, T.nilable(String), default: nil
-
2
const :referer, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :format, T.nilable(Symbol), default: nil
-
2
const :controller, T.nilable(String), default: nil
-
2
const :action, T.nilable(String), default: nil
-
2
const :status, T.nilable(Integer), default: nil
-
2
const :duration_ms, T.nilable(Float), default: nil
-
2
const :view, T.nilable(Float), default: nil
-
2
const :database, T.nilable(Float), default: nil
-
2
const :params, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
-
2
const :host, T.nilable(String), default: nil
-
2
const :content_type, T.nilable(String), default: nil
-
2
const :accept, T.nilable(String), default: nil
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Request fields (optional)
-
2
include LogStruct::Log::Interfaces::RequestFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
2
include LogStruct::Log::Shared::AddRequestFields
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
8
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
8
h[LogField::Path] = path unless path.nil?
-
8
h[LogField::HttpMethod] = http_method unless http_method.nil?
-
8
h[LogField::SourceIp] = source_ip unless source_ip.nil?
-
8
h[LogField::UserAgent] = user_agent unless user_agent.nil?
-
8
h[LogField::Referer] = referer unless referer.nil?
-
8
h[LogField::Format] = format unless format.nil?
-
8
h[LogField::Controller] = controller unless controller.nil?
-
8
h[LogField::Action] = action unless action.nil?
-
8
h[LogField::Status] = status unless status.nil?
-
8
h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
-
8
h[LogField::View] = view unless view.nil?
-
8
h[LogField::Database] = database unless database.nil?
-
8
h[LogField::Params] = params unless params.nil?
-
8
h[LogField::Host] = host unless host.nil?
-
8
h[LogField::ContentType] = content_type unless content_type.nil?
-
8
h[LogField::Accept] = accept unless accept.nil?
-
8
h
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/source_parent.rb.erb
-
-
2
require_relative "security/ip_spoof"
-
2
require_relative "security/csrf_violation"
-
2
require_relative "security/blocked_host"
-
-
2
module LogStruct
-
2
module Log
-
2
class Security
-
2
class BaseFields < T::Struct
-
2
extend T::Sig
-
2
const :path, T.nilable(String), default: nil
-
2
const :http_method, T.nilable(String), default: nil
-
2
const :source_ip, T.nilable(String), default: nil
-
2
const :user_agent, T.nilable(String), default: nil
-
2
const :referer, T.nilable(String), default: nil
-
-
2
Kwargs = T.type_alias do
-
{
-
path: T.nilable(String),
-
http_method: T.nilable(String),
-
source_ip: T.nilable(String),
-
user_agent: T.nilable(String),
-
referer: T.nilable(String)
-
}
-
end
-
-
2
sig { returns(Kwargs) }
-
2
def to_kwargs
-
{
-
path: path,
-
http_method: http_method,
-
source_ip: source_ip,
-
user_agent: user_agent,
-
referer: referer
-
}
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Security
-
2
class BlockedHost < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Security, default: Source::Security
-
2
const :event, Event, default: Event::BlockedHost
-
8
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :path, T.nilable(String), default: nil
-
2
const :http_method, T.nilable(String), default: nil
-
2
const :source_ip, T.nilable(String), default: nil
-
2
const :user_agent, T.nilable(String), default: nil
-
2
const :referer, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :message, T.nilable(String), default: nil
-
2
const :blocked_host, T.nilable(String), default: nil
-
2
const :blocked_hosts, T.nilable(T::Array[String]), default: nil
-
2
const :x_forwarded_for, T.nilable(String), default: nil
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Request fields (optional)
-
2
include LogStruct::Log::Interfaces::RequestFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
2
include LogStruct::Log::Shared::AddRequestFields
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
3
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
3
h[LogField::Path] = path unless path.nil?
-
3
h[LogField::HttpMethod] = http_method unless http_method.nil?
-
3
h[LogField::SourceIp] = source_ip unless source_ip.nil?
-
3
h[LogField::UserAgent] = user_agent unless user_agent.nil?
-
3
h[LogField::Referer] = referer unless referer.nil?
-
3
h[LogField::Message] = message unless message.nil?
-
3
h[LogField::BlockedHost] = blocked_host unless blocked_host.nil?
-
3
h[LogField::BlockedHosts] = blocked_hosts unless blocked_hosts.nil?
-
3
h[LogField::XForwardedFor] = x_forwarded_for unless x_forwarded_for.nil?
-
3
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Security
-
2
class CSRFViolation < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Security, default: Source::Security
-
2
const :event, Event, default: Event::CSRFViolation
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :path, T.nilable(String), default: nil
-
2
const :http_method, T.nilable(String), default: nil
-
2
const :source_ip, T.nilable(String), default: nil
-
2
const :user_agent, T.nilable(String), default: nil
-
2
const :referer, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :message, T.nilable(String), default: nil
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Request fields (optional)
-
2
include LogStruct::Log::Interfaces::RequestFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
2
include LogStruct::Log::Shared::AddRequestFields
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
4
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
4
h[LogField::Path] = path unless path.nil?
-
4
h[LogField::HttpMethod] = http_method unless http_method.nil?
-
4
h[LogField::SourceIp] = source_ip unless source_ip.nil?
-
4
h[LogField::UserAgent] = user_agent unless user_agent.nil?
-
4
h[LogField::Referer] = referer unless referer.nil?
-
4
h[LogField::Message] = message unless message.nil?
-
4
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Security
-
2
class IPSpoof < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Security, default: Source::Security
-
2
const :event, Event, default: Event::IPSpoof
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
2
const :path, T.nilable(String), default: nil
-
2
const :http_method, T.nilable(String), default: nil
-
2
const :source_ip, T.nilable(String), default: nil
-
2
const :user_agent, T.nilable(String), default: nil
-
2
const :referer, T.nilable(String), default: nil
-
-
# Event-specific fields
-
2
const :message, T.nilable(String), default: nil
-
2
const :client_ip, T.nilable(String), default: nil
-
2
const :x_forwarded_for, T.nilable(String), default: nil
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Request fields (optional)
-
2
include LogStruct::Log::Interfaces::RequestFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
2
include LogStruct::Log::Shared::AddRequestFields
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
2
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
2
h[LogField::Path] = path unless path.nil?
-
2
h[LogField::HttpMethod] = http_method unless http_method.nil?
-
2
h[LogField::SourceIp] = source_ip unless source_ip.nil?
-
2
h[LogField::UserAgent] = user_agent unless user_agent.nil?
-
2
h[LogField::Referer] = referer unless referer.nil?
-
2
h[LogField::Message] = message unless message.nil?
-
2
h[LogField::ClientIp] = client_ip unless client_ip.nil?
-
2
h[LogField::XForwardedFor] = x_forwarded_for unless x_forwarded_for.nil?
-
2
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/source_parent.rb.erb
-
-
2
require_relative "shrine/upload"
-
2
require_relative "shrine/download"
-
2
require_relative "shrine/delete"
-
2
require_relative "shrine/metadata"
-
2
require_relative "shrine/exist"
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Shrine
-
2
class Delete < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Shrine, default: Source::Shrine
-
2
const :event, Event, default: Event::Delete
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :storage, Symbol
-
2
const :location, String
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
2
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
2
h[LogField::Storage] = storage
-
2
h[LogField::Location] = location
-
2
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Shrine
-
2
class Download < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Shrine, default: Source::Shrine
-
2
const :event, Event, default: Event::Download
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :storage, Symbol
-
2
const :location, String
-
2
const :download_options, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
2
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
2
h[LogField::Storage] = storage
-
2
h[LogField::Location] = location
-
2
h[LogField::DownloadOptions] = download_options unless download_options.nil?
-
2
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Shrine
-
2
class Exist < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Shrine, default: Source::Shrine
-
2
const :event, Event, default: Event::Exist
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :storage, Symbol
-
2
const :location, String
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
2
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
2
h[LogField::Storage] = storage
-
2
h[LogField::Location] = location
-
2
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Shrine
-
2
class Metadata < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Shrine, default: Source::Shrine
-
2
const :event, Event, default: Event::Metadata
-
4
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :storage, Symbol
-
2
const :duration_ms, T.nilable(Float), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
2
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
2
h[LogField::Storage] = storage
-
2
h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
-
2
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Shrine
-
2
class Upload < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Shrine, default: Source::Shrine
-
2
const :event, Event, default: Event::Upload
-
5
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :storage, Symbol
-
2
const :location, String
-
2
const :mime_type, T.nilable(String), default: nil
-
2
const :size, T.nilable(Integer), default: nil
-
2
const :upload_options, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
-
2
const :options, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
-
2
const :uploader, T.nilable(String), default: nil
-
2
const :duration_ms, T.nilable(Float), default: nil
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
3
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
3
h[LogField::Storage] = storage
-
3
h[LogField::Location] = location
-
3
h[LogField::MimeType] = mime_type unless mime_type.nil?
-
3
h[LogField::Size] = size unless size.nil?
-
3
h[LogField::UploadOptions] = upload_options unless upload_options.nil?
-
3
h[LogField::Options] = options unless options.nil?
-
3
h[LogField::Uploader] = uploader unless uploader.nil?
-
3
h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
-
3
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/source_parent.rb.erb
-
-
2
require_relative "sidekiq/log"
-
2
require_relative "sidekiq/error"
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Sidekiq
-
2
class Error < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Sidekiq, default: Source::Sidekiq
-
2
const :event, Event, default: Event::Error
-
6
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :error_class, T.class_of(Exception)
-
2
const :message, String
-
2
const :backtrace, T.nilable(T::Array[String]), default: nil
-
2
const :error_context, T.nilable(String), default: nil
-
2
const :queue_name, T.nilable(String), default: nil
-
2
const :retry_count, T.nilable(Integer), default: nil
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
4
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
4
h[LogField::ErrorClass] = error_class
-
4
h[LogField::Message] = message
-
4
h[LogField::Backtrace] = backtrace unless backtrace.nil?
-
4
h[LogField::ErrorContext] = error_context unless error_context.nil?
-
4
h[LogField::QueueName] = queue_name unless queue_name.nil?
-
4
h[LogField::RetryCount] = retry_count unless retry_count.nil?
-
4
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "../../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class Sidekiq
-
2
class Log < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::Sidekiq, default: Source::Sidekiq
-
2
const :event, Event, default: Event::Log
-
7
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :message, T.nilable(String), default: nil
-
2
const :context, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
-
2
const :process_id, T.nilable(Integer), default: nil
-
2
const :thread_id, T.nilable(T.any(Integer, String)), default: nil
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
5
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
5
h[LogField::Message] = message unless message.nil?
-
5
h[LogField::Context] = context unless context.nil?
-
5
h[LogField::ProcessId] = process_id unless process_id.nil?
-
5
h[LogField::ThreadId] = thread_id unless thread_id.nil?
-
5
h
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# AUTO-GENERATED: DO NOT EDIT
-
# Generated by scripts/generate_structs.rb
-
# Schemas dir: schemas/log_sources/
-
# Template: tools/codegen/templates/sorbet/event.rb.erb
-
-
2
require "log_struct/shared/interfaces/common_fields"
-
2
require "log_struct/shared/interfaces/additional_data_field"
-
2
require "log_struct/shared/interfaces/request_fields"
-
2
require "log_struct/shared/serialize_common"
-
2
require "log_struct/shared/merge_additional_data_fields"
-
2
require "log_struct/shared/add_request_fields"
-
2
require_relative "../enums/source"
-
2
require_relative "../enums/event"
-
2
require_relative "../enums/level"
-
2
require_relative "../enums/log_field"
-
-
2
module LogStruct
-
2
module Log
-
2
class SQL < T::Struct
-
2
extend T::Sig
-
-
# Shared/common fields
-
2
const :source, Source::App, default: Source::App
-
2
const :event, Event, default: Event::Database
-
43
const :timestamp, Time, factory: -> { Time.now }
-
2
const :level, Level, default: Level::Info
-
-
# Event-specific fields
-
2
const :message, String
-
2
const :sql, String
-
2
const :name, String
-
2
const :duration_ms, Float
-
2
const :row_count, T.nilable(Integer), default: nil
-
2
const :adapter, T.nilable(String), default: nil
-
2
const :bind_params, T.nilable(T::Array[T.untyped]), default: nil
-
2
const :database_name, T.nilable(String), default: nil
-
2
const :connection_pool_size, T.nilable(Integer), default: nil
-
2
const :active_connections, T.nilable(Integer), default: nil
-
2
const :operation_type, T.nilable(String), default: nil
-
2
const :table_names, T.nilable(T::Array[String]), default: nil
-
-
# Additional data
-
2
include LogStruct::Log::Interfaces::AdditionalDataField
-
2
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
-
2
include LogStruct::Log::Shared::MergeAdditionalDataFields
-
-
# Serialize shared fields
-
2
include LogStruct::Log::Interfaces::CommonFields
-
2
include LogStruct::Log::Shared::SerializeCommon
-
-
3
sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
-
2
def to_h
-
13
h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
-
13
h[LogField::Message] = message
-
13
h[LogField::Sql] = sql
-
13
h[LogField::Name] = name
-
13
h[LogField::DurationMs] = duration_ms
-
13
h[LogField::RowCount] = row_count unless row_count.nil?
-
13
h[LogField::Adapter] = adapter unless adapter.nil?
-
13
h[LogField::BindParams] = bind_params unless bind_params.nil?
-
13
h[LogField::DatabaseName] = database_name unless database_name.nil?
-
13
h[LogField::ConnectionPoolSize] = connection_pool_size unless connection_pool_size.nil?
-
13
h[LogField::ActiveConnections] = active_connections unless active_connections.nil?
-
13
h[LogField::OperationType] = operation_type unless operation_type.nil?
-
13
h[LogField::TableNames] = table_names unless table_names.nil?
-
13
h
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
# Exceptions LogStruct has already logged as a structured line, so an integration that sees the same
-
# exception again doesn't log it twice. GoodJob passes a failed job's exception to on_thread_error after
-
# the ActiveJob integration has logged it, for example. Exceptions are held weakly, so a mark doesn't keep
-
# one alive.
-
2
module LoggedErrors
-
2
extend T::Sig
-
-
2
@logged = T.let(ObjectSpace::WeakMap.new, ObjectSpace::WeakMap)
-
2
@mutex = T.let(Mutex.new, Mutex)
-
-
3
sig { params(error: Exception).void }
-
2
def self.mark(error)
-
128
@mutex.synchronize { @logged[error] = true }
-
end
-
-
3
sig { params(error: Exception).returns(T::Boolean) }
-
2
def self.logged?(error)
-
84
@mutex.synchronize { @logged.key?(error) }
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "active_support/tagged_logging"
-
-
# Monkey-patch ActiveSupport::TaggedLogging::Formatter to work with structured logging.
-
#
-
# Problem: Rails' TaggedLogging prepends tags as text and converts messages to strings.
-
# When we pass a hash to super(), Rails does "#{tags_text}#{msg}" which calls .to_s
-
# on the hash, producing Ruby inspect format {message: "..."} instead of valid JSON.
-
#
-
# Solution: When LogStruct is enabled, we add tags as a hash entry and delegate to
-
# LogStruct::Formatter for proper JSON serialization with filtering and standard fields.
-
2
module ActiveSupport
-
2
module TaggedLogging
-
2
extend T::Sig
-
-
# Add class-level current_tags method for compatibility with Rails code
-
2
sig { returns(T::Array[T.any(String, Symbol)]) }
-
2
def self.current_tags
-
Thread.current[:activesupport_tagged_logging_tags] || []
-
end
-
-
2
module FormatterExtension
-
2
extend T::Sig
-
2
extend T::Helpers
-
2
requires_ancestor { ::ActiveSupport::TaggedLogging::Formatter }
-
-
3
sig { params(severity: T.any(String, Symbol), time: Time, progname: T.untyped, data: T.untyped).returns(String) }
-
2
def call(severity, time, progname, data)
-
# Preserve original Rails behavior when LogStruct is disabled
-
14
return super unless ::LogStruct.enabled?
-
-
12
tags = current_tags
-
-
# Add tags to data as hash entry (not text prefix)
-
12
data_with_tags = if data.is_a?(Hash)
-
1
tags.present? ? data.merge(tags: tags) : data
-
11
elsif data.is_a?(::LogStruct::Log::Interfaces::CommonFields) || (data.is_a?(T::Struct) && data.respond_to?(:serialize))
-
2
hash = data.serialize
-
2
tags.present? ? hash.merge(tags: tags) : hash
-
else
-
9
base = {LogStruct::LogField::Message.serialize => data.to_s}
-
9
tags.present? ? base.merge(tags: tags) : base
-
end
-
-
# Delegate to LogStruct::Formatter for JSON serialization with filtering
-
12
logstruct_formatter.call(severity, time, progname, data_with_tags)
-
end
-
-
2
private
-
-
3
sig { returns(::LogStruct::Formatter) }
-
2
def logstruct_formatter
-
12
@logstruct_formatter ||= T.let(::LogStruct::Formatter.new, T.nilable(::LogStruct::Formatter))
-
end
-
end
-
end
-
end
-
-
2
ActiveSupport::TaggedLogging::Formatter.prepend(ActiveSupport::TaggedLogging::FormatterExtension)
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "enums/error_reporter"
-
2
require_relative "handlers"
-
-
# Try to require all supported error reporting libraries
-
# Users may have multiple installed, so we should load all of them
-
2
%w[sentry-ruby bugsnag rollbar honeybadger].each do |gem_name|
-
8
require gem_name
-
rescue LoadError
-
# If a particular gem is not available, we'll still load the others
-
end
-
-
2
module LogStruct
-
# MultiErrorReporter provides a unified interface for reporting errors to various services.
-
# You can also override this with your own error reporter by setting
-
# LogStruct#.config.error_reporting_handler
-
# NOTE: This is used for cases where an error should be reported
-
# but the operation should be allowed to continue (e.g. scrubbing log data.)
-
2
class MultiErrorReporter
-
# Class variable to store the selected reporter
-
2
class CallableReporterWrapper
-
2
extend T::Sig
-
-
3
sig { params(callable: T.untyped).void }
-
2
def initialize(callable)
-
2
@callable = callable
-
end
-
-
3
sig { returns(T.untyped) }
-
2
attr_reader :callable
-
2
alias_method :original, :callable
-
-
3
sig { params(error: StandardError, context: T.nilable(T::Hash[Symbol, T.untyped]), source: Source).void }
-
2
def call(error, context, source)
-
2
case callable_arity
-
when 3
-
1
callable.call(error, context, source)
-
when 2
-
1
callable.call(error, context)
-
when 1
-
callable.call(error)
-
else
-
callable.call(error, context, source)
-
end
-
end
-
-
2
private
-
-
3
sig { returns(Integer) }
-
2
def callable_arity
-
2
callable.respond_to?(:arity) ? callable.arity : -1
-
end
-
end
-
-
4
ReporterImpl = T.type_alias { T.any(ErrorReporter, CallableReporterWrapper) }
-
-
2
@reporter_impl = T.let(nil, T.nilable(ReporterImpl))
-
-
2
class << self
-
2
extend T::Sig
-
-
3
sig { returns(ReporterImpl) }
-
2
def reporter
-
8
reporter_impl
-
end
-
-
# Set the reporter to use (user-friendly API that accepts symbols)
-
3
sig { params(reporter_type: T.any(ErrorReporter, Symbol, Handlers::ErrorReporter)).returns(ReporterImpl) }
-
2
def reporter=(reporter_type)
-
8
@reporter_impl = case reporter_type
-
when ErrorReporter
-
reporter_type
-
when Symbol
-
6
resolve_symbol_reporter(reporter_type)
-
else
-
2
wrap_callable_reporter(reporter_type)
-
end
-
end
-
-
# Auto-detect which error reporting service to use
-
3
sig { returns(ErrorReporter) }
-
2
def detect_reporter
-
1
if defined?(::Sentry)
-
1
ErrorReporter::Sentry
-
elsif defined?(::Bugsnag)
-
ErrorReporter::Bugsnag
-
elsif defined?(::Rollbar)
-
ErrorReporter::Rollbar
-
elsif defined?(::Honeybadger)
-
ErrorReporter::Honeybadger
-
else
-
ErrorReporter::RailsLogger
-
end
-
end
-
-
# Report an error to the configured error reporting service
-
3
sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
-
2
def report_error(error, context = {})
-
# Call the appropriate reporter method based on what's available
-
8
impl = reporter_impl
-
-
8
case impl
-
when ErrorReporter::Sentry
-
2
report_to_sentry(error, context)
-
when ErrorReporter::Bugsnag
-
1
report_to_bugsnag(error, context)
-
when ErrorReporter::Rollbar
-
1
report_to_rollbar(error, context)
-
when ErrorReporter::Honeybadger
-
1
report_to_honeybadger(error, context)
-
when ErrorReporter::RailsLogger
-
1
fallback_logging(error, context)
-
when CallableReporterWrapper
-
2
impl.call(error, context, Source::Internal)
-
end
-
end
-
-
2
private
-
-
3
sig { returns(ReporterImpl) }
-
2
def reporter_impl
-
16
@reporter_impl ||= detect_reporter
-
end
-
-
3
sig { params(symbol: Symbol).returns(ErrorReporter) }
-
2
def resolve_symbol_reporter(symbol)
-
6
case symbol
-
1
when :sentry then ErrorReporter::Sentry
-
2
when :bugsnag then ErrorReporter::Bugsnag
-
1
when :rollbar then ErrorReporter::Rollbar
-
1
when :honeybadger then ErrorReporter::Honeybadger
-
1
when :rails_logger then ErrorReporter::RailsLogger
-
else
-
valid_types = ErrorReporter.values.map { |v| ":#{v.serialize}" }.join(", ")
-
raise ArgumentError, "Unknown reporter type: #{symbol}. Valid types are: #{valid_types}"
-
end
-
end
-
-
3
sig { params(callable: T.untyped).returns(CallableReporterWrapper) }
-
2
def wrap_callable_reporter(callable)
-
2
unless callable.respond_to?(:call)
-
raise ArgumentError, "Reporter must respond to #call"
-
end
-
-
2
CallableReporterWrapper.new(callable)
-
end
-
-
# Report to Sentry
-
3
sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
-
2
def report_to_sentry(error, context = {})
-
2
return unless defined?(::Sentry)
-
-
# Use the proper Sentry interface defined in the RBI
-
2
::Sentry.capture_exception(error, extra: context)
-
rescue => e
-
1
fallback_logging(e, {original_error: error.class.to_s})
-
end
-
-
# Report to Bugsnag
-
3
sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
-
2
def report_to_bugsnag(error, context = {})
-
1
return unless defined?(::Bugsnag)
-
-
1
::Bugsnag.notify(error) do |report|
-
1
report.add_metadata(:context, context)
-
end
-
rescue => e
-
fallback_logging(e, {original_error: error.class.to_s})
-
end
-
-
# Report to Rollbar
-
3
sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
-
2
def report_to_rollbar(error, context = {})
-
1
return unless defined?(::Rollbar)
-
-
1
::Rollbar.error(error, context)
-
rescue => e
-
fallback_logging(e, {original_error: error.class.to_s})
-
end
-
-
# Report to Honeybadger
-
3
sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
-
2
def report_to_honeybadger(error, context = {})
-
1
return unless defined?(::Honeybadger)
-
-
1
::Honeybadger.notify(error, context: context)
-
rescue => e
-
fallback_logging(e, {original_error: error.class.to_s})
-
end
-
-
# Fallback logging when no error reporting services are available
-
# Uses the LogStruct.error method to properly log the error
-
3
sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
-
2
def fallback_logging(error, context = {})
-
2
return if error.nil?
-
-
# Create a proper error log entry
-
2
error_log = Log.from_exception(Source::Internal, error, context)
-
-
# Use LogStruct.error to properly log the error
-
2
LogStruct.error(error_log)
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "json"
-
2
require_relative "hash_utils"
-
2
require_relative "string_scrubber"
-
2
require_relative "config_struct/filters"
-
2
require_relative "enums/source"
-
-
2
module LogStruct
-
# This class contains methods for filtering sensitive data in logs
-
# It is used by Formatter to determine which keys should be filtered
-
2
class ParamFilters
-
2
class << self
-
2
extend T::Sig
-
-
# Check if a key should be filtered based on our defined sensitive keys.
-
# logstruct_field: the key is one of LogStruct's own fields at the top of a line.
-
# path: the keys from the line's top-level field down to this one (the key alone if not given).
-
# root: the top-level field's value, or the whole line for a top-level key.
-
2
sig do
-
2
params(
-
key: T.untyped,
-
value: T.untyped,
-
logstruct_field: T::Boolean,
-
path: T.nilable(T::Array[String]),
-
root: T.untyped
-
).returns(T::Boolean)
-
end
-
2
def should_filter_key?(key, value = nil, logstruct_field: false, path: nil, root: nil)
-
12359
filters = LogStruct.config.filters
-
12359
normalized_key = key.to_s
-
-
12359
words = KeyWords.normalize(normalized_key).to_sym
-
# By its words too, so :api_key filters apiKey and X-Api-Key. Keys in filter_keys_with_hashes are
-
# filtered too, with a hash of their value.
-
12359
[filters.filter_keys, filters.filter_keys_with_hashes].each do |keys|
-
24657
return true if keys.include?(normalized_key.downcase.to_sym) || keys.include?(words)
-
end
-
-
12262
matched = filters.filter_matchers.any? do |matcher|
-
24315
next false if logstruct_field && !matcher.matches_logstruct_fields
-
-
14273
matcher.matches?(normalized_key, value)
-
rescue => e
-
handle_filter_matcher_error(e, matcher.label, normalized_key)
-
false
-
end
-
12262
return true if matched
-
12165
return false if filters.path_matchers.empty?
-
-
35
path ||= [normalized_key]
-
35
root = {} if root.nil?
-
35
filters.path_matchers.any? do |matcher|
-
35
matcher.matches?(path, value, root)
-
rescue => e
-
handle_filter_matcher_error(e, matcher.label, normalized_key)
-
false
-
end
-
end
-
-
# Check if a key should be hashed rather than completely filtered
-
3
sig { params(key: T.untyped).returns(T::Boolean) }
-
2
def should_include_string_hash?(key)
-
67
keys = LogStruct.config.filters.filter_keys_with_hashes
-
67
keys.include?(key.to_s.downcase.to_sym) || keys.include?(KeyWords.normalize(key.to_s).to_sym)
-
end
-
-
# Convert a value to a filtered summary hash (e.g. { _filtered: { class: "String", ... }})
-
3
sig { params(key: T.untyped, data: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
-
2
def summarize_json_attribute(key, data)
-
86
case data
-
when Hash
-
13
summarize_hash(data)
-
when Array
-
7
summarize_array(data)
-
when String
-
63
summarize_string(data, should_include_string_hash?(key))
-
else
-
3
{_class: data.class}
-
end
-
end
-
-
# Summarize a filtered String: its class, and an HMAC of it for keys in filter_keys_with_hashes.
-
# No length, which would say how long a password or token is.
-
3
sig { params(string: String, include_hash: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
-
2
def summarize_string(string, include_hash)
-
filtered_string = {
-
65
_class: String
-
}
-
65
filtered_string[:_hash] = StringScrubber.value_hash(string) if include_hash
-
-
65
filtered_string
-
end
-
-
# Summarize a filtered Hash: its first 10 keys and how many there are. No size, which would say
-
# how long the secrets in it are.
-
3
sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
-
2
def summarize_hash(hash)
-
16
return {_class: "Hash", _empty: true} if hash.empty?
-
-
{
-
15
_class: Hash,
-
_keys_count: hash.keys.size,
-
30
_keys: hash.keys.take(10).map { |key| normalize_summary_key(StringScrubber.scrub_name(key)) }
-
}
-
end
-
-
# Summarize a filtered Array: how many items it has
-
3
sig { params(array: T::Array[T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
-
2
def summarize_array(array)
-
9
return {_class: "Array", _empty: true} if array.empty?
-
-
{
-
7
_class: Array,
-
_count: array.size
-
}
-
end
-
-
2
private
-
-
3
sig { params(key: T.any(String, Symbol, Integer, T.untyped)).returns(T.any(Symbol, String)) }
-
2
def normalize_summary_key(key)
-
30
if key.is_a?(Symbol)
-
27
key
-
3
elsif key.respond_to?(:to_sym)
-
2
key.to_sym
-
else
-
1
key.to_s
-
end
-
rescue
-
key.to_s
-
end
-
-
2
sig { params(error: StandardError, label: String, key: String).void }
-
2
def handle_filter_matcher_error(error, label, key)
-
context = {
-
matcher: label,
-
key: key
-
}
-
LogStruct.handle_exception(error, source: Source::Internal, context: context)
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
# Errors from parsing a request's params: Rails' ParseError (and its ParamErrors), and Rack's. Their messages, and
-
# those of errors raised because of them (Rails' "Invalid request parameters: ..."), quote the request's input: a
-
# JSON parser's excerpt of the body, Rack's "invalid %-encoding (<value>)". Nothing can filter a body that didn't
-
# parse, so LogStruct's lines leave the input out.
-
2
module ParamsErrors
-
2
extend T::Sig
-
-
# Matched by name, so this works without ActionDispatch or Rack loaded
-
2
NAMES = T.let(
-
[
-
"ActionDispatch::Http::Parameters::ParseError",
-
"Rack::BadRequest",
-
"Rack::QueryParser::InvalidParameterError",
-
"Rack::QueryParser::ParameterTypeError",
-
"Rack::QueryParser::ParamsTooDeepError"
-
].freeze,
-
T::Array[String]
-
)
-
# Rails' message for a ParseError
-
2
PARSE_ERROR_MESSAGE = "Error occurred while parsing request parameters"
-
2
PARAMS_ERROR_MESSAGE = "Invalid request parameters"
-
2
CAUSE_MESSAGE_LEFT_OUT = "(left out: it quotes the request's input)"
-
-
# The message and the cause's message to log for an error, without the input a params parser quoted
-
3
sig { params(error: Exception).returns([String, T.nilable(String)]) }
-
2
def self.messages(error)
-
47
cause = error.cause
-
47
if params_error?(error)
-
7
[parse_error?(error) ? PARSE_ERROR_MESSAGE : PARAMS_ERROR_MESSAGE, nil]
-
40
elsif cause && params_error?(cause)
-
4
message = error.message.sub(cause.message, "").sub(/[\s:]+\z/, "")
-
4
[message.empty? ? PARAMS_ERROR_MESSAGE : message, nil]
-
else
-
36
[error.message, cause&.message]
-
end
-
end
-
-
# For text that writes an error's whole chain (its causes too): each message in it that quotes a params parser's
-
# input, with what to write instead. Longest first, since an error's message can contain its cause's. An error
-
# raised without a message (Rack's and Rails' depth limits) has its class name as its message, which quotes
-
# nothing and is also the text's label for its class, so it stays.
-
3
sig { params(error: Exception).returns(T::Array[[String, String]]) }
-
2
def self.replacements(error)
-
5
pairs = T.let([], T::Array[[String, String]])
-
5
seen = T.let({}.compare_by_identity, T::Hash[Exception, TrueClass])
-
5
current = T.let(error, T.nilable(Exception))
-
5
while current && !seen.key?(current)
-
10
seen[current] = true
-
10
message, cause_message = messages(current)
-
10
pairs << [current.message, message] if message != current.message && !default_message?(current)
-
10
cause = current.cause
-
10
pairs << [cause.message, CAUSE_MESSAGE_LEFT_OUT] if cause && cause_message.nil? && !default_message?(cause)
-
10
current = cause
-
end
-
16
pairs.reject { |quoted, _| quoted.empty? }.uniq.sort_by { |quoted, _| -quoted.length }
-
end
-
-
3
sig { params(error: Exception).returns(T::Boolean) }
-
2
def self.default_message?(error)
-
8
error.message == error.class.name
-
end
-
-
3
sig { params(error: Exception).returns(T::Boolean) }
-
2
def self.parse_error?(error)
-
7
!!defined?(::ActionDispatch::Http::Parameters::ParseError) &&
-
error.instance_of?(::ActionDispatch::Http::Parameters::ParseError)
-
end
-
-
3
sig { params(error: Exception).returns(T::Boolean) }
-
2
def self.params_error?(error)
-
660
error.class.ancestors.any? { |ancestor| NAMES.include?(ancestor.name) }
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "sorbet-runtime"
-
-
2
module LogStruct
-
2
module RailsBootBannerSilencer
-
2
extend T::Sig
-
-
2
@installed = T.let(false, T::Boolean)
-
-
4
sig { void }
-
2
def self.install!
-
4
return if @installed
-
4
@installed = true
-
-
4
return unless ARGV.include?("server")
-
# Rails' own banner stays when LogStruct is disabled
-
1
return unless LogStruct.enabled?
-
-
patch!
-
end
-
-
3
sig { returns(T::Boolean) }
-
2
def self.patch!
-
begin
-
2
require "rails/command"
-
1
require "rails/commands/server/server_command"
-
rescue LoadError
-
# Best-effort – if Rails isn't available yet we'll try again later
-
1
return false
-
end
-
-
1
server_command = T.let(nil, T.untyped)
-
# rubocop:disable Sorbet/ConstantsFromStrings
-
begin
-
1
server_command = ::Object.const_get("Rails::Command::ServerCommand")
-
rescue NameError
-
server_command = nil
-
end
-
# rubocop:enable Sorbet/ConstantsFromStrings
-
1
return false unless server_command
-
-
1
patch_server_command(server_command)
-
1
true
-
end
-
-
3
sig { params(server_command: T.untyped).void }
-
2
def self.patch_server_command(server_command)
-
6
return if server_command <= ServerCommandSilencer
-
-
5
server_command.prepend(ServerCommandSilencer)
-
end
-
-
2
module ServerCommandSilencer
-
2
extend T::Sig
-
2
extend T::Helpers
-
2
requires_ancestor { Object }
-
-
3
sig { params(args: T.untyped, block: T.nilable(T.proc.returns(T.untyped))).returns(T.untyped) }
-
2
def perform(*args, &block)
-
1
::LogStruct.server_mode = true
-
1
super
-
end
-
-
3
sig { params(server: T.untyped, url: T.nilable(String)).void }
-
2
def print_boot_information(server, url)
-
2
::LogStruct.server_mode = true
-
2
consume_boot_banner(server, url)
-
end
-
-
2
private
-
-
3
sig { params(server: T.untyped, url: T.nilable(String)).void }
-
2
def consume_boot_banner(server, url)
-
2
return unless defined?(::LogStruct::Integrations::Puma)
-
-
begin
-
2
::LogStruct::Integrations::Puma.emit_boot_if_needed!
-
rescue => e
-
::LogStruct::Integrations::Puma.handle_integration_error(e)
-
end
-
-
begin
-
2
model = ::ActiveSupport::Inflector.demodulize(server)
-
rescue
-
1
model = "Puma"
-
end
-
-
lines = [
-
2
"=> Booting #{model}",
-
build_rails_banner_line(url),
-
"=> Run `#{lookup_executable} --help` for more startup options"
-
]
-
-
2
lines.each do |line|
-
6
::LogStruct::Integrations::Puma.process_line(line)
-
rescue => e
-
::LogStruct::Integrations::Puma.handle_integration_error(e)
-
end
-
end
-
-
3
sig { params(url: T.nilable(String)).returns(String) }
-
2
def build_rails_banner_line(url)
-
2
suffix = url ? " #{url}" : ""
-
2
"=> Rails #{::Rails.version} application starting in #{::Rails.env}#{suffix}"
-
rescue
-
1
"=> Rails application starting"
-
end
-
-
3
sig { returns(String) }
-
2
def lookup_executable
-
3
return "rails" unless respond_to?(:executable, true)
-
-
2
T.cast(send(:executable), String)
-
rescue
-
1
"rails"
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "rails"
-
2
require "semantic_logger"
-
2
require_relative "formatter"
-
2
require_relative "semantic_logger/setup"
-
2
require_relative "integrations"
-
-
2
module LogStruct
-
# Railtie to integrate with Rails
-
2
class Railtie < ::Rails::Railtie
-
# Replace the logger as soon as Rails creates it, so boot-time logs and anything that keeps
-
# a reference to Rails.logger use LogStruct's logger
-
2
initializer "logstruct.configure_logger", after: :initialize_logger do |app|
-
3
next unless LogStruct.enabled?
-
-
# Use SemanticLogger for powerful logging features
-
3
LogStruct::SemanticLogger::Setup.configure_semantic_logger(app)
-
-
# Initializers that configure Sidekiq log with its logger while config/initializers run
-
3
Integrations::Sidekiq.configure_logger if LogStruct.config.integrations.enable_sidekiq
-
end
-
-
# Set up integrations after config/initializers, so that the settings in
-
# config/initializers/logstruct.rb and filter_parameters added by other initializers apply
-
2
initializer "logstruct.setup", after: :load_config_initializers, before: :build_middleware_stack do |app|
-
# An initializer may have changed `enabled` or `enabled_environments` after the logger was
-
# (or wasn't) replaced
-
3
LogStruct.apply_initializer_settings!
-
3
if LogStruct.enabled?
-
3
if LogStruct::SemanticLogger::Setup.configured?
-
# The formatter, colors, noisy logger filter and log file settings in config/initializers apply too
-
3
LogStruct::SemanticLogger::Setup.reconfigure_appenders(app)
-
else
-
LogStruct::SemanticLogger::Setup.configure_semantic_logger(app)
-
end
-
elsif LogStruct::SemanticLogger::Setup.configured?
-
LogStruct::SemanticLogger::Setup.restore_rails_logger(app)
-
end
-
3
unless LogStruct.enabled? && LogStruct.config.integrations.enable_sidekiq
-
1
Integrations::Sidekiq.restore_logger
-
end
-
3
Integrations::ActionMailer.setup_fallback_error_handlers(LogStruct.config)
-
-
3
next unless LogStruct.enabled?
-
-
# Merge Rails filter parameters into our filters
-
3
LogStruct.merge_rails_filter_parameters!
-
-
3
Integrations.setup_integrations(stage: :non_middleware)
-
end
-
-
# Add middleware before the middleware stack is built
-
2
initializer "logstruct.configure_middleware", after: "logstruct.setup", before: :build_middleware_stack do |app|
-
2
next unless LogStruct.enabled?
-
-
2
Integrations.setup_integrations(stage: :middleware)
-
end
-
-
# Emit Puma lifecycle logs when running `rails server`
-
2
initializer "logstruct.puma_lifecycle", after: "logstruct.configure_logger" do
-
3
next unless LogStruct.enabled?
-
2
is_server = ::LogStruct.server_mode?
-
2
next unless is_server
-
begin
-
require "log_struct/log/puma"
-
port = LogStruct::Integrations::Puma.port_from_argv(ARGV)
-
started = LogStruct::Log::Puma::Start.new(
-
mode: "single",
-
environment: (defined?(::Rails) && ::Rails.respond_to?(:env)) ? ::Rails.env : nil,
-
process_id: Process.pid,
-
listening_addresses: port ? ["tcp://127.0.0.1:#{port}"] : nil
-
)
-
LogStruct.info(started)
-
-
at_exit do
-
shutdown = LogStruct::Log::Puma::Shutdown.new(
-
process_id: Process.pid
-
)
-
LogStruct.info(shutdown)
-
end
-
rescue
-
# best-effort
-
end
-
end
-
-
# Delegate integration initializers to Integrations module
-
2
LogStruct::Integrations.setup_initializers(self)
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "semantic_logger"
-
2
require_relative "formatter"
-
-
2
module LogStruct
-
2
module SemanticLogger
-
# Writes lines as pretty-printed JSON in color, for reading in a terminal: keys yellow, strings green, numbers
-
# blue, booleans magenta, nil red and logger names cyan (config.color_map changes them). LogStruct uses it in
-
# development when LogStruct is enabled there, prefer_json_in_development is false and enable_color_output is
-
# true (its default). LogStruct entries and Hashes are the JSON line, pretty-printed. Plain lines keep
-
# SemanticLogger's layout, with their message, exception and tags scrubbed and their named tags filtered as a
-
# JSON line's ctx is.
-
2
class ColorFormatter < ::SemanticLogger::Formatters::Color
-
2
extend T::Sig
-
-
3
sig { params(color_map: T.nilable(T::Hash[Symbol, Symbol]), args: T.untyped).void }
-
2
def initialize(color_map: nil, **args)
-
15
super(**args)
-
15
@logstruct_formatter = T.let(LogStruct::Formatter.new, LogStruct::Formatter)
-
15
@json_formatter = T.let(LogStruct::SemanticLogger::Formatter.new, LogStruct::SemanticLogger::Formatter)
-
-
# Set up custom color mapping
-
15
@custom_colors = T.let(default_color_map.merge(color_map || {}), T::Hash[Symbol, Symbol])
-
end
-
-
3
sig { override.params(log: ::SemanticLogger::Log, logger: T.untyped).returns(String) }
-
2
def call(log, logger)
-
# Handle LogStruct types specially with colorization
-
13
if log.payload.is_a?(Hash) || log.payload.is_a?(T::Struct)
-
6
formatted = format_logstruct_payload(log, logger)
-
6
formatted if formatted
-
else
-
# For plain messages, use SemanticLogger's default colorization
-
7
super
-
end
-
end
-
-
# The parts of a plain line that hold what was logged, scrubbed like the strings in a JSON line
-
3
sig { returns(T.untyped) }
-
2
def message
-
7
scrub_text(super)
-
end
-
-
3
sig { returns(T.untyped) }
-
2
def payload
-
7
scrub_text(super)
-
end
-
-
# SemanticLogger's text for the exception and its causes, without the input a params parser quoted
-
3
sig { returns(T.untyped) }
-
2
def exception
-
7
text = super
-
7
error = log.exception
-
7
if text.is_a?(String) && error.is_a?(Exception)
-
5
ParamsErrors.replacements(error).each { |quoted, logged| text = text.gsub(quoted, logged) }
-
end
-
7
scrub_text(text)
-
end
-
-
3
sig { returns(T.untyped) }
-
2
def tags
-
7
scrub_text(super)
-
end
-
-
# Filtered and scrubbed as they are in a JSON line's ctx: a key filtering hides there is hidden here too
-
3
sig { returns(T.nilable(String)) }
-
2
def named_tags
-
7
named_tags = log.named_tags
-
7
return if named_tags.nil? || named_tags.empty?
-
-
1
filtered = @logstruct_formatter.process_values({ctx: named_tags.to_h.transform_keys(&:to_sym)})[:ctx]
-
3
list = filtered.map { |name, value| "#{color}#{name}: #{value}#{color_map.clear}" }
-
1
"{#{list.join(", ")}}"
-
end
-
-
2
private
-
-
3
sig { params(text: T.untyped).returns(T.untyped) }
-
2
def scrub_text(text)
-
28
text.is_a?(String) ? StringScrubber.scrub(text) : text
-
end
-
-
2
sig { returns(LogStruct::Formatter) }
-
2
attr_reader :logstruct_formatter
-
-
# Default color mapping for LogStruct JSON
-
3
sig { returns(T::Hash[Symbol, Symbol]) }
-
2
def default_color_map
-
15
{
-
key: :yellow,
-
string: :green,
-
number: :blue,
-
bool: :magenta,
-
nil: :red,
-
name: :cyan
-
}
-
end
-
-
# The line as pretty-printed JSON, with ANSI colors for its keys, strings, numbers, booleans and nulls
-
3
sig { params(data: T::Hash[String, T.untyped]).returns(String) }
-
2
def colorize_json(data)
-
6
json_str = JSON.pretty_generate(data)
-
-
# Apply basic colorization with regex
-
6
json_str.gsub(/"([^"]+)":/, colorize_text('\1', :key) + ":")
-
.gsub(/: "([^"]*)"/, ": " + colorize_text('\1', :string))
-
.gsub(/: (\d+\.?\d*)/, ": " + colorize_text('\1', :number))
-
.gsub(/: (true|false)/, ": " + colorize_text('\1', :bool))
-
.gsub(": null", ": " + colorize_text("null", :nil))
-
end
-
-
# The line LogStruct::SemanticLogger::Formatter writes (message, tags and ctx included), in color
-
3
sig { params(log: ::SemanticLogger::Log, logger: T.untyped).returns(T.nilable(String)) }
-
2
def format_logstruct_payload(log, logger)
-
6
logstruct_json = @json_formatter.call(log, logger)
-
-
6
parsed_data = T.let(JSON.parse(logstruct_json), T::Hash[String, T.untyped])
-
6
colorized_json = colorize_json(parsed_data)
-
-
6
prefix = format("%<time>s %<level>s [%<process>s] %<name>s -- ",
-
time: format_time(log.time),
-
level: format_level(log.level),
-
process: log.process_info,
-
name: format_name(log.name))
-
-
# SemanticLogger's appenders add the newline
-
6
"#{prefix}#{colorized_json}"
-
rescue JSON::ParserError
-
nil
-
end
-
-
# Add ANSI color codes to text
-
3
sig { params(text: String, color_type: Symbol).returns(String) }
-
2
def colorize_text(text, color_type)
-
30
color = @custom_colors[color_type] || :white
-
30
"\e[#{color_code_for(color)}m#{text}\e[0m"
-
end
-
-
# Format timestamp
-
3
sig { params(time: Time).returns(String) }
-
2
def format_time(time)
-
13
time.strftime("%Y-%m-%d %H:%M:%S.%6N")
-
end
-
-
# Format log level with color
-
3
sig { params(level: T.any(String, Symbol)).returns(String) }
-
2
def format_level(level)
-
6
level_str = level.to_s.upcase[0]
-
6
color = level_color_for(level.to_sym)
-
6
"\e[#{color_code_for(color)}m#{level_str}\e[0m"
-
end
-
-
# Format logger name with color
-
3
sig { params(name: T.nilable(String)).returns(String) }
-
2
def format_name(name)
-
6
return "" unless name
-
6
color = @custom_colors[:name] || :cyan
-
6
"\e[#{color_code_for(color)}m#{name}\e[0m"
-
end
-
-
# Get color for log level
-
3
sig { params(level: Symbol).returns(Symbol) }
-
2
def level_color_for(level)
-
6
case level
-
when :debug then :magenta
-
6
when :info then :cyan
-
when :warn then :yellow
-
when :error then :red
-
when :fatal then :red
-
else :cyan
-
end
-
end
-
-
# Get ANSI color code for color symbol
-
3
sig { params(color: Symbol).returns(String) }
-
2
def color_code_for(color)
-
42
case color
-
when :black then "30"
-
8
when :red then "31"
-
6
when :green then "32"
-
4
when :yellow then "33"
-
6
when :blue then "34"
-
6
when :magenta then "35"
-
12
when :cyan then "36"
-
when :white then "37"
-
when :bright_black then "90"
-
when :bright_red then "91"
-
when :bright_green then "92"
-
when :bright_yellow then "93"
-
when :bright_blue then "94"
-
when :bright_magenta then "95"
-
when :bright_cyan then "96"
-
when :bright_white then "97"
-
else "37" # default to white
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module SemanticLogger
-
2
module Concerns
-
2
module LogMethods
-
2
extend T::Sig
-
2
extend T::Helpers
-
2
requires_ancestor { LogStruct::SemanticLogger::Logger }
-
-
# Override log methods to handle LogStruct types and broadcast
-
4
sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
-
2
def debug(message = nil, payload = nil, exception = nil, &block)
-
14
result = super(*semantic_logger_args(message, payload, exception), &block)
-
14
instrument_log(message, :debug)
-
14
broadcast_message(:debug, message, block)
-
14
result
-
end
-
-
4
sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
-
2
def info(message = nil, payload = nil, exception = nil, &block)
-
1508
result = super(*semantic_logger_args(message, payload, exception), &block)
-
1508
instrument_log(message, :info)
-
1507
broadcast_message(:info, message, block)
-
1507
result
-
end
-
-
4
sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
-
2
def warn(message = nil, payload = nil, exception = nil, &block)
-
17
result = super(*semantic_logger_args(message, payload, exception), &block)
-
17
instrument_log(message, :warn)
-
17
broadcast_message(:warn, message, block)
-
17
result
-
end
-
-
3
sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
-
2
def error(message = nil, payload = nil, exception = nil, &block)
-
78
result = super(*semantic_logger_args(message, payload, exception), &block)
-
78
instrument_log(message, :error)
-
78
broadcast_message(:error, message, block)
-
78
result
-
end
-
-
3
sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
-
2
def fatal(message = nil, payload = nil, exception = nil, &block)
-
3
result = super(*semantic_logger_args(message, payload, exception), &block)
-
3
instrument_log(message, :fatal)
-
3
broadcast_message(:fatal, message, block)
-
3
result
-
end
-
-
2
private
-
-
# SemanticLogger's arguments for a line: a LogStruct entry or struct goes in the payload, and a Hash is the
-
# payload itself, so its keys are written at the top level of the line. An exception can come second
-
# (logger.error({id: 1}, error)) or third.
-
4
sig { params(message: T.untyped, payload: T.untyped, exception: T.untyped).returns([T.untyped, T.untyped, T.untyped]) }
-
2
def semantic_logger_args(message, payload, exception)
-
1620
if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct)
-
237
[nil, {payload: message}, nil]
-
1383
elsif message.is_a?(Hash)
-
29
[nil, message, payload.is_a?(Exception) ? payload : exception]
-
else
-
1354
[message, payload, exception]
-
end
-
end
-
-
# Sends a LogStruct entry to the log.logstruct subscribers, after its line is queued to be written, so a
-
# subscriber's error can't lose it. The error is handled like the app's other errors (standard_errors, which
-
# raises it from the log call by default), and the lines written while handling it aren't sent to the
-
# subscribers again.
-
4
sig { params(message: T.untyped, level: Symbol).void }
-
2
def instrument_log(message, level)
-
1620
return unless message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct)
-
237
return if Thread.current[:logstruct_instrumenting_log]
-
-
235
Thread.current[:logstruct_instrumenting_log] = true
-
begin
-
235
::ActiveSupport::Notifications.instrument("log.logstruct", log: message, level: level)
-
rescue => e
-
2
LogStruct.handle_exception(e, source: Source::App, context: {notification: "log.logstruct"})
-
ensure
-
235
Thread.current[:logstruct_instrumenting_log] = false
-
end
-
end
-
-
# Other loggers get String messages, and what blocks return, scrubbed like a log line's strings
-
4
sig { params(level: Symbol, message: T.untyped, block: T.nilable(T.proc.returns(String))).void }
-
2
def broadcast_message(level, message, block)
-
1619
broadcasts.each do |logger|
-
11
next unless logger.respond_to?(level)
-
-
11
if message.is_a?(String)
-
9
logger.public_send(level, StringScrubber.scrub(message))
-
2
elsif block
-
1
logger.public_send(level) do
-
# The block type says String, but nothing checks what a block returns
-
1
text = T.let(block.call, T.untyped)
-
1
text.is_a?(String) ? StringScrubber.scrub(text) : text
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "semantic_logger"
-
2
require_relative "../formatter"
-
2
require_relative "../params_errors"
-
-
2
module LogStruct
-
2
module SemanticLogger
-
# Writes each SemanticLogger line as LogStruct's JSON: LogStruct entries, Hashes, messages and exceptions, with
-
# SemanticLogger's tags and named tags (the request ID and the running Sidekiq job's context among them). LogStruct::Formatter
-
# filters and scrubs every value, within the size limits in config.limits.
-
2
class Formatter < ::SemanticLogger::Formatters::Json
-
2
extend T::Sig
-
-
4
sig { void }
-
2
def initialize
-
161
super
-
161
@logstruct_formatter = T.let(LogStruct::Formatter.new, LogStruct::Formatter)
-
end
-
-
4
sig { params(log: ::SemanticLogger::Log, logger: T.untyped).returns(String) }
-
2
def call(log, logger)
-
# Extract LogStruct from various locations where it might be stored
-
1628
logstruct = extract_logstruct(log)
-
-
# The request ID, the running Sidekiq job's context and the other named tags (logger.tagged(user_id: 1))
-
# go in req_id and ctx, and tags (logger.tagged("billing"), config.log_tags, ActiveJob's) in tags
-
1628
named_tags = log.named_tags
-
1628
request_id = named_tags[:request_id]
-
1628
job_context = named_tags[:sidekiq_context]
-
1628
context = named_tags.except(:request_id, :sidekiq_context)
-
1628
context = context.merge(job_context) if job_context.is_a?(Hash)
-
metadata = {
-
3256
request_id: request_id.is_a?(String) ? request_id : nil,
-
1628
context: context.empty? ? nil : context,
-
1628
tags: log.tags.empty? ? nil : log.tags
-
}
-
-
1628
json = if logstruct
-
# Use our formatter to process LogStruct types directly
-
240
@logstruct_formatter.call(log.level, log.time, log.name, logstruct, **metadata)
-
1388
elsif log.exception
-
9
@logstruct_formatter.call(log.level, log.time, log.name, exception_log(log), **metadata)
-
1379
elsif log.payload.is_a?(Hash)
-
# Process hashes through our formatter
-
31
payload = with_message(without_reserved_keys(log.payload), log.message)
-
31
@logstruct_formatter.call(log.level, log.time, log.name, payload, **metadata)
-
1348
elsif log.payload.is_a?(T::Struct)
-
@logstruct_formatter.call(log.level, log.time, log.name, log.payload, **metadata)
-
else
-
# For plain messages, create a Plain log entry
-
1348
message_data = log.payload || log.message
-
1348
plain_log = ::LogStruct::Log::Plain.new(
-
message: message_data,
-
timestamp: log.time
-
)
-
1348
@logstruct_formatter.call(log.level, log.time, log.name, plain_log, **metadata)
-
end
-
-
# SemanticLogger appenders typically add their own newline. Avoid double newlines by stripping ours.
-
1628
json.end_with?("\n") ? json.chomp : json
-
end
-
-
2
private
-
-
# logger.error(exception) and logger.error("Message", exception): an error line with the
-
# exception's class, message and backtrace. A logged message goes in msg, and the exception's
-
# message in error_message. A Hash logged with them adds its keys to the line.
-
3
sig { params(log: ::SemanticLogger::Log).returns(LogStruct::Log::Error) }
-
2
def exception_log(log)
-
9
exception = T.let(log.exception, Exception)
-
9
message = log.message
-
9
logged_message = (message.is_a?(String) && !message.empty?) ? message : nil
-
9
payload = log.payload
-
# Without the input a params parser quoted
-
9
exception_message = ParamsErrors.messages(exception).first
-
9
LogStruct::Log::Error.new(
-
source: Source::App,
-
error_class: exception.class,
-
message: logged_message || exception_message,
-
9
error_message: logged_message ? exception_message : nil,
-
backtrace: exception.backtrace,
-
9
additional_data: payload.is_a?(Hash) ? without_reserved_keys(payload) : nil,
-
timestamp: log.time
-
)
-
end
-
-
# A message logged beside a Hash (logger.info("Saved", id: 1)) goes in msg, unless the Hash
-
# has its own
-
3
sig { params(payload: T::Hash[T.untyped, T.untyped], message: T.untyped).returns(T::Hash[T.untyped, T.untyped]) }
-
2
def with_message(payload, message)
-
31
return payload unless message.is_a?(String) && !message.empty?
-
3
return payload if payload.key?(:msg) || payload.key?("msg")
-
-
2
{msg: message}.merge(payload)
-
end
-
-
# A logged Hash may set its own src and evt, but the line's timestamp and level come from the logger
-
2
HASH_RESERVED_KEYS = T.let(%w[ts lvl].freeze, T::Array[String])
-
-
3
sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[T.untyped, T.untyped]) }
-
2
def without_reserved_keys(hash)
-
135
reserved = hash.keys.select { |key| HASH_RESERVED_KEYS.include?(key.to_s) }
-
33
return hash if reserved.empty?
-
-
# A copy, since other appenders format the same Hash
-
1
copy = hash.dup
-
1
reserved.each do |key|
-
2
LogStruct.handle_exception(
-
ArgumentError.new("A logged Hash attempted to set reserved key: #{key}"),
-
source: Source::Internal
-
)
-
2
copy.delete(key)
-
end
-
1
copy
-
end
-
-
# Extract a LogStruct from the various places it might be stored in a SemanticLogger::Log
-
2
sig do
-
2
params(log: ::SemanticLogger::Log).returns(
-
T.nilable(
-
T.any(
-
LogStruct::Log::Interfaces::CommonFields,
-
LogStruct::Log::Interfaces::PublicCommonFields,
-
T::Struct
-
)
-
)
-
)
-
end
-
2
def extract_logstruct(log)
-
# Check payload first (most common path for structured logging)
-
1628
if log.payload.is_a?(Hash) && log.payload[:payload].is_a?(LogStruct::Log::Interfaces::CommonFields)
-
238
return T.cast(log.payload[:payload], LogStruct::Log::Interfaces::CommonFields)
-
end
-
-
1390
if log.payload.is_a?(LogStruct::Log::Interfaces::CommonFields)
-
return log.payload
-
end
-
-
1390
if log.payload.is_a?(LogStruct::Log::Interfaces::PublicCommonFields)
-
return log.payload
-
end
-
-
# Check message - this is where structs end up when passed directly to logger.info(struct)
-
1390
if log.message.is_a?(LogStruct::Log::Interfaces::CommonFields)
-
return T.cast(log.message, LogStruct::Log::Interfaces::CommonFields)
-
end
-
-
# Check for T::Struct in payload hash (might be a LogStruct struct not implementing CommonFields directly)
-
1390
if log.payload.is_a?(Hash) && log.payload[:payload].is_a?(T::Struct)
-
2
struct = log.payload[:payload]
-
2
if struct.respond_to?(:source) && struct.respond_to?(:event)
-
2
return struct
-
end
-
end
-
-
1388
nil
-
end
-
-
2
sig { returns(LogStruct::Formatter) }
-
2
attr_reader :logstruct_formatter
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "active_support/logger"
-
2
require "semantic_logger"
-
2
require_relative "concerns/log_methods"
-
-
2
module LogStruct
-
2
module SemanticLogger
-
# Rails.logger's replacement: a SemanticLogger logger that also takes LogStruct's typed entries and Hashes, and
-
# passes each line on to the loggers added with broadcast_to.
-
#
-
# ```ruby
-
# logger = LogStruct::SemanticLogger::Logger.new("MyApp")
-
#
-
# # A typed entry
-
# logger.info(LogStruct::Log::Plain.new(message: "User authenticated", additional_data: {user_id: 123}))
-
#
-
# # A Hash: its keys are written at the top level of the line
-
# logger.info(action: "user_login", user_id: 123)
-
#
-
# # A message, with a Hash or an exception beside it
-
# logger.info("User logged in", user_id: 123)
-
# logger.error("Import failed", error)
-
# ```
-
2
class Logger < ::SemanticLogger::Logger
-
2
extend T::Sig
-
-
4
sig { params(name: T.any(String, Symbol, Module, T::Class[T.anything]), level: T.nilable(Symbol), filter: T.untyped).void }
-
2
def initialize(name = "Application", level: nil, filter: nil)
-
# SemanticLogger::Logger expects positional arguments, not named arguments
-
125
super(name, level, filter)
-
# T.untyped because users can pass any logger: ::Logger, ActiveSupport::Logger,
-
# custom loggers (FakeLogger in tests), or third-party loggers
-
125
@broadcasts = T.let([], T::Array[T.untyped])
-
# ActiveJob expects logger.formatter to exist and respond to current_tags
-
125
@formatter = T.let(FormatterProxy.new, FormatterProxy)
-
end
-
-
# ActiveSupport::BroadcastLogger compatibility
-
# These methods allow Rails.logger to broadcast to multiple loggers
-
4
sig { returns(T::Array[T.untyped]) }
-
2
attr_reader :broadcasts
-
-
# ActiveJob compatibility - expects logger.formatter.current_tags
-
4
sig { returns(FormatterProxy) }
-
2
attr_reader :formatter
-
-
# T.untyped for logger param because we accept any logger-like object:
-
# ::Logger, ActiveSupport::Logger, test doubles, etc.
-
3
sig { params(logger: T.untyped).returns(T.untyped) }
-
2
def broadcast_to(logger)
-
# Every SemanticLogger logger writes to the same appenders, so broadcasting to one
-
# would write each message twice. Sidekiq's Rails integration broadcasts Rails.logger
-
# to Sidekiq's logger, for example.
-
11
return logger if logger.is_a?(::SemanticLogger::Logger)
-
# `rails server` in development broadcasts to a STDOUT logger, which would print each
-
# message again as text beside LogStruct's line
-
10
return logger if console_logger_with_console_appender?(logger)
-
-
9
@broadcasts << logger
-
9
logger
-
end
-
-
3
sig { params(logger: T.untyped).void }
-
2
def stop_broadcasting_to(logger)
-
2
@broadcasts.delete(logger)
-
end
-
-
2
include Concerns::LogMethods
-
-
# Support for tagged logging
-
3
sig { params(tags: T.untyped, block: T.proc.returns(T.untyped)).returns(T.untyped) }
-
2
def tagged(*tags, &block)
-
26
tag_array = tags.flatten
-
26
if tag_array.empty?
-
super(&block)
-
else
-
# Sorbet can't check a splat of an Array whose size it doesn't know
-
26
super(*T.unsafe(tag_array), &block)
-
end
-
end
-
-
# Ensure compatibility with Rails.logger interface
-
2
sig { returns(T::Array[T.any(String, Symbol)]) }
-
2
def current_tags
-
::SemanticLogger.tags
-
end
-
-
2
sig { void }
-
2
def clear_tags!
-
# SemanticLogger doesn't have clear_tags!, use pop_tags instead
-
count = ::SemanticLogger.tags.length
-
::SemanticLogger.pop_tags(count) if count > 0
-
end
-
-
2
sig { params(tags: T.untyped).returns(T::Array[T.untyped]) }
-
2
def push_tags(*tags)
-
flat = tags.flatten.compact
-
flat.each { |tag| ::SemanticLogger.push_tags(tag) }
-
flat
-
end
-
-
2
sig { params(count: Integer).void }
-
2
def pop_tags(count = 1)
-
::SemanticLogger.pop_tags(count)
-
end
-
-
# Support for << operator (used by RailsLogSplitter). info passes the message on to the broadcast loggers.
-
3
sig { params(msg: String).returns(T.self_type) }
-
2
def <<(msg)
-
2
info(msg)
-
2
self
-
end
-
-
2
private
-
-
3
sig { params(logger: T.untyped).returns(T::Boolean) }
-
2
def console_logger_with_console_appender?(logger)
-
# rails server's logger writes to the STDOUT constant, which differs from $stdout when $stdout was replaced
-
10
::SemanticLogger.appenders.console_output? &&
-
::ActiveSupport::Logger.logger_outputs_to?(logger, $stdout, $stderr, STDOUT, STDERR) # rubocop:disable Style/GlobalStdStream
-
end
-
end
-
-
# Proxy object to provide ActiveJob-compatible formatter interface
-
# Also implements the standard Logger formatter interface (call method)
-
# for compatibility with Ruby's Logger (especially logger gem 1.7.0+)
-
2
class FormatterProxy < ::Logger::Formatter
-
2
extend T::Sig
-
-
3
sig { returns(T::Array[T.any(String, Symbol)]) }
-
2
def current_tags
-
15
Thread.current[:activesupport_tagged_logging_tags] || []
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module SemanticLogger
-
# SemanticLogger writes lines from a background thread, and a forked child doesn't get its parent's threads, so
-
# lines logged in Puma, Unicorn, Passenger or Pitchfork workers, Resque jobs and other forked children were never
-
# written. Ruby calls Process._fork for every fork, so each child reopens the appenders (a new writer thread).
-
2
module ReopenAfterFork
-
2
extend T::Sig
-
-
2
@installed = T.let(false, T::Boolean)
-
-
4
sig { void }
-
2
def self.install!
-
3
return if @installed
-
2
@installed = true
-
-
2
Process.singleton_class.prepend(ForkHook)
-
end
-
-
2
module ForkHook
-
2
extend T::Sig
-
-
3
sig { returns(Integer) }
-
2
def _fork
-
4
pid = T.let(super, Integer)
-
4
if pid.zero?
-
begin
-
::SemanticLogger.reopen
-
rescue => e
-
LogStruct.handle_exception(e, source: LogStruct::Source::Internal, context: {integration: :semantic_logger})
-
end
-
end
-
4
pid
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "semantic_logger"
-
2
require_relative "formatter"
-
2
require_relative "color_formatter"
-
2
require_relative "logger"
-
2
require_relative "reopen_after_fork"
-
-
2
module LogStruct
-
# LogStruct writes its lines through SemanticLogger. It replaces Rails.logger and the framework loggers with a
-
# SemanticLogger logger whose formatter writes LogStruct's JSON lines. SemanticLogger formats and writes each line on
-
# a background thread, so a request or job doesn't wait for the write, and it can write to several appenders
-
# (STDOUT, a file, a log service) at once.
-
2
module SemanticLogger
-
# Sets up SemanticLogger for a Rails app: its level, appenders and formatters, and the loggers it replaces
-
2
module Setup
-
2
extend T::Sig
-
-
# Load hooks for the framework components whose loggers LogStruct replaces
-
2
COMPONENT_LOAD_HOOKS = T.let(
-
%i[active_record action_controller action_mailer active_job action_view action_cable].freeze,
-
T::Array[Symbol]
-
)
-
-
# The logger LogStruct installed, and the loggers it replaced
-
2
@logger = T.let(nil, T.nilable(LogStruct::SemanticLogger::Logger))
-
2
@original_rails_logger = T.let(nil, T.untyped)
-
2
@original_log_subscriber_logger = T.let(nil, T.untyped)
-
2
@original_app_logger = T.let(nil, T.untyped)
-
# The appenders add_appenders added. LogStruct replaces only these, and keeps any an app adds.
-
2
@appenders = T.let([], T::Array[T.untyped])
-
-
4
sig { returns(T::Boolean) }
-
2
def self.configured?
-
2
!@logger.nil?
-
end
-
-
# Configures SemanticLogger and replaces Rails' loggers with LogStruct's:
-
# - the level from config.log_level (info in production, debug elsewhere)
-
# - the appenders for the environment (see add_appenders)
-
# - Rails.logger and the framework loggers (ActiveRecord, ActionController, ActiveJob, ...)
-
#
-
# @param app [Rails::Application] The Rails application instance
-
4
sig { params(app: T.untyped).void }
-
2
def self.configure_semantic_logger(app)
-
# Set SemanticLogger configuration
-
2
::SemanticLogger.application = Rails.application.class.module_parent_name
-
2
::SemanticLogger.environment = Rails.env
-
-
# Determine log level from Rails config
-
2
log_level = determine_log_level(app)
-
2
::SemanticLogger.default_level = log_level
-
-
# Add appropriate appenders based on environment
-
2
remove_appenders
-
2
add_appenders(app)
-
2
ReopenAfterFork.install!
-
-
# Replace Rails.logger with SemanticLogger
-
2
replace_rails_logger(app)
-
end
-
-
# Adds the appenders again, with the settings config/initializers changed after the logger was set up
-
# (prefer_json_in_development, enable_color_output, color_map, filter_noisy_loggers, local_environments).
-
# Appenders the app added in config/initializers stay.
-
4
sig { params(app: T.untyped).void }
-
2
def self.reconfigure_appenders(app)
-
4
remove_appenders
-
4
add_appenders(app)
-
end
-
-
# Removes the appenders add_appenders added, after writing the lines already queued for them
-
4
sig { void }
-
2
def self.remove_appenders
-
6
return if @appenders.empty?
-
-
4
::SemanticLogger.flush
-
10
@appenders.each { |appender| ::SemanticLogger.remove_appender(appender) }
-
4
@appenders = []
-
end
-
-
4
sig { params(app: T.untyped).returns(Symbol) }
-
2
def self.determine_log_level(app)
-
2
level = if app.config.log_level
-
2
app.config.log_level
-
elsif Rails.env.production?
-
:info
-
elsif Rails.env.test?
-
:debug
-
else
-
:debug
-
end
-
# Rails config.log_level can be a String or Symbol
-
2
level.is_a?(String) ? level.to_sym : level
-
end
-
-
4
sig { params(app: T.untyped).void }
-
2
def self.add_appenders(app)
-
11
config = LogStruct.config
-
-
# Determine output destination
-
11
io = determine_output(app)
-
-
# Ruby buffers STDOUT when it isn't a terminal, so in a container lines waited until the buffer
-
# filled or the process exited. (GoodJob's executable only syncs STDOUT when it writes its own
-
# text logs there.)
-
11
io.sync = true if io.equal?($stdout)
-
-
11
appenders = []
-
11
appenders << if Rails.env.development?
-
1
if config.prefer_json_in_development
-
# Default to production-style JSON in development when enabled
-
::SemanticLogger.add_appender(
-
io: io,
-
formatter: LogStruct::SemanticLogger::Formatter.new,
-
filter: determine_filter
-
)
-
1
elsif config.enable_color_output
-
# Opt-in colorful human formatter in development
-
1
::SemanticLogger.add_appender(
-
io: io,
-
formatter: LogStruct::SemanticLogger::ColorFormatter.new(
-
color_map: config.color_map
-
),
-
filter: determine_filter
-
)
-
else
-
::SemanticLogger.add_appender(
-
io: io,
-
formatter: LogStruct::SemanticLogger::Formatter.new,
-
filter: determine_filter
-
)
-
end
-
else
-
# Use our custom JSON formatter in non-development environments
-
10
::SemanticLogger.add_appender(
-
io: io,
-
formatter: LogStruct::SemanticLogger::Formatter.new,
-
filter: determine_filter
-
)
-
end
-
-
# Log to Rails' log file too in development and test (config.local_environments), unless
-
# RAILS_LOG_TO_STDOUT says not to. Production and other servers log to STDOUT only: nothing
-
# rotates the file, and in a container it only fills the disk.
-
11
local = config.local_environments.include?(Rails.env.to_sym)
-
11
if local && app.config.paths["log"].first && ENV["RAILS_LOG_TO_STDOUT"].blank?
-
5
appenders << ::SemanticLogger.add_appender(
-
file_name: app.config.paths["log"].first,
-
formatter: LogStruct::SemanticLogger::Formatter.new,
-
filter: determine_filter
-
)
-
end
-
# add_appender returns nil when it ignores a second STDOUT appender
-
11
@appenders = appenders.compact
-
end
-
-
4
sig { params(app: T.untyped).returns(T.untyped) }
-
2
def self.determine_output(app)
-
# Always honor explicit STDOUT directive
-
11
return $stdout if ENV["RAILS_LOG_TO_STDOUT"].present?
-
-
6
if Rails.env.test?
-
# Use StringIO in test to keep stdout clean
-
5
StringIO.new
-
else
-
# Use STDOUT for app logs in dev/production
-
1
$stdout
-
end
-
end
-
-
2
NOISY_LOGGERS = /\A(ActionView|ActionController::RoutingError|ActiveRecord::SchemaMigration)/
-
-
# With filter_noisy_loggers on, an appender filter that leaves out NOISY_LOGGERS' lines. SemanticLogger
-
# writes a line when a Proc filter returns true (a Regexp filter would keep only the names it matches).
-
4
sig { returns(T.nilable(T.proc.params(log: ::SemanticLogger::Log).returns(T::Boolean))) }
-
2
def self.determine_filter
-
17
return nil unless LogStruct.config.filter_noisy_loggers
-
-
3
->(log) { !NOISY_LOGGERS.match?(log.name.to_s) }
-
end
-
-
# Replaces Rails.logger, ActiveSupport::LogSubscriber.logger, config.logger and the framework
-
# loggers (ActiveRecord, ActionController, ActionMailer, ActiveJob, ActionView, ActionCable)
-
# with LogStruct's SemanticLogger logger, so every Rails line is a LogStruct JSON line
-
#
-
# @param app [Rails::Application] The Rails application instance
-
4
sig { params(app: T.untyped).void }
-
2
def self.replace_rails_logger(app)
-
# Create new SemanticLogger instance
-
2
logger = LogStruct::SemanticLogger::Logger.new("Rails")
-
2
@logger = logger
-
2
@original_rails_logger = Rails.logger
-
2
@original_log_subscriber_logger = ActiveSupport::LogSubscriber.logger
-
2
@original_app_logger = app.config.logger
-
-
# Replace Rails.logger
-
2
Rails.logger = logger
-
-
# Rails assigns ActiveSupport::LogSubscriber.logger once during boot
-
# (in the active_support.set_log_subscriber_logger initializer), so log
-
# subscribers such as Lograge keep writing to the original boot logger
-
# unless this reference is updated along with Rails.logger.
-
2
ActiveSupport::LogSubscriber.logger = logger
-
-
# Also replace the component loggers. Use load hooks rather than referencing the base
-
# classes: loading them this early would apply their on_load hooks before the app's
-
# initializers run, e.g. ActiveRecord would copy an empty filter_parameters into
-
# filter_attributes.
-
2
COMPONENT_LOAD_HOOKS.each do |hook|
-
21
ActiveSupport.on_load(hook) { self.logger = logger }
-
end
-
-
# Store reference in app config
-
2
app.config.logger = logger
-
end
-
-
# Puts back the loggers that replace_rails_logger replaced. Used when an initializer
-
# disables LogStruct after the logger was replaced at boot.
-
2
sig { params(app: T.untyped).void }
-
2
def self.restore_rails_logger(app)
-
logger = @logger
-
return unless logger
-
-
original_logger = @original_rails_logger
-
Rails.logger = original_logger
-
ActiveSupport::LogSubscriber.logger = @original_log_subscriber_logger
-
app.config.logger = @original_app_logger
-
remove_appenders
-
-
# These hooks run after the ones that installed LogStruct's logger, including for
-
# components that are already loaded
-
COMPONENT_LOAD_HOOKS.each do |hook|
-
ActiveSupport.on_load(hook) do
-
self.logger = original_logger if self.logger.equal?(logger)
-
end
-
end
-
-
@logger = nil
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "../enums/log_field"
-
2
require_relative "interfaces/request_fields"
-
-
2
module LogStruct
-
2
module Log
-
2
module Shared
-
2
module AddRequestFields
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
requires_ancestor { Interfaces::RequestFields }
-
-
2
sig { params(hash: T::Hash[Symbol, T.untyped]).void }
-
2
def add_request_fields(hash)
-
hash[LogField::Path.serialize] = path if path
-
hash[LogField::HttpMethod.serialize] = http_method if http_method
-
hash[LogField::SourceIp.serialize] = source_ip if source_ip
-
hash[LogField::UserAgent.serialize] = user_agent if user_agent
-
hash[LogField::Referer.serialize] = referer if referer
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# Moved from lib/log_struct/log/interfaces/additional_data_field.rb
-
2
module LogStruct
-
2
module Log
-
2
module Interfaces
-
2
module AdditionalDataField
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
interface!
-
-
2
requires_ancestor { T::Struct }
-
-
2
sig { abstract.returns(T.nilable(T::Hash[T.any(String, Symbol), T.untyped])) }
-
2
def additional_data
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "../../enums/level"
-
-
2
module LogStruct
-
2
module Log
-
2
module Interfaces
-
2
module CommonFieldBase
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
interface!
-
-
2
sig { abstract.returns(Level) }
-
2
def level
-
end
-
-
2
sig { abstract.returns(Time) }
-
2
def timestamp
-
end
-
-
3
sig { abstract.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
-
2
def serialize(strict = true)
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "../../enums/source"
-
2
require_relative "../../enums/event"
-
2
require_relative "../../enums/level"
-
2
require_relative "common_field_base"
-
-
2
module LogStruct
-
2
module Log
-
2
module Interfaces
-
2
module CommonFields
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
interface!
-
-
2
include CommonFieldBase
-
-
2
sig { abstract.returns(Source) }
-
2
def source
-
end
-
-
2
sig { abstract.returns(Event) }
-
2
def event
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "../../enums/level"
-
2
require_relative "common_field_base"
-
-
2
module LogStruct
-
2
module Log
-
2
module Interfaces
-
2
module PublicCommonFields
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
interface!
-
-
2
include CommonFieldBase
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Log
-
2
module Interfaces
-
2
module RequestFields
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
interface!
-
-
2
sig { abstract.returns(T.nilable(String)) }
-
2
def path
-
end
-
-
2
sig { abstract.returns(T.nilable(String)) }
-
2
def http_method
-
end
-
-
2
sig { abstract.returns(T.nilable(String)) }
-
2
def source_ip
-
end
-
-
2
sig { abstract.returns(T.nilable(String)) }
-
2
def user_agent
-
end
-
-
2
sig { abstract.returns(T.nilable(String)) }
-
2
def referer
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "interfaces/additional_data_field"
-
2
require_relative "../enums/source"
-
-
2
module LogStruct
-
2
module Log
-
2
module Shared
-
2
module MergeAdditionalDataFields
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
requires_ancestor { T::Struct }
-
2
requires_ancestor { Interfaces::AdditionalDataField }
-
-
# Reserved keys that cannot be overwritten by additional_data.
-
# These are the core log structure fields that must not be modified.
-
2
RESERVED_KEYS = T.let(%i[src evt lvl ts].freeze, T::Array[Symbol])
-
-
4
sig { params(hash: T::Hash[Symbol, T.untyped]).void }
-
2
def merge_additional_data_fields(hash)
-
1573
ad = additional_data
-
1573
return unless ad
-
49
ad.each do |key, value|
-
55
sym_key = key.to_sym
-
55
if RESERVED_KEYS.include?(sym_key)
-
10
LogStruct.handle_exception(
-
ArgumentError.new("additional_data attempted to overwrite reserved key: #{sym_key}"),
-
source: Source::Internal
-
)
-
5
next
-
end
-
45
hash[sym_key] = value
-
end
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "../enums/log_field"
-
2
require_relative "interfaces/common_fields"
-
2
require_relative "merge_additional_data_fields"
-
-
2
module LogStruct
-
2
module Log
-
2
module Shared
-
2
module SerializeCommon
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
requires_ancestor { Interfaces::CommonFields }
-
-
4
sig { params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
-
2
def serialize(strict = true)
-
# Start with shared fields (source, event, level, timestamp)
-
1718
out = serialize_common(strict)
-
-
# Merge event/base fields from the struct-specific hash
-
1718
kernel_self = T.cast(self, Kernel)
-
1718
field_hash = T.cast(kernel_self.public_send(:to_h), T::Hash[LogStruct::LogField, T.untyped])
-
1718
field_hash.each do |log_field, value|
-
3321
next if value.nil?
-
3321
key = log_field.serialize
-
# (The formatter cuts a backtrace to config.limits.max_backtrace_lines)
-
3321
out[key] = value.is_a?(::Time) ? value.iso8601(3) : value
-
end
-
-
# Merge any additional_data at top level if available
-
1718
if kernel_self.respond_to?(:merge_additional_data_fields)
-
# merge_additional_data_fields expects symbol keys
-
1568
merge_target = T.cast(self, LogStruct::Log::Shared::MergeAdditionalDataFields)
-
1568
merge_target.merge_additional_data_fields(out)
-
end
-
-
1714
out
-
end
-
-
4
sig { params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
-
2
def serialize_common(strict = true)
-
{
-
1718
LogField::Source.serialize => source.serialize.to_s,
-
LogField::Event.serialize => event.serialize.to_s,
-
LogField::Level.serialize => level.serialize.to_s,
-
LogField::Timestamp.serialize => timestamp.iso8601(3)
-
}
-
end
-
-
3
sig { params(options: T.untyped).returns(T::Hash[String, T.untyped]) }
-
2
def as_json(options = nil)
-
7
serialize.transform_keys(&:to_s)
-
end
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require_relative "../enums/log_field"
-
2
require_relative "interfaces/public_common_fields"
-
-
2
module LogStruct
-
2
module Log
-
# Common serialization for public custom log structs with string/symbol source/event
-
2
module SerializeCommonPublic
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
requires_ancestor { Interfaces::PublicCommonFields }
-
2
requires_ancestor { Kernel }
-
-
3
sig { params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
-
2
def serialize_common_public(strict = true)
-
5
unless respond_to?(:source) && respond_to?(:event)
-
raise ArgumentError, "Public log struct must define #source and #event"
-
end
-
-
5
src_val = public_send(:source)
-
5
evt_val = public_send(:event)
-
5
src = src_val.respond_to?(:serialize) ? src_val.public_send(:serialize).to_s : src_val.to_s
-
5
evt = evt_val.respond_to?(:serialize) ? evt_val.public_send(:serialize).to_s : evt_val.to_s
-
5
lvl = level.serialize.to_s
-
5
ts = timestamp.iso8601(3)
-
-
{
-
5
LogField::Source.serialize => src,
-
LogField::Event.serialize => evt,
-
LogField::Level.serialize => lvl,
-
LogField::Timestamp.serialize => ts
-
}
-
end
-
-
3
sig { params(options: T.untyped).returns(T::Hash[String, T.untyped]) }
-
2
def as_json(options = nil)
-
1
serialize.transform_keys(&:to_s)
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
# Note: We use T::Struct for our Log classes so Sorbet is a hard requirement,
-
# not an optional dependency.
-
2
require "sorbet-runtime"
-
2
require "log_struct/sorbet/serialize_symbol_keys"
-
-
# Don't extend T::Sig to all modules! We're just a library, not a private Rails application
-
# See: https://sorbet.org/docs/sigs
-
# class Module
-
# include T::Sig
-
# end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
module LogStruct
-
2
module Sorbet
-
2
module SerializeSymbolKeys
-
2
extend T::Sig
-
2
extend T::Helpers
-
-
2
requires_ancestor { T::Struct }
-
-
3
sig { params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
-
2
def serialize(strict = true)
-
4
super.deep_symbolize_keys
-
end
-
-
2
sig { returns(T::Hash[Symbol, T.untyped]) }
-
2
def to_h
-
serialize
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
2
require "base64"
-
2
require "ipaddr"
-
2
require "uri"
-
2
require_relative "hash_utils"
-
2
require_relative "param_filters"
-
-
2
module LogStruct
-
# StringScrubber is inspired by logstop by @ankane: https://github.com/ankane/logstop
-
# Enhancements:
-
# - Shows which type of data was filtered
-
# - Includes a hash of each filtered email (HashUtils) for request tracing
-
# - Uses configuration options from LogStruct.config
-
2
module StringScrubber
-
2
class << self
-
2
extend T::Sig
-
-
# The password in a URL's user info (postgres://user:password@host, redis://:password@host).
-
# The user ends at its first colon; the password runs to the last @ before the host.
-
2
URL_PASSWORD_REGEX = %r{(//[^\s/:@]*:)[^\s/]+(@)}
-
# The same in a URL-encoded URL, like https%3A%2F%2Fuser%3Asecret%40example.com
-
# cspell:ignore Fuser Asecret
-
2
ENCODED_URL_PASSWORD_REGEX = %r{(%2F%2F(?:(?!%3A|%2F|%40)[^\s/&])*%3A)(?:(?!%2F)[^\s/&])+(%40)}i
-
2
URL_PASSWORD_REPLACEMENT = '\1[PASSWORD]\2'
-
# A URL's user info that runs past what the scrubbers saw of a long string, where the cut can separate the
-
# password from the @ that closes it. Everything after the user's colon goes: the password can contain an
-
# @ of its own (p@ss).
-
2
CUT_URL_PASSWORD_REGEX = %r{(//[^\s/:@]*:)[^\s/"'<>]*\z}
-
2
CUT_ENCODED_URL_PASSWORD_REGEX = %r{(%2F%2F(?:(?!%3A|%2F|%40)[^\s/&])*%3A)(?:(?!%2F)[^\s/&"'<>])*\z}i
-
2
CUT_URL_PASSWORD_REPLACEMENT = '\1[PASSWORD]'
-
# Basic credentials that run past what the scrubbers saw: a cut token doesn't decode, so it can't be checked
-
2
CUT_BASIC_CREDENTIALS_REGEX = %r{\b(Basic\s+)[A-Za-z0-9+/]+=*\z}i
-
-
# A URL's query string, for the sensitive params in it
-
2
QUERY_STRING_REGEX = /\?([^\s#"'<>]+)/
-
# OAuth authorization codes, which no key pattern would filter
-
2
SENSITIVE_QUERY_PARAMS = T.let(["code"].freeze, T::Array[String])
-
2
QUERY_PARAM_REPLACEMENT = "[FILTERED]"
-
-
# Letters and digits of any script (an accented name, a domain in another alphabet)
-
2
EMAIL_REGEX = /(?<![\p{L}\p{N}._%+-])[\p{L}\p{N}._%+-]+@[\p{L}\p{N}.-]+\.\p{L}{2,}(?![\p{L}\p{N}])/
-
# A whole String that is an email address
-
2
WHOLE_EMAIL_REGEX = /\A#{EMAIL_REGEX}\z/
-
-
# The credentials after Bearer or Basic, as in an Authorization header. A Bearer token has at least 16
-
# characters, and Basic credentials must decode to user:password, so "Bearer token" and "Basic plan" stay.
-
2
AUTHORIZATION_CREDENTIALS_REGEX = %r{\b(Bearer|Basic)(\s+)([A-Za-z0-9\-._~+/]+=*)}i
-
2
AUTHORIZATION_CREDENTIALS_REPLACEMENT = "[FILTERED]"
-
-
# 13 to 19 digits, written together, in fours (4111 1111 1111 1111, with a 19-digit card's last
-
# three) or in American Express and Diners Club groups (3782 822463 10005). Numbers that fail
-
# the Luhn check are left alone, so most IDs and timestamps are too.
-
2
CREDIT_CARD_REGEX = /
-
(?<![A-Za-z0-9.-])
-
(?:
-
[2-6]\d{12,18}
-
| [2-6]\d{3}(?<sep>[\ -])\d{4}\k<sep>\d{4}\k<sep>\d{4}(?:\k<sep>\d{3})?
-
| 3\d{3}(?<sep2>[\ -])\d{6}\k<sep2>\d{4,5}
-
)
-
(?![A-Za-z0-9]|-\d)
-
/x
-
2
CREDIT_CARD_REPLACEMENT = "[CREDIT_CARD]"
-
-
# US numbers: 555-123-4567, 555 123 4567, 555.123.4567 and (555) 123-4567, with or without +1 or 1
-
2
PHONE_REGEX = /(?<![\w+])(?:\+?1[\s.-]?)?(?:\(\d{3}\)[\s.-]?|\d{3}[\s.-])\d{3}[\s.-]\d{4}(?!\w)/
-
2
PHONE_REPLACEMENT = "[PHONE]"
-
-
# 123-45-6789 and 123 45 6789, but not inside a longer number (9-123-45-6789, 123-45-6789-0)
-
2
SSN_REGEX = /(?<!\w|\d-)\d{3}[\s-]\d{2}[\s-]\d{4}(?!\w|-\d)/
-
2
SSN_REPLACEMENT = "[SSN]"
-
-
2
IP_REGEX = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/
-
# Text that could be an IPv6 address (2001:db8::1, ::ffff:192.168.1.1, fe80::1%en0 with its zone). IPAddr
-
# decides whether it is one, and it must have a digit, so Ruby constants (Face::Bead) and times (12:30:45)
-
# stay.
-
2
IPV6_CANDIDATE_REGEX = /(?<![\w:.])(?=[\h:.]*\d)(?:\h{0,4}:){2,7}(?:\d{1,3}(?:\.\d{1,3}){3}|\h{1,4})?(?:%\w+)?(?![\w:])/
-
2
IP_REPLACEMENT = "[IP]"
-
-
# Six pairs of hex digits, separated by colons or by dashes
-
2
MAC_REGEX = /\b[0-9a-f]{2}([:-])[0-9a-f]{2}(?:\1[0-9a-f]{2}){4}\b/i
-
2
MAC_REPLACEMENT = "[MAC]"
-
-
# Hash keys and Symbols like these are never changed by the scrubbers, so they're skipped
-
2
IDENTIFIER_REGEX = /\A[A-Za-z_][A-Za-z0-9_]*\z/
-
-
# Scrub sensitive information from a string
-
4
sig { params(string: String).returns(String) }
-
2
def scrub(string)
-
12958
return string if string.empty?
-
-
# The scrubbers match letters of any script, which needs valid UTF-8
-
12949
string = valid_utf8(string.to_s).dup
-
12949
config = LogStruct.config.filters
-
-
12949
scrub_authorization_credentials!(string) if config.authorization_credentials
-
-
# Sensitive params in URLs' query strings
-
12949
scrub_query_params!(string) if config.url_query_params && string.include?("?")
-
-
# Passwords in URLs
-
12949
if config.url_passwords
-
12945
string.gsub!(URL_PASSWORD_REGEX, URL_PASSWORD_REPLACEMENT)
-
12945
string.gsub!(ENCODED_URL_PASSWORD_REGEX, URL_PASSWORD_REPLACEMENT) if string.include?("%")
-
end
-
-
# Emails
-
12949
if config.email_addresses
-
13249
string.gsub!(EMAIL_REGEX) { |email| "[EMAIL:#{value_hash(email)}]" }
-
end
-
-
# Credit card numbers
-
12949
if config.credit_card_numbers
-
12943
string.gsub!(CREDIT_CARD_REGEX) do |number|
-
24
luhn_valid?(number.delete(" -")) ? CREDIT_CARD_REPLACEMENT : number
-
end
-
end
-
-
# Phone numbers
-
12949
string.gsub!(PHONE_REGEX, PHONE_REPLACEMENT) if config.phone_numbers
-
-
# SSNs
-
12949
string.gsub!(SSN_REGEX, SSN_REPLACEMENT) if config.ssns
-
-
# IPs: IPv6 first, which can end in an IPv4 address
-
12949
if config.ip_addresses
-
225
scrub_ipv6_addresses!(string) if string.include?(":")
-
225
string.gsub!(IP_REGEX, IP_REPLACEMENT)
-
end
-
-
# MAC addresses
-
12949
string.gsub!(MAC_REGEX, MAC_REPLACEMENT) if config.mac_addresses
-
-
# Custom scrubber
-
12949
custom_scrubber = LogStruct.config.string_scrubbing_handler
-
12949
string = custom_scrubber.call(string) if !custom_scrubber.nil?
-
-
12949
string
-
end
-
-
# The end of the part of a long string the scrubbers saw (Formatter#scrub_limited_string). They saw only the
-
# start of a value that runs past it, so a URL password separated from its closing @ (which
-
# URL_PASSWORD_REGEX needs) and Basic credentials cut short (which no longer decode) are replaced, however
-
# long they were.
-
3
sig { params(string: String).returns(String) }
-
2
def scrub_cut_end(string)
-
6
filters = LogStruct.config.filters
-
6
if filters.url_passwords
-
6
string = string.sub(CUT_URL_PASSWORD_REGEX, CUT_URL_PASSWORD_REPLACEMENT)
-
.sub(CUT_ENCODED_URL_PASSWORD_REGEX, CUT_URL_PASSWORD_REPLACEMENT)
-
end
-
6
if filters.authorization_credentials
-
6
string = string.sub(CUT_BASIC_CREDENTIALS_REGEX, "\\1#{AUTHORIZATION_CREDENTIALS_REPLACEMENT}")
-
end
-
6
string
-
end
-
-
# The hash of a value (HashUtils.hash_value). Email addresses are hashed in lowercase, so an address has
-
# one hash however it's written, and the same hash under a filtered key as in a scrubbed string.
-
3
sig { params(value: String).returns(String) }
-
2
def value_hash(value)
-
346
HashUtils.hash_value(WHOLE_EMAIL_REGEX.match?(value) ? value.downcase : value)
-
end
-
-
# A String as valid UTF-8: binary and other encodings are converted, and bytes that aren't valid
-
# become U+FFFD
-
4
sig { params(string: String).returns(String) }
-
2
def valid_utf8(string)
-
37885
return string if string.ascii_only? || (string.encoding == Encoding::UTF_8 && string.valid_encoding?)
-
-
4
utf8 = if string.encoding == Encoding::BINARY || string.encoding == Encoding::US_ASCII
-
3
string.dup.force_encoding(Encoding::UTF_8)
-
else
-
begin
-
1
string.encode(Encoding::UTF_8)
-
rescue EncodingError
-
string.dup.force_encoding(Encoding::UTF_8)
-
end
-
end
-
4
utf8.valid_encoding? ? utf8 : utf8.scrub
-
end
-
-
# A Hash key or a Symbol, scrubbed like a String unless it's a plain identifier (user_id,
-
# createdAt). Returns it unchanged, or the scrubbed String when scrubbing changed it.
-
4
sig { params(name: T.untyped).returns(T.untyped) }
-
2
def scrub_name(name)
-
12334
return name unless name.is_a?(String) || name.is_a?(Symbol)
-
-
12332
string = name.to_s
-
12332
return name if IDENTIFIER_REGEX.match?(string)
-
-
9
scrubbed = scrub(string)
-
9
(scrubbed == string) ? name : scrubbed
-
end
-
-
# The URL without its query string and fragment (for Referer headers)
-
3
sig { params(url: T.nilable(String)).returns(T.nilable(String)) }
-
2
def without_query(url)
-
23
return url if url.nil?
-
-
4
url.split(/[?#]/, 2).first || ""
-
end
-
-
2
private
-
-
4
sig { params(string: String).void }
-
2
def scrub_ipv6_addresses!(string)
-
80
string.gsub!(IPV6_CANDIDATE_REGEX) do |candidate|
-
# The zone (an interface's name) is replaced with its address
-
13
ipv6_address?(candidate.split("%", 2).first.to_s) ? IP_REPLACEMENT : candidate
-
end
-
end
-
-
3
sig { params(candidate: String).returns(T::Boolean) }
-
2
def ipv6_address?(candidate)
-
13
::IPAddr.new(candidate).ipv6?
-
rescue ::IPAddr::Error
-
3
false
-
end
-
-
# Bearer tokens and Basic credentials (Authorization: Bearer eyJ... becomes Bearer [FILTERED])
-
4
sig { params(string: String).void }
-
2
def scrub_authorization_credentials!(string)
-
12946
return unless string.match?(/bearer|basic/i)
-
-
31
string.gsub!(AUTHORIZATION_CREDENTIALS_REGEX) do |match|
-
12
scheme = Regexp.last_match(1).to_s
-
12
credentials = Regexp.last_match(3).to_s
-
12
if authorization_credentials?(scheme, credentials)
-
4
"#{scheme}#{Regexp.last_match(2)}#{AUTHORIZATION_CREDENTIALS_REPLACEMENT}"
-
else
-
8
match
-
end
-
end
-
end
-
-
3
sig { params(scheme: String, credentials: String).returns(T::Boolean) }
-
2
def authorization_credentials?(scheme, credentials)
-
12
return credentials.length >= 16 if scheme.casecmp?("bearer")
-
-
8
credentials.length >= 8 && Base64.strict_decode64(credentials).include?(":")
-
rescue ArgumentError
-
1
false
-
end
-
-
# Replaces the values of query params whose names would be filtered as keys
-
# (/reset?token=abc&page=2 becomes /reset?token=[FILTERED]&page=2)
-
4
sig { params(string: String).void }
-
2
def scrub_query_params!(string)
-
18
string.gsub!(QUERY_STRING_REGEX) do
-
14
query = Regexp.last_match(1).to_s
-
14
params = query.split("&", -1).map do |param|
-
22
name, value = param.split("=", 2)
-
22
next param if value.nil? || value.empty? || value == QUERY_PARAM_REPLACEMENT
-
-
18
sensitive_query_param?(name.to_s, value) ? "#{name}=#{QUERY_PARAM_REPLACEMENT}" : param
-
end
-
14
"?#{params.join("&")}"
-
end
-
end
-
-
# user[password] is checked as password, written plainly or URL-encoded
-
3
sig { params(name: String, value: String).returns(T::Boolean) }
-
2
def sensitive_query_param?(name, value)
-
decoded = begin
-
18
URI.decode_www_form_component(name)
-
rescue ArgumentError
-
name
-
end
-
18
key = decoded[/\[([^\[\]]*)\]\z/, 1] || decoded
-
18
return false if key.empty?
-
-
18
SENSITIVE_QUERY_PARAMS.include?(key.downcase) || ParamFilters.should_filter_key?(key, value)
-
end
-
-
# The Luhn checksum that every card number passes
-
3
sig { params(digits: String).returns(T::Boolean) }
-
2
def luhn_valid?(digits)
-
24
sum = 0
-
24
digits.reverse.each_char.with_index do |char, index|
-
379
digit = char.to_i
-
379
if index.odd?
-
187
digit *= 2
-
187
digit -= 9 if digit > 9
-
end
-
379
sum += digit
-
end
-
24
(sum % 10).zero?
-
end
-
end
-
end
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
1
require "log_struct"
-
# Add your own tasks in files placed in lib/tasks ending in .rake,
-
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
-
-
1
require_relative "config/application"
-
-
1
Rails.application.load_tasks
-
# typed: strict
-
# frozen_string_literal: true
-
-
1
class ApplicationController < ActionController::Base
-
end
-
# typed: true
-
# frozen_string_literal: true
-
-
# The app's own error pages, for config.exceptions_app = routes (ErrorPagesTest turns them on). Rails sends the
-
# request here as GET /<status>.
-
1
class ErrorsController < ApplicationController
-
1
def show
-
3
status = request.path_info.delete_prefix("/").to_i
-
3
render json: {status: status}, status: status
-
end
-
end
-
# typed: true
-
# frozen_string_literal: true
-
-
1
class LoggingController < ApplicationController
-
# Basic logging
-
1
def test_basic
-
# Test standard Rails logging - this is the primary usage pattern
-
12
Rails.logger.info("Info level message")
-
12
Rails.logger.warn("Warning level message")
-
12
Rails.logger.debug("Debug level message with context")
-
-
# For structured data, use LogStruct's Log::Plain
-
12
plain_log = LogStruct::Log::Plain.new(
-
message: "Structured log message",
-
source: LogStruct::Source::App
-
)
-
12
Rails.logger.info(plain_log)
-
-
# Test email scrubbing in plain string
-
12
Rails.logger.info("User email is test@example.com and password is secret123")
-
-
12
render json: {status: "ok", message: "Basic logging completed"}
-
end
-
-
# Reads the request's params
-
1
def test_params
-
1
render json: {keys: params.keys}
-
end
-
-
# Error logging
-
1
def test_error
-
# Since the tests run in the test environment and Rails' test behavior may catch exceptions
-
# differently, let's log the error but also raise it to ensure it's properly captured
-
3
Rails.logger.info("About to raise test error")
-
begin
-
3
raise "Test error for integration testing"
-
rescue => e
-
# Log the error first
-
3
error_log = LogStruct::Log::Error.new(
-
source: LogStruct::Source::App,
-
error_class: e.class,
-
message: e.message
-
)
-
3
Rails.logger.error(error_log)
-
-
# Then re-raise it for the test to catch
-
3
raise
-
end
-
end
-
-
# Custom log structures
-
1
def test_custom
-
# Create and log a custom log structure
-
1
5.times do |i|
-
5
custom_log = LogStruct::Log::Plain.new(
-
message: "Custom log message #{i}",
-
source: LogStruct::Source::App,
-
additional_data: {
-
iteration: i,
-
timestamp: Time.now.to_f,
-
random: rand(100)
-
}
-
)
-
5
Rails.logger.info(custom_log)
-
end
-
-
1
render json: {status: "ok", message: "Custom logging completed"}
-
end
-
-
# Request logging test - DO NOT MODIFY THIS METHOD
-
# This method INTENTIONALLY reproduces the SystemStackError issue
-
# which must be fixed in the LogStruct codebase itself.
-
1
def test_request
-
# This is exactly the code that was causing the infinite recursion issue
-
# We need to fix the library - not modify this test!
-
1
request_log = LogStruct::Log::Request.new(
-
http_method: "GET",
-
path: "/api/users",
-
status: 200,
-
duration_ms: 15.5,
-
source_ip: "127.0.0.1"
-
)
-
1
Rails.logger.info(request_log)
-
-
1
render json: {status: "ok", message: "Request logging completed"}
-
end
-
-
# Model-related logging
-
1
def test_model
-
# Create a test user to trigger ActiveRecord logging
-
user = User.create!(name: "Test User", email: "user@example.com")
-
# Simple string logging
-
Rails.logger.info("Created user #{user.id}")
-
-
# Get the existing user
-
found_user = User.find(user.id)
-
Rails.logger.info("Found user: #{found_user.name}")
-
-
render json: {status: "ok", message: "Model logging completed", user_id: user.id}
-
end
-
-
# Job-related logging
-
1
def test_job
-
# Enqueue a job to test ActiveJob integration
-
job = TestJob.perform_later("test_argument")
-
Rails.logger.info("Job enqueued with ID: #{job.job_id}")
-
-
# LogStruct will automatically enhance job enqueued/performed logs
-
render json: {status: "ok", message: "Job enqueued for testing", job_id: job.job_id}
-
end
-
-
# Context and tagging
-
1
def test_context
-
# TODO: Fix types for the tagged method
-
# Test Rails' built-in tagged logging
-
Rails.logger.tagged("REQUEST_ID_123", "USER_456") do
-
Rails.logger.info("Message with tags")
-
-
# Nested tags
-
Rails.logger.tagged("NESTED") do
-
Rails.logger.warn("Message with nested tags")
-
end
-
end
-
-
# Message without tags
-
Rails.logger.info("Message without tags")
-
-
render json: {status: "ok", message: "Context logging completed"}
-
end
-
-
1
def test_error_logging
-
# Also test error handling in formatter by logging to trigger fallback handlers
-
begin
-
# Raise an error
-
1
raise "Test error for recursion safety"
-
rescue => e
-
# Log the error, which would trigger the formatter code
-
1
Rails.logger.error("Error occurred: #{e.message}")
-
-
# Also try structured error logging
-
1
error_log = LogStruct::Log::Error.new(
-
source: LogStruct::Source::App,
-
message: e.message,
-
error_class: e.class
-
)
-
1
Rails.logger.error(error_log)
-
end
-
-
# If we got here without a SystemStackError, the infinite recursion was prevented
-
1
render json: {status: "ok", message: "Stack-safe error handling test completed"}
-
end
-
-
# AMS serialization - triggers ActiveModelSerializers instrumentation
-
1
def test_ams_serialization
-
2
user = User.create!(name: "AMS Test User", email: "ams@example.com")
-
2
render json: user, serializer: UserSerializer
-
end
-
-
# Shrine file upload - triggers Shrine instrumentation logging
-
1
def test_shrine_upload
-
5
document = Document.create_with_shrine_file(
-
filename: "test_document.txt",
-
content: "This is test content for Shrine upload"
-
)
-
-
5
render json: {status: "ok", message: "Shrine upload completed", document_id: document.id}
-
end
-
end
-
# typed: true
-
# frozen_string_literal: true
-
-
1
class SecurityController < ApplicationController
-
# The test app is API-only, with no session for protect_from_forgery to check, so this raises the
-
# error protect_from_forgery raises for a request without a valid token
-
1
def submit
-
4
raise ActionController::InvalidAuthenticityToken, "Can't verify CSRF token authenticity."
-
end
-
-
# Reading remote_ip runs Rails' IP spoofing check
-
1
def remote_ip
-
1
render json: {ip: request.remote_ip}
-
end
-
end
-
# typed: true
-
# frozen_string_literal: true
-
-
1
class ApplicationJob < ActiveJob::Base
-
end
-
# typed: true
-
# frozen_string_literal: true
-
-
1
class TestJob < ApplicationJob
-
1
queue_as :default
-
-
1
def perform(arg)
-
# Log job processing - standard Rails approach
-
logger.info("Processing job #{job_id} with argument: #{arg}")
-
-
# Simulate some work
-
sleep 0.1
-
-
# Test error handling in a job
-
begin
-
raise StandardError, "Test job error"
-
rescue => e
-
# Standard Rails logging
-
logger.error("Job error: #{e.message}")
-
-
# Example of enhanced structured logging
-
exception_log = LogStruct::Log::Error.new(
-
source: LogStruct::Source::Job,
-
error_class: e.class,
-
message: e.message,
-
additional_data: {job_class: self.class.name, job_id: job_id}
-
)
-
logger.error(exception_log)
-
end
-
-
# Log job completion
-
logger.info("Job #{job_id} completed successfully")
-
end
-
end
-
1
class ApplicationMailer < ActionMailer::Base
-
1
default from: "from@example.com"
-
1
layout "mailer"
-
end
-
# typed: true
-
# frozen_string_literal: true
-
-
1
class TestMailer < ApplicationMailer
-
1
def test_email_with_ids(account, user)
-
1
@account = account
-
1
@user = user
-
1
mail(to: "test@example.com", subject: "Test Email")
-
end
-
-
1
def test_email_with_organization(organization)
-
1
@organization = organization
-
1
mail(to: "test@example.com", subject: "Test Email")
-
end
-
end
-
# typed: true
-
# frozen_string_literal: true
-
-
1
class ApplicationRecord < ActiveRecord::Base
-
1
primary_abstract_class
-
1
self.abstract_class = true
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
1
class Document < ApplicationRecord
-
1
extend T::Sig
-
-
# ActiveStorage attachment (used by active_storage_test.rb)
-
1
has_one_attached :active_storage_file
-
-
# Shrine attachment (used by shrine_logging_test.rb)
-
1
include DocumentUploader::Attachment(:file)
-
-
2
sig { params(filename: String, content: String).returns(Document) }
-
1
def self.create_with_file(filename:, content:)
-
8
document = T.let(create!, Document)
-
8
document.active_storage_file.attach(
-
io: StringIO.new(content),
-
filename: filename,
-
content_type: "text/plain"
-
)
-
8
document
-
end
-
-
2
sig { params(filename: String, content: String).returns(Document) }
-
1
def self.create_with_shrine_file(filename:, content:)
-
5
io = StringIO.new(content)
-
10
io.define_singleton_method(:original_filename) { filename }
-
5
document = T.let(new, Document)
-
5
document.file = io
-
5
document.save!
-
5
document
-
end
-
end
-
# typed: true
-
# frozen_string_literal: true
-
-
1
class User < ApplicationRecord
-
1
validates :name, presence: true
-
1
validates :email, presence: true, format: {with: URI::MailTo::EMAIL_REGEXP}
-
-
# Add callbacks to test logging
-
1
after_create :log_creation
-
1
after_update :log_update
-
-
1
private
-
-
1
def log_creation
-
2
Rails.logger.info("User created with ID: #{id} and email: #{attributes["email"]}")
-
end
-
-
1
def log_update
-
# Standard Rails logging with context
-
changed_attrs = previous_changes.keys.join(", ")
-
Rails.logger.info("User #{id} updated. Changed attributes: #{changed_attrs}")
-
end
-
end
-
# typed: true
-
# frozen_string_literal: true
-
-
1
class UserSerializer < ActiveModel::Serializer
-
1
attributes :id, :name, :email
-
end
-
# typed: true
-
# frozen_string_literal: true
-
-
1
class DocumentUploader < Shrine
-
end
-
# typed: true
-
-
1
require_relative "boot"
-
-
1
require "rails/all"
-
-
# Require the gems listed in Gemfile, including any gems
-
# you've limited to :test, :development, or :production.
-
1
Bundler.require(*Rails.groups)
-
-
1
module LogstructTestApp
-
1
class Application < Rails::Application
-
# Initialize configuration defaults for originally generated Rails version.
-
1
config.load_defaults 8.2
-
-
# Configuration for the application, engines, and railties goes here.
-
#
-
# These settings can be overridden in specific environments using the files
-
# in config/environments, which are processed later.
-
#
-
# config.time_zone = "Central Time (US & Canada)"
-
# config.eager_load_paths << Rails.root.join("extras")
-
-
# Only use API mode
-
1
config.api_only = true
-
-
# Rails' default error pages, or the app's own from its routes (config.exceptions_app = routes), which
-
# ErrorPagesTest turns on. An unset config.x setting is an empty, truthy OrderedOptions.
-
1
config.x.error_pages_from_routes = false
-
1
config.exceptions_app = lambda do |env|
-
10
exceptions_app = config.x.error_pages_from_routes ? routes : ActionDispatch::PublicExceptions.new(Rails.public_path)
-
10
exceptions_app.call(env)
-
end
-
-
# Use test adapter for ActiveJob in all environments for testing
-
1
config.active_job.queue_adapter = :test
-
-
# Force all environments to log to STDOUT so development behaves like test/production
-
# This mirrors how many platforms and 12-factor apps expect logs to be emitted.
-
1
config.log_level = :debug
-
1
stdout_logger = ActiveSupport::Logger.new($stdout)
-
1
stdout_logger.formatter = config.log_formatter
-
1
config.logger = ActiveSupport::TaggedLogging.new(stdout_logger)
-
end
-
end
-
# Load the Rails application.
-
1
require_relative "application"
-
-
# Initialize the Rails application.
-
1
Rails.application.initialize!
-
# The test environment is used exclusively to run your application's
-
# test suite. You never need to work with it otherwise. Remember that
-
# your test database is "scratch space" for the test suite and is wiped
-
# and recreated between test runs. Don't rely on the data there!
-
-
1
Rails.application.configure do
-
# Host authorization for tests - allow .localhost subdomains, IPs, and www.example.com
-
1
config.hosts = [
-
".localhost",
-
"www.example.com",
-
IPAddr.new("0.0.0.0/0"), # IPv4
-
IPAddr.new("::/0"), # IPv6
-
]
-
-
# Settings specified here will take precedence over those in config/application.rb.
-
-
# While tests run files are not watched, reloading is not necessary.
-
1
config.enable_reloading = false
-
-
# Eager loading loads your entire application. When running a single test locally,
-
# this is usually not necessary, and can slow down your test suite. However, it's
-
# recommended that you enable it in continuous integration systems to ensure eager
-
# loading is working properly before deploying your code.
-
1
config.eager_load = ENV["CI"].present?
-
-
# Configure public file server for tests with cache-control for performance.
-
1
config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }
-
-
# Show full error reports.
-
1
config.consider_all_requests_local = true
-
1
config.cache_store = :null_store
-
-
# Render exception templates for rescuable exceptions and raise for other exceptions.
-
1
config.action_dispatch.show_exceptions = :rescuable
-
-
# Disable request forgery protection in test environment.
-
1
config.action_controller.allow_forgery_protection = false
-
-
# Store uploaded files on the local file system in a temporary directory.
-
1
config.active_storage.service = :test
-
-
# Tell Action Mailer not to deliver emails to the real world.
-
# The :test delivery method accumulates sent emails in the
-
# ActionMailer::Base.deliveries array.
-
1
config.action_mailer.delivery_method = :test
-
-
# Set host to be used by links generated in mailer templates.
-
1
config.action_mailer.default_url_options = { host: "example.com" }
-
-
# Print deprecation notices to the stderr.
-
1
config.active_support.deprecation = :stderr
-
-
# Raises error for missing translations.
-
# config.i18n.raise_on_missing_translations = true
-
-
# Annotate rendered view with file names.
-
# config.action_view.annotate_rendered_view_with_filenames = true
-
-
# Raise error when a before_action's only/except options reference missing actions.
-
1
config.action_controller.raise_on_missing_callback_actions = true
-
end
-
# Be sure to restart your server when you modify this file.
-
-
# Avoid CORS issues when API is called from the frontend app.
-
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin Ajax requests.
-
-
# Read more: https://github.com/cyu/rack-cors
-
-
# Rails.application.config.middleware.insert_before 0, Rack::Cors do
-
# allow do
-
# origins "example.com"
-
#
-
# resource "*",
-
# headers: :any,
-
# methods: [:get, :post, :put, :patch, :delete, :options, :head]
-
# end
-
# end
-
# Be sure to restart your server when you modify this file.
-
-
# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
-
# Use this to limit dissemination of sensitive information.
-
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
-
1
Rails.application.config.filter_parameters += [
-
:passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc
-
]
-
# Be sure to restart your server when you modify this file.
-
-
# Add new inflection rules using the following format. Inflections
-
# are locale specific, and you may define rules for as many different
-
# locales as you wish. All of these examples are active by default:
-
# ActiveSupport::Inflector.inflections(:en) do |inflect|
-
# inflect.plural /^(ox)$/i, "\\1en"
-
# inflect.singular /^(ox)en/i, "\\1"
-
# inflect.irregular "person", "people"
-
# inflect.uncountable %w( fish sheep )
-
# end
-
-
# These inflection rules are supported but not enabled by default:
-
# ActiveSupport::Inflector.inflections(:en) do |inflect|
-
# inflect.acronym "RESTful"
-
# end
-
# typed: strict
-
-
1
require "log_struct"
-
-
# Configure LogStruct
-
1
LogStruct.configure do |config|
-
# Specify which environments to enable in
-
1
config.enabled_environments = [:development, :test, :production]
-
-
# Lets BootLogsIntegrationTest check that `enabled` and `enabled_environments` set here are honored
-
1
if ENV.key?("LOGSTRUCT_TEST_INITIALIZER_ENABLED")
-
config.enabled = ENV["LOGSTRUCT_TEST_INITIALIZER_ENABLED"] == "true"
-
end
-
1
if ENV.key?("LOGSTRUCT_TEST_INITIALIZER_ENVIRONMENTS")
-
config.enabled_environments = ENV.fetch("LOGSTRUCT_TEST_INITIALIZER_ENVIRONMENTS").split(",").map(&:to_sym)
-
end
-
-
# Specify which environments are considered local/development
-
1
config.local_environments = [:development, :test]
-
-
# Configure integrations
-
1
config.integrations.enable_lograge = true
-
1
config.integrations.enable_actionmailer = true
-
1
config.integrations.enable_activejob = true
-
1
config.integrations.enable_rack_error_handler = true
-
1
config.integrations.enable_sidekiq = !!defined?(Sidekiq)
-
1
config.integrations.enable_shrine = !!defined?(Shrine)
-
1
config.integrations.enable_carrierwave = !!defined?(CarrierWave)
-
1
config.integrations.enable_activestorage = true
-
-
# Configure string scrubbing filters
-
1
config.filters.email_addresses = true
-
1
config.filters.url_passwords = true
-
1
config.filters.credit_card_numbers = true
-
1
config.filters.phone_numbers = true
-
1
config.filters.ssns = true
-
1
config.filters.ip_addresses = true
-
1
config.filters.mac_addresses = true
-
-
# Configure error handling modes
-
1
config.error_handling_modes.logstruct_errors = LogStruct::ErrorHandlingMode::Log
-
1
config.error_handling_modes.security_errors = LogStruct::ErrorHandlingMode::Report
-
1
config.error_handling_modes.standard_errors = LogStruct::ErrorHandlingMode::LogProduction
-
end
-
# typed: true
-
# frozen_string_literal: true
-
-
1
require "shrine"
-
1
require "shrine/storage/memory"
-
-
# Use memory storage for tests (fast, no filesystem needed)
-
1
Shrine.storages = {
-
cache: Shrine::Storage::Memory.new,
-
store: Shrine::Storage::Memory.new
-
}
-
-
# Standard Shrine plugins matching DocSpring's production setup
-
1
Shrine.plugin :cached_attachment_data
-
1
Shrine.plugin :activerecord
-
-
# IMPORTANT: This is the key plugin that causes issues.
-
# A normal Rails app will have this configured BEFORE LogStruct loads.
-
# LogStruct must handle this gracefully and NOT add a duplicate log_subscriber.
-
1
Shrine.plugin :instrumentation
-
# typed: strict
-
# frozen_string_literal: true
-
-
1
Rails.application.routes.draw do
-
# Testing routes
-
1
get "/logging/basic", to: "logging#test_basic"
-
1
get "/logging/error", to: "logging#test_error"
-
1
get "/logging/model", to: "logging#test_model"
-
1
get "/logging/job", to: "logging#test_job"
-
1
get "/logging/context", to: "logging#test_context"
-
1
get "/logging/custom", to: "logging#test_custom"
-
1
get "/logging/request", to: "logging#test_request"
-
1
get "/logging/error_logging", to: "logging#test_error_logging"
-
1
get "/logging/shrine_upload", to: "logging#test_shrine_upload"
-
1
get "/logging/ams", to: "logging#test_ams_serialization"
-
1
post "/logging/params", to: "logging#test_params"
-
-
1
post "/security/submit", to: "security#submit"
-
1
get "/security/remote_ip", to: "security#remote_ip"
-
-
# Error pages, for config.exceptions_app = routes
-
5
%w[400 404 422 500].each { |status| match "/#{status}", to: "errors#show", via: :all }
-
-
# Healthcheck route
-
4
get "/health", to: proc { [200, {}, ["OK"]] }
-
end
-
# typed: strict
-
# frozen_string_literal: true
-
-
1
namespace :logging do
-
1
desc "Test log output for rake task logging tests"
-
1
task test_output: :environment do
-
Rails.logger.info "Test log message from rake task"
-
Rails.logger.tagged("custom_tag") do
-
Rails.logger.info "Tagged test log message"
-
end
-
end
-
end