What is Sorbet?
Sorbet is a type checker for Ruby. Its static checker (srb tc) checks your code before it runs, and sorbet-runtime checks method signatures (sigs) and typed structs while it runs. It:
- Finds type errors before the code runs
- Checks the values that reach typed code at runtime, such as params and parsed JSON, which the static checker can't see
- Gives editors that use Sorbet's language server types for completion
LogStruct is written with Sorbet: its files are # typed: strict and its methods have sigs. Its log lines are typed structs (T::Struct), with a type for every field.
Adding Sorbet to Your Application
You don't need Sorbet to use LogStruct. LogStruct depends on sorbet-runtime, so your app has it already. To type-check your own code, add the static checker and Tapioca, which writes RBI files that describe your gems to Sorbet:
gem "sorbet", group: :development
gem "tapioca", require: false, group: [:development, :test]Then run:
bundle install
bundle exec tapioca init
bundle exec srb tctapioca init creates the sorbet/ directory and an RBI file for each gem. LogStruct's includes its method signatures, so srb tc checks your calls to LogStruct. See Adopting Sorbet for the next steps.
Using LogStruct with Sorbet
Each built-in log line is a typed struct under LogStruct::Log. Build one and pass it to LogStruct.info (or debug, warn, error and fatal), which logs it with Rails.logger.
# Create a typed request log entry
request_log = LogStruct::Log::Request.new(
http_method: "GET",
path: "/users",
status: 200,
duration_ms: 45.2,
source: LogStruct::Source::Rails
)
# Log the typed struct at info level
LogStruct.info(request_log)
# Create a typed error log entry
error_log = LogStruct::Log::Error.new(
source: LogStruct::Source::App,
error_class: StandardError,
message: "An error occurred during processing"
)
# Log the error at error level
LogStruct.error(error_log)With Sorbet in your app, srb tc checks the fields you pass. At runtime, sorbet-runtime checks them when the struct is built: a missing field or a field of the wrong type is a type error, which is raised in development and test. In production it's logged and reported, and the struct is built anyway (see below).
Error Handling for Type Errors
When a runtime check fails, Sorbet calls one of four handlers, which raise the error by default. In the processes where LogStruct is on (see Enabling LogStruct), LogStruct sets all four:
inline_type_error_handler: a failedT.let,T.castorT.mustcall_validation_error_handler: an argument or a return value that doesn't match a method'ssig, or a struct field of the wrong typesig_builder_error_handler: asigblock that can't be builtsig_validation_error_handler: asigthat fails validation, such as an override that doesn't match its parent'ssig
Each one passes the error to the type_checking_errors mode. The default, ReportProduction, raises the error in the local_environments (development and test), as Sorbet does. Elsewhere it logs the error and reports it to your error reporter, and the code carries on with the wrong value. These handlers cover type errors in your app and in its gems.
For example, with this method:
class Greeter
extend T::Sig
sig { params(name: String).returns(String) }
def self.greet(name) = "Hello, #{name}"
enda value from outside the typed code that isn't a String raises a TypeError in development and test:
payload = JSON.parse('{"name": 42}')
Greeter.greet(payload["name"])In production the call returns "Hello, 42" and writes this line. Its msg goes on with the file and line of the call and of the method, and its backtrace (left out here) starts at the call:
{
"src": "type_checking",
"evt": "error",
"lvl": "error",
"ts": "2026-10-17T09:30:15.123+00:00",
"error_class": "TypeError",
"msg": "Parameter 'name': Expected type String, got type Integer with value 42",
"prog": "Rails"
}To handle type errors another way, change the mode:
LogStruct.configure do |config|
# Log Sorbet type errors in every environment, without raising or reporting them
config.error_handling_modes.type_checking_errors = LogStruct::ErrorHandlingMode::Log
endTo keep Sorbet's own behavior, or handlers your app sets, turn LogStruct's off. LogStruct sets its handlers after config/initializers run, so it replaces handlers set there unless this is false:
LogStruct.configure do |config|
config.integrations.enable_sorbet_error_handlers = false
endCustom Typed Logs
You can define typed log lines of your own. A custom log is a T::Struct that includes:
LogStruct::Log::Interfaces::PublicCommonFields, which asks forlevel(aLogStruct::Level),timestamp(aTime) andserialize.LogStruct::Log::SerializeCommonPublic, whoseserialize_common_publicreturnssrc,evt,lvlandtsfrom yoursource,event,levelandtimestamp. The struct must definesourceandevent; each can be a String, a Symbol or aT::Enum.LogStruct::Log::Interfaces::AdditionalDataFieldandLogStruct::Log::Shared::MergeAdditionalDataFields, for an optionaladditional_dataHash whose keys go at the top level of the line. A key namedsrc,evt,lvlortsis a LogStruct error (logstruct_errors): raised in development and test, and logged and left out in production.
Your serialize starts from serialize_common_public, merges the additional data, and adds your own fields:
# Define a custom typed log using LogStruct interfaces
module TestApp
module Log
class Payments < T::Struct
extend T::Sig
include ::LogStruct::Log::Interfaces::PublicCommonFields
include ::LogStruct::Log::Interfaces::AdditionalDataField
include ::LogStruct::Log::SerializeCommonPublic
include ::LogStruct::Log::Shared::MergeAdditionalDataFields
# Event restricted to a specific set for type safety (T::Enum)
class Event < T::Enum
enums do
Processed = new(:processed)
Failed = new(:failed)
Refunded = new(:refunded)
end
end
# Fixed source: not overridable by callers
sig { returns(String) }
def source = "payments"
const :event, Event
const :level, ::LogStruct::Level, default: T.let(::LogStruct::Level::Info, ::LogStruct::Level)
const :timestamp, Time, factory: -> { Time.now }
# Domain fields
const :payment_id, String
const :amount_cents, Integer
const :currency, String
const :status, String
const :user_id, T.nilable(Integer), default: nil
# Optional extra data merged at top-level
const :additional_data, T::Hash[Symbol, T.untyped], default: {}
sig { override.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
def serialize(strict = true)
h = serialize_common_public(strict)
merge_additional_data_fields(h)
h[:payment_id] = payment_id
h[:amount_cents] = amount_cents
h[:currency] = currency
h[:status] = status
h[:user_id] = user_id if user_id
h
end
end
end
endLog it like a built-in line. Its fields are filtered and scrubbed like the rest of the line:
payment_log = TestApp::Log::Payments.new(
event: TestApp::Log::Payments::Event::Processed,
payment_id: "pay_123456",
amount_cents: 9999,
currency: "USD",
status: "succeeded",
user_id: 123,
additional_data: {note: "Receipt sent to jo@example.com"}
)
LogStruct.info(payment_log){
"src": "payments",
"evt": "processed",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"note": "Receipt sent to [EMAIL:bf02ccc06e51f611]",
"payment_id": "pay_123456",
"amount_cents": 9999,
"currency": "USD",
"status": "succeeded",
"user_id": 123,
"prog": "Rails"
}src. Write CloudWatch patterns for custom logs yourself (see Match on other fields). The generated TypeScript types and the lists below cover the built-in log types only.Built-In Log Classes
LogStruct's log lines are typed structs under LogStruct::Log.
Log::ActionMailer
Delivered- Logged after a mail is delivered (after_deliver), unless the delivery failedDelivery- Logged before a mail is sent (before_deliver)Error- For exception details with stack traces
Log::ActiveJob
Discard- discard_on caught a job's exception and dropped the job (ActiveJob)Enqueue- ActiveJob enqueued a job to run now (perform_later or perform_all_later)Error- For exception details with stack tracesFinish- A job finished without raising an exception (ActiveJob)Retry- retry_on caught a job's exception and enqueued the job again (ActiveJob)RetryStopped- retry_on caught a job's exception after the job's last attempt (ActiveJob)Schedule- ActiveJob enqueued a job to run later (perform_later with wait or wait_until)Start- Lifecycle start event for jobs or services (ActiveJob or Puma boot)
Log::ActiveModelSerializers
- For render events produced by ActiveModelSerializers (serializer, adapter, resource, duration)
Log::ActiveStorage
Delete- A stored file was deleted (ActiveStorage, CarrierWave, or Shrine)Download- A stored file was read (ActiveStorage or Shrine), or CarrierWave fetched a remote URL into its cacheExist- Existence check for a stored file (ActiveStorage or Shrine)Metadata- ActiveStorage updated a stored file's metadata, or Shrine extracted a file's metadataUpload- A file was stored (ActiveStorage, CarrierWave, or Shrine)Url- ActiveStorage generated a URL for a stored file (the URL itself is not logged)
Log::Ahoy
- One line for each Ahoy::Tracker#track call, with the event name and its properties
Log::CarrierWave
Delete- A stored file was deleted (ActiveStorage, CarrierWave, or Shrine)Download- A stored file was read (ActiveStorage or Shrine), or CarrierWave fetched a remote URL into its cacheUpload- A file was stored (ActiveStorage, CarrierWave, or Shrine)
Log::Dotenv
Load- dotenv-rails load event when environment variables are read from diskRestore- dotenv-rails restore event rolling back to the last saved stateSave- dotenv-rails save event persisting environment variablesUpdate- dotenv-rails update event after manipulating environment values
Log::Error
- For exception details with stack traces
Log::GoodJob
Error- For exception details with stack tracesLog- A log line: a plain message, or a line from GoodJob's or Sidekiq's own logger
Log::Plain
- For general purpose logging
Log::Puma
Error- For exception details with stack tracesShutdown- Puma server shutdown eventStart- Lifecycle start event for jobs or services (ActiveJob or Puma boot)
Log::Request
- For HTTP request details
Log::Security
BlockedHost- Request blocked by ActionDispatch::HostAuthorization (host not on the allowed list)CSRFViolation- Request rejected due to a CSRF token violationIPSpoof- Rails raised IpSpoofAttackError where the app read request.remote_ip (the Client-IP header isn't in X-Forwarded-For)
Log::Shrine
Delete- A stored file was deleted (ActiveStorage, CarrierWave, or Shrine)Download- A stored file was read (ActiveStorage or Shrine), or CarrierWave fetched a remote URL into its cacheExist- Existence check for a stored file (ActiveStorage or Shrine)Metadata- ActiveStorage updated a stored file's metadata, or Shrine extracted a file's metadataUpload- A file was stored (ActiveStorage, CarrierWave, or Shrine)
Log::Sidekiq
Error- For exception details with stack tracesLog- A log line: a plain message, or a line from GoodJob's or Sidekiq's own logger
Log::SQL
- For ActiveRecord SQL queries slower than sql_slow_query_threshold (100 ms by default)
Built-In Enums
Levels, sources, events and error handling modes are typed enums (T::Enum) under LogStruct::.
Level
Log severity levels for different types of log messages
Debug- Detailed debugging informationInfo- General informational messagesWarn- Warning conditions that should be notedError- Error conditions that affect operationFatal- Severe error conditions that cause the application to terminateUnknown- Used when a log level cannot be determined
Source
Sources of log messages to identify which part of the system generated them
TypeChecking- Type checking errors (Sorbet)Security- Security-related events and checksInternal- Errors from LogStruct itselfRails- Core Rails framework componentsJob- Background job processingStorage- ActiveStorage logs and errorsMailer- Email delivery and processingApp- Application-specific codeShrine- Shrine file operations (uploads, downloads, deletes, metadata, existence checks)CarrierWave- CarrierWave file operations (stores, deletes, remote downloads)Sidekiq- Sidekiq background job logs and errorsDotenv- Dotenv-rails configuration events (env file load/update/save/restore)Puma- Puma server lifecycle events and request errors
Event
Event types for different kinds of operations and activities
Log- Standard log messageRequest- HTTP requestEnqueue- Job added to queueSchedule- Job scheduled for future processingStart- Job processing started, or server startedFinish- Job processing completedRetry- Job enqueued again by retry_on after an exceptionRetryStopped- retry_on stopped retrying a job after its last attemptDiscard- Job dropped by discard_on after an exceptionUpload- File upload operationDownload- File download operationDelete- File deletion operationMetadata- File metadata operationExist- File existence check operationUrl- File URL generation operationGenerate- Serialization/render event (ActiveModelSerializers)Delivery- Email about to be sent (before_deliver)Delivered- Email delivered (after_deliver)Load- Configuration load operation (e.g., dotenv file loaded)Update- Configuration update operation (e.g., env var set)Save- Configuration state saved (e.g., snapshot)Restore- Configuration state restored (e.g., snapshot restored)Shutdown- Server shutting downIPSpoof- Rails raised IpSpoofAttackError when the app read request.remote_ipCSRFViolation- Cross-Site Request Forgery violationBlockedHost- Request blocked by ActionDispatch::HostAuthorizationDatabase- Database query event and metricsError- Error occurrenceUnknown- Unclassified event type
ErrorHandlingMode
Error handling strategies for different types of errors
Ignore- Completely ignore errorsLog- Log errors but don't report themReport- Log and report errors to error serviceLogProduction- Raise in local_environments (development and test by default), log everywhere elseReportProduction- Raise in local_environments (development and test by default), log and report everywhere elseRaise- Raise the error, without logging or reporting it