Rack logging lab

Plain Ruby on Rack, no framework. How do its logs land on Laravel Cloud?

Each card writes to the logs in a different way. Every action reports back the request_id so you can find the exact lines in the log viewer.

Ruby
4.0.7
Rack / Puma
3.2.7 ยท 8.0.2
LOG_LEVEL
info
LOG_FORMAT
text
ACCESS_LOG
stderr
Request id from
X-Request-Id header

Ruby's Logger

The standard library ::Logger, which is what most plain Ruby apps use.

01

Logger

stdlib ::Logger

Writes at a given severity to STDOUT. Levels below LOG_LEVEL are dropped. The block form is only evaluated if the level is enabled. The app's formatter adds the request id.

LOGGER = Logger.new($stdout, level: :info)
LOGGER.info "message"
LOGGER.debug { expensive_string }
02

progname

Per-component loggers

The closest thing stdlib Logger has to tags: a progname per logger instance, printed on every line.

billing = Logger.new($stdout, progname: "billing")
billing.info "message"

Structured

Named loggers with data attached instead of free-form text.

03

SemanticLogger

semantic_logger gem

popular

The most used structured logging gem outside Rails. Supports named loggers, payload hashes, tags and timing blocks. It writes from a background thread by default. Set LOG_FORMAT=json for JSON output.

logger = SemanticLogger["Checkout"]
SemanticLogger.tagged("billing") do
  logger.info("Charged card", user_id: 42)
  logger.measure_info("Called API") { ... }
end

Rack

What the Rack spec itself provides.

04

Access log

Rack::CommonLogger

One Apache-style line per request. Rack writes it to rack.errors by default, which Puma points at STDERR. Change it with ACCESS_LOG=stdout|off. Pick a status to see how the platform treats it.

use Rack::CommonLogger
# 1.2.3.4 - - [12/Oct/2026:02:23:12 +0000] "GET /status?code=503 HTTP/1.1" 503 - 0.0004
05

rack.errors

The Rack error stream

Every Rack server gives the app an error stream in env["rack.errors"]. Puma connects it to STDERR, and frameworks often write warnings to it.

env["rack.errors"].puts "message"

Errors

Logged exceptions, and real crashes with multi-line backtraces.

06

Logged exception

Rescued and logged

Passing an exception object to the logger. Stdlib prints the message, class and backtrace as one multi-line entry. SemanticLogger records it as a structured exception.

rescue => e
  LOGGER.error(e)
  SemanticLogger["Demo"].error("Caught an error", e)
07

Unhandled exception

Handled by Puma

With no framework, Puma catches the error and writes a one-line summary to STDERR. In production there's no backtrace. It returns a bare 500, so use the browser back button to return.

raise RuntimeError, "boom"

Threads & stdio

Lines written outside the request, or outside the logger altogether.

08

Background thread

Outside the request

Logs from a thread after the response has gone, so there's no request id. This is how plain Ruby apps usually do background work.

Thread.new { LOGGER.info "message" }
09

Raw STDOUT / STDERR

Bypasses the logger

Writes straight to the process streams. Checks whether the platform captures both, and whether it treats stderr as errors.

$stdout.puts "message"
$stderr.puts "message"
warn "message"

Edge cases

Inputs that tend to break log pipelines.

BurstMany lines in one request
Huge lineOne line of N bytes
Multi-lineEmbedded newlines
Unicode + ANSIEmoji, CJK, colour codes
Every leveldebug through unknown