API Design: Typed vs. Untyped

LogStruct is designed for an idiomatic Rails experience first, with an optional typed path for teams that use sorbet-runtime. Most Rails developers can adopt LogStruct without learning Sorbet. Teams wanting stronger guarantees can progressively introduce typed log structs.

Untyped, Idiomatic Rails

You can continue using Rails.logger with hashes and strings. LogStruct's formatter scrubs sensitive values and keeps output JSON-friendly.

# Untyped, idiomatic Rails logging (works out of the box)
Rails.logger.info({
  msg: "User signed in",
  user_id: current_user.id,
  feature: "onboarding"
})

Optional Typed Path

For teams that want stricter contracts, use LogStruct's typed structs. These are runtime-checked via sorbet-runtime and integrate with our formatter seamlessly.

# Optional typed API (sorbet-runtime)
log = LogStruct::Log::Request.new(
  message: "GET /projects",
  event: LogStruct::Event::Request,
  source: LogStruct::Source::Rails,
  controller: "ProjectsController",
  action: "index"
)

LogStruct.info(log)

Custom Typed Structures (Sketch)

You can define app-specific typed logs by composing LogStruct interfaces. Keep this ergonomic and discoverable; the untyped path remains first-class.

# Sketch: defining a custom typed log struct
class MyApp::Logs::Checkout < T::Struct
  include LogStruct::Log::Interfaces::CommonFields
  include LogStruct::Log::Interfaces::AdditionalDataField

  const :event, LogStruct::Event::Log
  const :source, LogStruct::Source, default: T.let(LogStruct::Source::App, LogStruct::Source)
  const :message, String
  const :cart_id, String
  const :amount_cents, Integer
end

# Then log it with
LogStruct.info(
  MyApp::Logs::Checkout.new(message: "checkout_completed", cart_id: cart.id, amount_cents: 1299)
)