Semantic Logger for Ruby: structured events, appenders, and timing

Use Semantic Logger payloads, exceptions, appenders, duration measurements, and signal controls while accounting for queue pressure, shutdown, and Rails integration.

Best practices
Illustration of logs flowing through a collection pipeline

Semantic Logger gives Ruby applications structured payloads, exception fields, duration measurements, and multiple output destinations. By default, a background processor writes queued events to appenders. The companion rails_semantic_logger gem integrates that model with Rails.

The operational choices still matter: define the schema, decide what happens when a destination is slow, and verify shutdown and collection. Asynchronous logging moves work off the calling thread; it does not remove CPU, memory, or I/O costs.

Install and create a logger

Add the gem to your bundle:

# Gemfile
gem "semantic_logger"

Configure it once during application startup:

require "semantic_logger"

SemanticLogger.default_level = :info
SemanticLogger.add_appender(io: $stdout)
logger = SemanticLogger["OrderService"]

logger.info("Service started")
logger.error("An operation failed")
SemanticLogger.flush

The text formatter includes context such as time, severity, process/thread identity, and logger name. Formatting varies with the selected formatter; use formatter: :color explicitly when you want colorized development output, or formatter: :json for machine-readable records.

Inside a class, include SemanticLogger::Loggable to obtain a class-named logger:

class OrderService
  include SemanticLogger::Loggable

  def process(order)
    logger.info("Processing order", { order_id: order.id })
  end
end

The order argument represents your application's object. Log an identifier or an allowlisted payload instead of serializing the entire object.

Understand the asynchronous pipeline

The normal asynchronous path enqueues an event and uses a shared background processor to write to its appenders. Appenders do not each receive an independent queue by default. A slow destination can delay the shared processor and, eventually, the application.

Current Semantic Logger 5.x documentation describes a bounded main queue with a default capacity of 10,000 and blocking when it fills. Version-specific options allow dropping instead, or giving an appender its own asynchronous queue. Inspect the installed version and measure its behavior; do not infer “zero blocking” from the word asynchronous. An unbounded queue trades backpressure for potentially unbounded memory use.

SemanticLogger.flush drains queued output and asks appenders to flush. The library also registers normal process-exit flushing. An abrupt crash, forced kill, or terminated container can still lose queued events, and a flush can take time when a destination is slow. Allow enough graceful-shutdown time and monitor queue pressure.

For synchronous operation, use the dedicated require at startup:

# Gemfile
gem "semantic_logger", require: "semantic_logger/sync"

Alternatively, call SemanticLogger.sync! during startup before adding any appenders. Appender writes then run on the calling path. That can suit small command-line tools or workflows that need immediate output, but it is not a guarantee of durable audit storage: filesystem caches and remote acknowledgments remain separate concerns.

Preserve payloads and exceptions as fields

For ordinary logging calls, the positional form accepts a message, a payload or exception, and optionally an exception. Structured payloads stay separate from message text:

logger.info("User signed in", { user_id: 42, ip: "192.0.2.8" })

begin
  raise "Payment gateway timed out"
rescue StandardError => error
  logger.error("Charge failed", { order_id: 1024 }, error)
  # Decide whether the caller must also receive the failure.
end

This demonstration catches the exception so it can show the event. In business code, explicitly choose whether to re-raise or return a failure; writing an error log does not decide that for you. User IDs and IP addresses still need a purpose, access controls, and retention limits.

Enable JSON on an appender to retain the nested payload and exception structure:

SemanticLogger.add_appender(io: $stdout, formatter: :json)

Use this instead of the earlier text appender if you want only one output copy. Adding both registers two appenders and emits two records to the same stream.

An illustrative event includes these fields, along with additional runtime context:

{"host":"web-01","timestamp":"2026-08-25T14:46:03.873Z","level":"error","name":"OrderService","message":"Charge failed","payload":{"order_id":1024},"exception":{"name":"RuntimeError","message":"Payment gateway timed out","stack_trace":["app/services/order_service.rb:12:in `charge'"]}}

Check an actual payload event and exception event from your installed version. Exception text and backtraces can contain sensitive values even when the payload itself is safe.

Configure multiple appenders deliberately

Each appender can have its own format, level, and filter:

require "fileutils"

FileUtils.mkdir_p("logs")
SemanticLogger.default_level = :trace
SemanticLogger.add_appender(io: $stdout, formatter: :color)
SemanticLogger.add_appender(
  file_name: "logs/error.log",
  formatter: :json,
  level: :error
)

This example permits all standard levels at the logger's default threshold, writes them to the console, and restricts the file to errors and above. An appender cannot recover an event already filtered out by the logger. An explicitly set per-logger threshold can also override the global default.

The appender ecosystem includes files and network destinations such as HTTP, TCP/UDP, and databases, with dependencies and delivery behavior specific to the appender. For many deployments, JSON to stdout or a local file plus a collector is easier to operate than coupling every application process to a remote logging service. If you use a network appender, test retries, blocked writes, dropped events, and shutdown.

Measure operations without building timing strings

The measure_* methods add duration information to the resulting event. This standalone example logs only when the measured block exceeds the configured threshold:

logger.measure_info(
  "Called payment gateway",
  metric: "PaymentGateway/request_time",
  min_duration: 1000
) do
  sleep 1.05 # Replace with the application's gateway call.
end

min_duration is in milliseconds, so 1000 selects operations taking longer than one second. The JSON event's duration_ms is numeric. The metric name supports metric subscribers and downstream grouping; adding it does not automatically create a Guance metric or dashboard.

A thresholded event stream describes slow operations, not the full latency distribution. Keep a histogram or another complete measurement when you need service-level latency objectives. The measure API also handles exceptions according to its options; its default measured-block behavior logs the failure and re-raises, so callers still need appropriate error handling.

Change verbosity with signals

On platforms that support the signals, opt in during startup:

SemanticLogger.add_signal_handler

Then send SIGUSR2 to the intended process:

kill -SIGUSR2 1234

Replace 1234 with the verified application PID. Each signal advances the global default through fatal → error → warn → info → debug → trace, then wraps around. It does not necessarily reset to debug, and it does not change loggers whose level was set explicitly. Appender thresholds still apply.

Record the previous level, change only the needed processes, and restore it after the investigation. Check for signal conflicts with the process manager or application; this mechanism is not portable to every operating system. The same registration also supports thread-dump signals, which can generate sensitive diagnostic output.

Integrate with Rails

Use the companion gem for framework integration:

# Gemfile
gem "rails_semantic_logger"

In rails_semantic_logger 5.x, declare the production destinations through the appenders block:

# config/environments/production.rb
config.rails_semantic_logger.appenders do |appenders|
  appenders.add(io: $stdout, formatter: :json)
end

Declaring an appender replaces the default file-appender configuration. Version 5 requires Ruby 3.2 or later and Rails 7.2 or later; older v4 add_file_appender/config.semantic_logger.add_appender settings are deprecated in v5. Keep the application's other required settings. The integration replaces the Rails logger and adds framework logging behavior; it is more than assigning a bare logger instance. Review request tags and formatter output after enabling it. Avoid stacking overlapping request-log replacements without checking for duplicates or missing events.

For distributed tracing, inject the actual active trace context and verify a log resolves to its collected trace. Request tags alone do not provide that integration.

Collect and validate in Guance

Use the DataKit log collector for your chosen output. Validate timestamp, level, name, and nested payload fields, then map the event time and severity to the collection schema. Preserve duration_ms as a number and check one deliberately slow operation.

File output needs rotation and reopen behavior that the logger and collector both support. Container output needs runtime collection and retention. Do not collect stdout and the same duplicated file record unintentionally.

Frequently asked questions

Can Semantic Logger coexist with Ruby's standard Logger?

It supports familiar Logger-style methods, which helps incremental migration. Existing standard Logger instances do not automatically feed Semantic Logger's appenders, however. Replace or adapt the logger at each integration point, and verify formatter, level, and exception behavior.

Does asynchronous logging preserve order?

The shared processor handles queued events in queue order. That is not a global causal ordering across application threads, processes, hosts, or remote destinations. Independently asynchronous appenders add their own scheduling. Synchronous output removes one queue but still does not establish distributed order; use timestamps and operation identifiers to reconstruct work.

How can we reduce measurement-log volume?

Use min_duration to retain slow operations or apply an intentional sampling policy. This changes the population available for analysis. Storage/indexing choices downstream may also affect cost, but verify the product's current units and behavior instead of assuming a lower-cost tier preserves every query capability.

Which appender should production use?

Start with one JSON stdout appender for a container, or one JSON file appender for a managed host, if that matches the collection architecture. Add destinations only for a concrete need. A collector can centralize forwarding, but its own buffering and delivery failures still need monitoring.

References and related reading

Consult the current Semantic Logger guide, configuration, operations and queue behavior, and Rails integration. Queue and integration options should be checked against the installed gem version.

Continue with Rails logging and logging best practices. The related guides to Ruby logging libraries and .NET logging are currently in Chinese.

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting