What is Sorbet?

Sorbet Logo

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:

Gemfile
gem "sorbet", group: :development
gem "tapioca", require: false, group: [:development, :test]

Then run:

bundle install
bundle exec tapioca init
bundle exec srb tc

tapioca 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 failed T.let, T.cast or T.must
  • call_validation_error_handler: an argument or a return value that doesn't match a method's sig, or a struct field of the wrong type
  • sig_builder_error_handler: a sig block that can't be built
  • sig_validation_error_handler: a sig that fails validation, such as an override that doesn't match its parent's sig

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}"
end

a 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
end

To 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
end

Custom 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 for level (a LogStruct::Level), timestamp (a Time) and serialize.
  • LogStruct::Log::SerializeCommonPublic, whose serialize_common_public returns src, evt, lvl and ts from your source, event, level and timestamp. The struct must define source and event; each can be a String, a Symbol or a T::Enum.
  • LogStruct::Log::Interfaces::AdditionalDataField and LogStruct::Log::Shared::MergeAdditionalDataFields, for an optional additional_data Hash whose keys go at the top level of the line. A key named src, evt, lvl or ts is 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
end

Log 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"
}
The Terraform provider only knows LogStruct's built-in sources and events, so it rejects a custom log's 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 failed
  • Delivery - 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 traces
  • Finish - 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 cache
  • Exist - Existence check for a stored file (ActiveStorage or Shrine)
  • Metadata - ActiveStorage updated a stored file's metadata, or Shrine extracted a file's metadata
  • Upload - 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 cache
  • Upload - A file was stored (ActiveStorage, CarrierWave, or Shrine)

Log::Dotenv

  • Load - dotenv-rails load event when environment variables are read from disk
  • Restore - dotenv-rails restore event rolling back to the last saved state
  • Save - dotenv-rails save event persisting environment variables
  • Update - dotenv-rails update event after manipulating environment values

Log::Error

  • For exception details with stack traces

Log::GoodJob

  • Error - For exception details with stack traces
  • Log - 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 traces
  • Shutdown - Puma server shutdown event
  • Start - 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 violation
  • IPSpoof - 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 cache
  • Exist - Existence check for a stored file (ActiveStorage or Shrine)
  • Metadata - ActiveStorage updated a stored file's metadata, or Shrine extracted a file's metadata
  • Upload - A file was stored (ActiveStorage, CarrierWave, or Shrine)

Log::Sidekiq

  • Error - For exception details with stack traces
  • Log - 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 information
  • Info - General informational messages
  • Warn - Warning conditions that should be noted
  • Error - Error conditions that affect operation
  • Fatal - Severe error conditions that cause the application to terminate
  • Unknown - 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 checks
  • Internal - Errors from LogStruct itself
  • Rails - Core Rails framework components
  • Job - Background job processing
  • Storage - ActiveStorage logs and errors
  • Mailer - Email delivery and processing
  • App - Application-specific code
  • Shrine - Shrine file operations (uploads, downloads, deletes, metadata, existence checks)
  • CarrierWave - CarrierWave file operations (stores, deletes, remote downloads)
  • Sidekiq - Sidekiq background job logs and errors
  • Dotenv - 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 message
  • Request - HTTP request
  • Enqueue - Job added to queue
  • Schedule - Job scheduled for future processing
  • Start - Job processing started, or server started
  • Finish - Job processing completed
  • Retry - Job enqueued again by retry_on after an exception
  • RetryStopped - retry_on stopped retrying a job after its last attempt
  • Discard - Job dropped by discard_on after an exception
  • Upload - File upload operation
  • Download - File download operation
  • Delete - File deletion operation
  • Metadata - File metadata operation
  • Exist - File existence check operation
  • Url - File URL generation operation
  • Generate - 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 down
  • IPSpoof - Rails raised IpSpoofAttackError when the app read request.remote_ip
  • CSRFViolation - Cross-Site Request Forgery violation
  • BlockedHost - Request blocked by ActionDispatch::HostAuthorization
  • Database - Database query event and metrics
  • Error - Error occurrence
  • Unknown - Unclassified event type

ErrorHandlingMode

Error handling strategies for different types of errors

  • Ignore - Completely ignore errors
  • Log - Log errors but don't report them
  • Report - Log and report errors to error service
  • LogProduction - Raise in local_environments (development and test by default), log everywhere else
  • ReportProduction - Raise in local_environments (development and test by default), log and report everywhere else
  • Raise - Raise the error, without logging or reporting it