# Semantic Logger - Complete Documentation > Semantic Logger is a high-performance, asynchronous structured logging framework for Ruby and Rails. This file concatenates every page of https://logger.rocketjob.io for consumption by AI assistants. It is generated from the markdown sources in docs/ by `bundle exec rake llms_full`; do not edit it directly. A per-page index is available at https://logger.rocketjob.io/llms.txt --- ## What is Semantic Logger? Semantic Logger is a high-performance, asynchronous structured logging framework for Ruby and Rails. It does two things that ordinary loggers do not: 1. **It logs structured data, not just strings.** Along with the usual text message you can attach a payload (any Hash), an exception, a duration, metrics, and tags. That data is preserved all the way to the destination, so it stays searchable instead of being flattened into a line of text. 2. **It logs asynchronously.** Log events are placed on an in-memory queue and written to their destinations by a separate background thread. Your application is not blocked while logs are written to disk, a database, or a remote service. If you are using Rails, use the companion gem [rails_semantic_logger](https://github.com/reidmorrison/rails_semantic_logger), which replaces the Rails logger automatically. See the [Rails guide](rails.html). ## Why use it? ### The problem with traditional logging With the standard Ruby logger you eventually end up building messages by hand: ~~~ruby logger.info("Queried users table in #{duration} ms, with a result code of #{result}") ~~~ That reads fine for a human, but to a machine it is just a string. To answer a question like "show me every query that took longer than 100 ms" you have to write fragile regular expressions against your log files, and every developer formats their messages slightly differently. ### The Semantic Logger way Log the message and the data separately: ~~~ruby logger.info("Queried users table", duration: duration, result: result, table: "users", action: "query") ~~~ Now the same event is still readable by a human, but a machine can index `duration`, `result`, `table`, and `action` as real fields. Send it to a JSON file, Elasticsearch, or Splunk and you can search, filter, and build dashboards on those fields directly, no log parsing required. ### Reasons developers choose Semantic Logger * **Fast.** Logging happens on a background thread, so even logging thousands of lines per second does not slow your application down. See [how it works](#how-it-works) below. * **Structured.** Every log entry can carry a payload, exception, duration, metrics, and tags without losing that structure. * **Human and machine readable at the same time.** Colorized text for developers, JSON for your log aggregator, written from the same log call. * **Many destinations at once.** Write to a file, the screen, and a remote service simultaneously, each with its own log level and format. See [Appenders](appenders.html). * **A familiar API.** It supports the standard `debug`/`info`/`warn`/`error`/`fatal` API, so existing code and other gems keep working. You mostly just change how the logger is created. * **Built for production.** Per-class log levels, change the log level of a running process with a [signal](operations.html#linux-signals), tagged logging for tracing requests across threads, automatic exception capture, and [metrics](metrics.html) for dashboards. ## Quick start Install the gem: ~~~bash gem install semantic_logger ~~~ Or add it to your `Gemfile`: ~~~ruby gem "semantic_logger" ~~~ Configure it once when your application starts, then log: ~~~ruby require "semantic_logger" # Log :info and above. Use :trace, :debug, :info, :warn, :error, or :fatal. SemanticLogger.default_level = :info # Send log output to the screen using the colorized formatter. SemanticLogger.add_appender(io: $stdout, formatter: :color) # Create a logger for the current class. The class name is added to every message. logger = SemanticLogger["MyApp"] logger.info("Application started") ~~~ That is the whole setup. The [Programmer's Guide](api.html) covers the full logging API. ## A tour of the features The examples below assume you already have a `logger` from `SemanticLogger["MyApp"]`. #### Log a message with structured data ~~~ruby logger.error("Outbound call failed", result: :failed, reason_code: -10) ~~~ #### Log an exception Pass the exception directly. The class, message, and backtrace are all captured, including any nested "caused by" exceptions. ~~~ruby begin # ... code that may raise rescue => exception logger.error("Import failed", exception) end ~~~ #### Only evaluate expensive messages when needed Pass a block. It runs only when the log level is active, so it is skipped entirely in production when the level is higher. ~~~ruby logger.debug { "Processed a total of #{records.sum(&:size)} bytes" } ~~~ #### Measure how long something takes The message is written when the block completes, together with its duration. If the block raises, the exception is logged with the duration and then re-raised. ~~~ruby logger.measure_info("Called external interface") do # Code to call the external service ... end ~~~ #### Tag a block of related log entries Every log entry inside the block carries the tags, which is invaluable for tracing one request across many classes and threads. ~~~ruby SemanticLogger.tagged(user: "jbloggs", request_id: "abc123") do logger.info("Hello World") logger.debug("More detail") end ~~~ #### Name your threads So concurrent log entries are easy to tell apart. ~~~ruby Thread.current.name = "worker-1" ~~~ ## How it works Every logger forwards its log events to a single, shared background thread through an in-memory queue. That thread writes each event to every registered appender (destination) in turn. Because the writing happens off to the side, the thread that called `logger.info` returns immediately. This design is why Semantic Logger is both fast and thread safe: log calls from hundreds of concurrent threads simply enqueue events, and each appender writes them out sequentially in the correct order. For the rare cases where you want logging to happen inline on the calling thread, see [Synchronous Operation](operations.html#synchronous-operation). This design is also tunable for production workloads. The in-memory queue is capped by default (blocking when full so that no log message is lost), but it can instead drop messages to protect application availability, give a slow appender its own thread, retry transient appender failures, and be monitored at runtime. See [Performance and reliability tuning](operations.html#performance-and-reliability-tuning) in the Configuration guide. ### Ruby Support For the complete list of supported Ruby versions, see the [Testing file](https://github.com/reidmorrison/semantic_logger/blob/main/.github/workflows/ci.yml). --- ## Programming Guide This guide covers the logging API: how to get a logger, and everything you can do with it. It builds up from the simplest log call to more advanced features further down, so you can read it top to bottom or jump to the part you need. It assumes Semantic Logger is already installed with at least one appender (a destination such as the screen or a file). If not, start with the [quick start](index.html#quick-start), then come back here. For configuring the library itself (log levels, formatters, destinations), see [Configuration](config.html). Every example below uses a `logger` obtained in Step 1. ## Step 1: Get a logger Create one logger per class, passing the class itself: ~~~ruby logger = SemanticLogger[MyClass] ~~~ When there is no class, for example in a script, pass a name instead: ~~~ruby logger = SemanticLogger["MyApp"] ~~~ The class or name you supply is attached to every entry that logger writes, so entries from different parts of the application stay easy to tell apart. Use one logger per class so each entry identifies where it came from. ### The Loggable mixin (recommended) Rather than creating a logger by hand in every class, include `SemanticLogger::Loggable`. It adds a `logger` method to both the class and its instances, already named for the class: ~~~ruby class Supplier include SemanticLogger::Loggable def self.some_class_method logger.debug("Accessible from class methods") end def call_supplier logger.debug("Accessible from instance methods") end end ~~~ Every entry is identified as coming from `Supplier`: ~~~ 2012-08-30 15:37:29.474 I [48308:main] Supplier -- Accessible from instance methods ~~~ By default `SemanticLogger[...]` returns a brand new logger on each call. To share a single logger per class instead, enable [logger caching](config.html#caching-loggers). ## Step 2: Log a message Semantic Logger supports the standard Ruby and Rails logger API, so existing code keeps working: ~~~ruby logger.info("Hello World") ~~~ There is one method per level. The levels, from the most detail to the least, are: :trace :debug :info :warn :error :fatal ~~~ruby logger.trace("Low level detail, such as data sent over a socket") logger.debug("Information to aid problem determination") logger.info("Something normal happened, such as a request received") logger.warn("Something unexpected, but handled") logger.error("An error occurred during processing") logger.fatal("Something really bad happened") ~~~ The active level acts as a threshold. At the default of `:info`, the `info`, `warn`, `error`, and `fatal` calls are written while `debug` and `trace` are skipped. `:trace` is the most detailed level, useful for tracing low level calls such as data exchanged with an external service. Setting the global default level, and changing it at runtime, is covered in [Configuration](config.html#default-log-level). To check whether a level is active (for example before doing expensive work): ~~~ruby logger.info? # => true when :info and above are being logged ~~~ ## Step 3: Add structured data This is what sets Semantic Logger apart. Instead of building a sentence by hand, pass the data as a Hash "payload" after the message: ~~~ruby # Traditional logging bakes the data into a string: logger.info("Queried users in #{duration}ms, result #{result}") # Semantic Logger keeps the message and the data separate: logger.info("Queried users", duration: duration, result: result, table: "users") ~~~ The message stays readable for a human, and the payload stays machine readable: a JSON, MongoDB, or Elasticsearch appender indexes `duration`, `result`, and `table` as real fields, so you can search and build dashboards on them without parsing log text. The fields that make up an entry are listed in [Log Event](log.html). ## Step 4: Log an exception Pass a Ruby exception as the second argument. Its class, message, and backtrace are all captured: ~~~ruby begin # ... code that may raise rescue => exception logger.error("Outbound call failed", exception) end ~~~ To log a payload and an exception together, pass both: ~~~ruby logger.error("Outbound call failed", {result: :failed}, exception) ~~~ ## Step 5: Skip expensive messages with a block When building the message is itself expensive, pass a block instead of a string. The block runs only when the level is active, so it costs nothing when that level is turned off (for example in production): ~~~ruby logger.debug { "Processed #{records.sum(&:size)} bytes across #{records.size} records" } ~~~ ### The full call signature Putting the pieces together, every level method accepts: ~~~ruby logger.info(message, payload_or_exception = nil, exception = nil, &block) ~~~ - `message`: the text message. Optional only when a block is supplied. - `payload_or_exception`: an optional Hash payload, or a Ruby exception. - `exception`: an optional exception, used when you are also passing a payload. - `&block`: evaluated only when the level is active; its return value becomes the message. The same call can also be written as a single Hash, which is handy when assembling fields programmatically: ~~~ruby logger.debug(message: "Calling Supplier", payload: {request: "update", user: "Jack"}) # Log a complete exception logger.error(message: "Calling Supplier", exception: exception) # Attach a duration of 100ms logger.error(message: "Calling Supplier", duration: 100) # Attach a metric (see Step 6) logger.error(message: "Calling Supplier", metric: "Supplier/inquiry", metric_amount: 21) ~~~ ## Step 6: Measure how long something takes It is good practice to "measure everything" in production, so that when things slow down it is obvious where the time is going. Wrap the code in a `measure_*` call: ~~~ruby logger.measure_info("Called external interface") do # Code to call the external service ... end ~~~ The entry is written once the block completes, and includes the duration: ~~~ 2012-08-30 15:37:29.474 I [48308:script/rails] (5.2ms) Rails -- Called external interface ~~~ If the block raises, the exception is logged at the same level along with the duration, then re-raised unchanged. There is a measure method for every level: ~~~ruby logger.measure_trace / measure_debug / measure_info / measure_warn / measure_error / measure_fatal ~~~ Or supply the level dynamically as the first argument: ~~~ruby logger.measure(:info, "Request received") do # ... end ~~~ ### Only log when it is slow (elastic logging) Pass `min_duration` (in milliseconds) to log only when the block runs longer than the threshold. This surfaces slow calls without the noise of the fast ones: ~~~ruby logger.measure_warn("Called memcache", min_duration: 3) do # Usually fast; only logged when it takes longer than 3 ms end ~~~ ### Record a metric Attach a `metric` name to feed dashboards. See [Metrics](metrics.html): ~~~ruby logger.measure_info("Called external interface", metric: "Supplier/inquiry") do # ... end ~~~ ### Supply the duration yourself When you already have a duration, log it without a block. This keeps the duration as structured data rather than embedding it in the message text: ~~~ruby duration = Time.now - start_time logger.measure_info("Called external interface", duration: duration) ~~~ Either a block or `:duration` must be supplied on every measure call. ### All measure options The second argument to a measure call is a Hash of options: - `:min_duration` [Float]: only log if the block takes longer than this many milliseconds. Default `0.0` (always log). - `:metric` [String]: notify metric subscribers with this metric name. - `:payload` [Hash]: an optional payload to log with the entry. - `:exception` [Exception]: an exception to log along with the duration. - `:duration` [Float]: the duration in ms, used when no block is supplied (then it is mandatory; with a block it is ignored). - `:log_exception` [Symbol]: how to report an exception raised in the block. `:full` logs the class, message, and backtrace; `:partial` logs the class and message only; `:off` does not log it. Default `:partial`. - `:on_exception_level` [Symbol]: if an exception is raised, raise the log level to this level. - `:silence` [Symbol]: the level to silence other log messages to within the block (current thread only). Putting several together: ~~~ruby logger.measure_info("Called external interface", log_exception: :full, min_duration: 100, metric: "Custom/Supplier/process") do # Code to call the external service ... end ~~~ ## Step 7: Tag related entries In a concurrent application it is invaluable to find every entry that belongs to one request or job. `tagged` adds tags to every entry logged inside its block: ~~~ruby tracking_number = "15354128" SemanticLogger.tagged(tracking_number) do logger.debug("Hello World") # this entry carries the tracking_number tag end ~~~ Prefer named tags. They are clearer as a system grows, and easier to filter and alert on in a centralized logging system: ~~~ruby SemanticLogger.tagged(user: "Jack", zip_code: 12345) do logger.debug("Hello World") # carries user and zip_code end ~~~ Tags are scoped to the current thread, so a new thread started inside the block does not inherit them. [Parallel Minion](https://github.com/reidmorrison/parallel_minion) creates threads that copy the tags across automatically. ### Bind tags to one logger (child loggers) The block form above scopes tags to the thread. Sometimes it is more convenient to bind tags to a single logger instance, for example when the logger belongs to an object with its own identity (an ActiveRecord model or a background job). Calling `tagged` (or its alias `with_tags`) **without a block** returns a new "child" logger that permanently carries the supplied tags. Every entry from that child, and only that child, includes them, even across threads: ~~~ruby class Cart include SemanticLogger::Loggable def initialize(id) @id = id # Bind this Cart's identity to its own logger instance. @logger = SemanticLogger["Cart"].tagged(cart_id: id) end attr_reader :logger def add_item(item_id) # Automatically tagged with cart_id, without wrapping every method in a block. logger.info("Added item", item_id: item_id) end end ~~~ Positional and named tags can be mixed: ~~~ruby logger = SemanticLogger["Payments"].tagged("billing", region: "eu") logger.info("Charged card") # tagged with ["billing"] and {region: "eu"} ~~~ Notes: - The original logger is never modified; `tagged` returns a copy. Child loggers can be nested, each level adding to the tags inherited from its parent. - Instance tags combine with any thread tags from a surrounding `tagged` block: positional thread tags come first, then the logger's instance tags. For named tags, the logger's own tags win on a key conflict, since they represent its identity. - Child loggers are ordinary instances, registered nowhere, so they are garbage collected along with the object that owns them. ## Going further The features above cover everyday logging. The rest of this guide covers less common needs. ### Name your threads Semantic Logger includes the thread name (or id) in every entry. On Ruby MRI the name defaults to the thread's object id: ~~~ 2013-11-07 16:25:14.279 I [35841:70184354571980] (0.0ms) ExternalSupplier -- Calling external interface ~~~ Give a thread a readable name so it stands out in the logs: ~~~ruby Thread.current.name = "User calculation thread 32" ~~~ ~~~ 2013-11-07 16:26:02.744 I [35841:User calculation thread 32] (0.0ms) ExternalSupplier -- Calling external interface ~~~ Keep the name unique, otherwise concurrent threads are hard to tell apart. Including the object id is one way to guarantee that: ~~~ruby Thread.current.name = "Worker Thread:#{Thread.current.object_id}" ~~~ On JRuby this also sets the underlying JVM thread name, which is useful when monitoring the JVM over JMX with tools such as jconsole. ### Change one class's level at runtime Because each class has its own logger, you can change one class's level on the fly, for example to temporarily turn on `:trace` while diagnosing an issue, without touching the rest of the application: ~~~ruby # Raise the detail for this one class ExternalSupplier.logger.level = :trace # ... reproduce the issue; trace entries from ExternalSupplier are now logged ... # Return it to following the global default level ExternalSupplier.logger.level = nil ~~~ To change the global default level for every logger that has not been set explicitly, set `SemanticLogger.default_level`. See [Configuration](config.html#default-log-level), and [Signals](operations.html#linux-signals) for changing it in a running process without a restart. ### Silence noisy code `silence` raises the level within a block, on the current thread only, to quiet a noisy section: ~~~ruby # Within this block, log only :error and above logger.silence do logger.info "not logged" logger.warn "not logged" logger.error "but errors are logged" end ~~~ It can also lower the level within the block, to get more detail from one section: ~~~ruby logger.silence(:trace) do logger.debug "logged, even though the default level is higher" end ~~~ `silence` has no effect on loggers whose level was set explicitly (those that do not follow the global default), and does not affect threads spawned inside the block. ### Map a noisy gem's debug logs to trace Some third party gems log a lot at `:debug`, because they do not have Semantic Logger's `:trace` level. Wrap such a library's logger in `SemanticLogger::DebugAsTraceLogger` so its `debug` calls are recorded as `:trace`, keeping them out of your `:debug` output: ~~~ruby logger = SemanticLogger::DebugAsTraceLogger.new("NoisyLibrary") logger.debug "Some very low level noisy message" # logged as :trace ~~~ ### Capture causal (nested) exceptions When one exception is rescued and another raised, Ruby records the original as the new exception's `cause`. Semantic Logger logs the whole chain automatically: ~~~ruby def oh_no File.new("filename", "w").read # raises IOError: not opened for reading rescue IOError raise RuntimeError, "Failed to write to file" end begin oh_no rescue StandardError => exception logger.error("Failed calling oh_no", exception) end ~~~ Both exceptions are logged, the second backtrace starting at `Cause:`: ~~~ E [17641:70311685126260 demo.rb:17] Demo -- Failed calling oh_no -- Exception: RuntimeError: Failed to write to file demo.rb:6:in `rescue in oh_no' demo.rb:2:in `oh_no' Cause: IOError: not opened for reading demo.rb:4:in `read' demo.rb:4:in `oh_no' ~~~ ### Replace the logger in other gems Rails Semantic Logger already replaces the loggers for many gems. When using Semantic Logger stand-alone, hand them a Semantic Logger instance yourself: ~~~ruby Resque.logger = SemanticLogger[Resque] if defined?(Resque) && Resque.respond_to?(:logger) Mongoid.logger = SemanticLogger[Mongoid] if defined?(Mongoid) Sidekiq.configure_server do |config| config.logger = SemanticLogger[Sidekiq] end ~~~ ## Next steps - [Log Event](log.html): the structure of every entry your filters, formatters, and appenders receive. - [Configuration](config.html): global settings, custom formatters, filtering, and destinations. - [Operations](operations.html): process forking, performance tuning, signals, and log rotation. --- ## Configuration Semantic Logger is configured once, when your application starts, by setting a few global values and adding one or more appenders (destinations). This page walks through that configuration from the simplest, most common settings at the top to formatter and appender customization further down. Production concerns such as process forking, performance tuning, signals, and log rotation are covered separately in [Operations](operations.html). If you are using Rails, most of this is handled for you by the companion gem [rails_semantic_logger](rails.html); configure it through `config.semantic_logger` and `config.rails_semantic_logger` instead. A minimal configuration looks like this: ~~~ruby require "semantic_logger" # 1. Choose the lowest level to log. SemanticLogger.default_level = :info # 2. Add at least one destination. SemanticLogger.add_appender(io: $stdout, formatter: :color) # 3. Get a logger and use it. logger = SemanticLogger["MyApp"] logger.info("Ready") ~~~ The sections below explain each piece, and everything you can tune around it. --- ## Global settings These module level settings apply to the whole process. Set them once, before or just after adding your appenders. ### Default log level Semantic Logger logs `:info` and above by default. The levels, from most detail to least, are: :trace, :debug, :info, :warn, :error, :fatal Setting the level to `:debug` includes `:info`, `:warn`, `:error`, and `:fatal`, but not `:trace`. To log everything, set the global default to `:trace`: ~~~ruby SemanticLogger.default_level = :trace ~~~ Every logger and appender uses this global default unless it has been given its own level. Once a logger or appender has an explicit level, changing `SemanticLogger.default_level` no longer affects it. The global default can also be changed at runtime (see [Signals](operations.html#linux-signals) for changing it in a running process without a restart): ~~~ruby SemanticLogger.default_level = :debug ~~~ ### Application, environment, and host name Semantic Logger can include the application name, environment, and host name in every log entry. Not every appender uses these fields, but structured appenders (JSON, Elasticsearch, Splunk, and so on) and centralized logging systems rely on them to tell apart logs coming from different applications and servers: ~~~ruby SemanticLogger.application = "my_app" SemanticLogger.environment = "production" SemanticLogger.host = "web-server-1" ~~~ When not set explicitly, these default to: - `application`: the `SEMANTIC_LOGGER_APP` environment variable, otherwise `"Semantic Logger"`. - `environment`: the first of `SEMANTIC_LOGGER_ENV`, `RAILS_ENV`, or `RACK_ENV` that is set. - `host`: the machine's host name. Each value can also be overridden for a single appender by passing `application:`, `environment:`, or `host:` to `add_appender` (see [Per-appender settings](#per-appender-settings)). ### Capturing backtraces Semantic Logger can capture the file name and line number where each log entry was created, include it in the output, and forward it to error services such as Bugsnag. Capturing a backtrace is expensive, so it is controlled by its own level, which defaults to `:error`. Only entries at this level or higher capture a backtrace: ~~~ruby # Capture backtraces for :error and :fatal entries (the default) SemanticLogger.backtrace_level = :error ~~~ To capture a backtrace for every entry, set it to `:trace`. To turn backtrace capture off entirely, set it to `nil`. It is strongly recommended to leave this at `:error` or higher in production. ### Caching loggers By default `SemanticLogger[...]` returns a brand new logger instance on every call. Enable logger caching to have a single shared logger returned per class: ~~~ruby SemanticLogger.cache_loggers = true SemanticLogger[MyClass].equal?(SemanticLogger[MyClass]) # => true ~~~ This makes it possible to obtain a logger once and later change its level (or filter) so that every holder of that logger sees the change: ~~~ruby SemanticLogger[MyClass].level = :debug ~~~ Notes: - Caching is opt-in and disabled by default. - Only Classes and Modules are cached. A String always returns a new instance, since string call sites commonly want an independent logger (for example to set a different level per call site). - Anonymous classes (those without a name) are never cached. - With caching enabled, `SemanticLogger[MyClass]` and the `SemanticLogger::Loggable` mixin's `MyClass.logger` return the same instance. - Setting `SemanticLogger.cache_loggers = false` clears the cache. It can also be cleared explicitly with `SemanticLogger.clear_logger_cache`, for example after redefining a class. --- ## Per-appender settings Each destination is added with `SemanticLogger.add_appender`. The full catalogue of destinations, and the options specific to each, is in [Appenders](appenders.html). In addition to its own settings, almost every appender accepts these common options: | Option | Description | |--------|-------------| | `level` | Only write entries at this level or higher to this appender. Defaults to `SemanticLogger.default_level`. | | `formatter` | How to format the output, for example `:default`, `:color`, or `:json`. See [Custom formatters](#custom-formatters). | | `filter` | A `Regexp` or `Proc` selecting which entries this appender accepts. See [Filtering](#filtering). | | `application`, `environment`, `host` | Override the global values for this appender only. | A per-appender `level` lets each destination keep a different subset of the logs. For example, keep a full trace log and a separate warnings-only log: ~~~ruby require "semantic_logger" SemanticLogger.default_level = :info # Everything at :trace and above: SemanticLogger.add_appender(file_name: "log/trace.log", level: :trace) # Only warnings and above: SemanticLogger.add_appender(file_name: "log/warnings.log", level: :warn) ~~~ --- ## Filtering A filter decides, for each log entry, whether it should be written. Use a filter to: * Quiet a noisy library without modifying its code. * Send only certain messages to a particular destination (for example, a dedicated audit file). * Strip sensitive data out of a message before it is written. ### A filter is one of three things 1. **A regular expression.** It is matched against the **class name** of the logger (the value you passed to `SemanticLogger["..."]`). The entry is kept only if the class name matches. ```ruby filter: /MyClass/ ``` 2. **A Proc (or lambda).** It receives the whole log event and must return `true` to **keep** the entry. Returning anything else (`false`, `nil`, a string, ...) **drops** it. ```ruby filter: ->(log) { log.message !~ /heartbeat/ } ``` 3. **A module or object that responds to `.call`.** Same contract as a Proc, but in a named, reusable, testable place. Reach for this when the logic grows beyond a one-liner. ```ruby module ExcludeHealthChecks def self.call(log) !log.message.to_s.start_with?("GET /health") end end filter: ExcludeHealthChecks ``` > **The most common gotcha:** a Proc or `.call` filter must return **exactly `true`** to keep an > entry. `0`, `"yes"`, or any truthy-but-not-`true` value will silently drop the entry. When in > doubt, end the filter with an explicit boolean expression. The log event passed to a Proc or module filter carries every attribute of the message: `name`, `message`, `level`, `payload`, `tags`, `named_tags`, `duration`, `exception`, and more. The full list is in [Log Event](log.html). ### Where a filter can be attached * **On an appender** (a destination). The filter affects only what *that one destination* writes. Use this to give one file or service a curated subset of the logs. * **On a logger instance.** The filter affects *every* appender, but only for entries coming *through that one logger*. Use this to quiet a single class or library across all destinations. ### Example: appender filter with a regular expression Keep a full log in `development.log`, and additionally maintain a `my_class.log` that contains **only** the messages from `MyClass`: ```ruby require "semantic_logger" # Step 1: a catch-all appender that records everything. SemanticLogger.add_appender(file_name: "development.log") # Step 2: a second appender that only keeps entries whose logger class name matches /MyClass/. SemanticLogger.add_appender(file_name: "my_class.log", filter: /MyClass/) # Step 3: log from two different classes. SemanticLogger["MyClass"].info "Written to BOTH development.log and my_class.log" SemanticLogger["OtherClass"].info "Written ONLY to development.log" ``` You can also set the filter after the appender exists: ```ruby appender = SemanticLogger.add_appender(file_name: "my_class.log") appender.filter = /MyClass/ ``` ### Example: appender filter with a Proc A `summary.log` that contains everything **except** the (very chatty) messages from `MyClass`: ```ruby SemanticLogger.add_appender( file_name: "summary.log", # Keep the entry (return true) unless it came from MyClass. filter: ->(log) { log.name != "MyClass" } ) ``` Because the Proc receives the whole log event, you can filter on anything: ```ruby # Drop entries below a duration threshold (only keep slow measure calls). filter: ->(log) { log.duration.to_f >= 100 } # Drop a specific noisy message regardless of which class logged it. filter: ->(log) { log.message !~ /\Aheartbeat/ } ``` ### Example: rewriting a message inside a filter A filter can also **modify** the log event before it is written, as long as it still returns `true` so the (now edited) entry is kept. This is handy for redacting sensitive data. Resque, for example, logs the entire job payload, which may contain private information: ```ruby Resque.logger.filter = ->(log) do if log.name == "Resque" && (match = log.message.to_s.match(/\A(got|done): /)) log.message = match[1] # replace the full payload with just the action end true # always return true so the (edited) message is still logged end ``` ### Example: logger filter to quiet a library When a library lets you replace its logger, attach a filter to a logger instance to suppress its noise everywhere it is logged, without touching any appender: ```ruby logger = SemanticLogger[Resque] logger.filter = ->(log) { log.message !~ /\A\*\*\* Checking/ } Resque.logger = logger ``` --- ## Custom formatters The formatter turns each log event into the text or JSON that an appender writes. Pass a `formatter:` when adding an appender. The simplest options are the built-in formatters selected by name (`:default`, `:color`, `:json`, `:logfmt`, and others; see [Appenders](appenders.html)). For anything beyond those, you have three choices, in increasing order of effort: a pattern string, a Proc, or a formatter class. ### Pattern formatter For simple layout changes there is no need to write any code. The built-in `:pattern` formatter builds each log line from a pattern string. Placeholders use the form `%{directive}`; to emit a literal `%{...}`, escape it as `%%{...}`. ~~~ruby # A message-only format on stdout, for end users: SemanticLogger.add_appender( io: $stdout, formatter: {pattern: {pattern: "%{message}"}} ) # A timestamped format to a file: SemanticLogger.add_appender( file_name: "application.log", formatter: {pattern: {pattern: "%{time} %{level} %{name} -- %{message}"}} ) ~~~ The pattern is parsed once when the appender is created, so formatting every entry is fast. An unknown directive (or an argument supplied to a directive that does not take one) raises an error immediately, when the appender is configured. Available directives: | Directive | Description | |------------------------|----------------------------------------------------------| | `%{time}` | Formatted timestamp. A strftime format may be supplied, e.g. `%{time:%Y-%m-%dT%H:%M:%S.%6N}`. | | `%{level}` | Full level name, e.g. `debug`. | | `%{level_short}` | Single character level, e.g. `D`. | | `%{name}` | Logger / class name. | | `%{message}` | Log message. | | `%{payload}` | Payload rendered as a string. | | `%{exception_class}` | Class of the logged exception, e.g. `RuntimeError`. | | `%{exception_message}` | Message of the logged exception. | | `%{backtrace}` | Backtrace of the logged exception. | | `%{duration}` | Human readable duration, e.g. `1.2ms`. | | `%{duration_ms}` | Duration in milliseconds (numeric). | | `%{thread_name}` | Name of the thread that logged the message. | | `%{pid}` | Process id. | | `%{file_name}` | Ruby file name that logged the message, e.g. `app.rb`. | | `%{line}` | Line number within the Ruby file, e.g. `42`. | | `%{tags}` | Tags, comma separated. | | `%{named_tags}` | All named tags. One tag with `%{named_tags:request_id}`. | | `%{host}` | Host name. | | `%{application}` | Application name. | | `%{environment}` | Environment name. | When the pattern is omitted it defaults to a layout similar to the default text formatter: `%{time} %{level} [%{pid}:%{thread_name}] %{name} -- %{message}`. #### Example: a custom timestamp format The `%{time}` directive accepts a [strftime](https://ruby-doc.org/core/Time.html#method-i-strftime) format string, applied directly to the log time: ~~~ruby SemanticLogger.add_appender( io: $stdout, formatter: {pattern: {pattern: "%{time:%Y-%m-%dT%H:%M:%S.%6N%z} %{level} -- %{message}"}} ) # => 2017-04-05T01:05:52.868286+0000 info -- Hello World ~~~ #### Example: include a request id from the named tags Named tags (set with `SemanticLogger.tagged(request_id: "...")`) can be pulled out individually with `%{named_tags:key}`: ~~~ruby SemanticLogger.add_appender( io: $stdout, formatter: {pattern: {pattern: "%{time} %{level} [%{named_tags:request_id}] %{name} -- %{message}"}} ) ~~~ ### Formatter as a Proc For full control with minimal ceremony, supply a block. It is called with two arguments, the log event and the appender; a block may accept just the log event and ignore the appender. For the structure of the log event, see [Log Event](log.html). ~~~ruby formatter = proc do |log| # This formatter just returns the log event as a string log.inspect end SemanticLogger.add_appender(io: $stdout, formatter: formatter) ~~~ ### Formatter as a class When the formatting logic is substantial or reused, subclass one of the built-in formatters and override just the methods you want to change. Override the default text formatter to upper-case the level name: ~~~ruby class MyFormatter < SemanticLogger::Formatters::Default # Return the complete log level name in uppercase def level log.level.upcase end end SemanticLogger.add_appender(file_name: "development.log", formatter: MyFormatter.new) ~~~ The colorized formatter can be customized the same way, keeping its color codes: ~~~ruby class MyFormatter < SemanticLogger::Formatters::Color def level "#{color}#{log.level.upcase}#{color_map.clear}" end end SemanticLogger.add_appender(file_name: "development.log", formatter: MyFormatter.new) ~~~ A common request is to leave out the process id, for example when running a single process per container (where the pid is always 1): ~~~ruby class NoPidFormatter < SemanticLogger::Formatters::Default # Leave out the pid def pid end end SemanticLogger.add_appender(file_name: "development.log", formatter: NoPidFormatter.new) ~~~ See [SemanticLogger::Formatters::Default](https://github.com/reidmorrison/semantic_logger/blob/main/lib/semantic_logger/formatters/default.rb) and [SemanticLogger::Formatters::Color](https://github.com/reidmorrison/semantic_logger/blob/main/lib/semantic_logger/formatters/color.rb) for all the methods that can be overridden. To replace the formatter on an appender that is already installed, for example in a Rails app: ~~~ruby # Find the file appender and replace its formatter: appender = SemanticLogger.appenders.find { |a| a.is_a?(SemanticLogger::Appender::File) } appender.formatter = MyFormatter.new ~~~ ### Escaping control characters By design, the human readable text formatters (`:default` and `:color`) write log messages exactly as supplied, including newlines and ANSI color codes. This is intentional and useful: multi-line messages and colorized output make local logs easier to read. When log messages can contain untrusted, attacker-controlled data (for example a user name, request parameter, or `User-Agent` header), those same characters can be abused. A newline can forge an additional, fake log entry ("log forging"), and an ANSI escape sequence can spoof or hide terminal output when the log is viewed in a terminal. Structured formatters such as `:json` are not affected, because JSON encoding always escapes control characters. They are the recommended choice when forwarding logs that may contain untrusted data to a centralized logging system. For the text formatters, enable the `escape_control_chars` option to replace control characters in untrusted log data (the message, tags, named tags, and exception message) with a printable, escaped form. For example a newline is written as `\n` and the ANSI escape as `\e`. The option is **disabled by default** to preserve the existing human readable output: ```ruby # Text appender that escapes control characters in untrusted data: SemanticLogger.add_appender(file_name: "production.log", formatter: {default: {escape_control_chars: true}}) # Colorized appender, still escaping control characters in the logged data # (the formatter's own color codes are preserved): SemanticLogger.add_appender(io: $stdout, formatter: {color: {escape_control_chars: true}}) ``` The option only escapes the control characters in the logged data; it does not touch the formatter's own decoration, so the `:color` formatter keeps emitting its color codes. Multi-line exception backtraces are also preserved, since they are generated by Semantic Logger rather than supplied as log data. The pattern formatter supports the same option (`{pattern: {pattern: "...", escape_control_chars: true}}`), and the syslog, TCP, and UDP appenders enable it where appropriate; see [Appenders](appenders.html). --- ## Custom appenders To write your own log appender it should meet the following requirements: * Inherit from `SemanticLogger::Subscriber`. * In the initializer, connect to the resource being logged to. * Implement `#log(log)`, which writes to the relevant resource. * Implement `#flush` if the resource can be flushed. * Write a test for the new appender. The `#log` method receives the log event as its parameter. For its structure, see [Log Event](log.html). Basic outline for an appender: ~~~ruby require "semantic_logger" class SimpleAppender < SemanticLogger::Subscriber attr_reader :host # Add additional arguments to the initializer while supporting all existing ones. def initialize(host: host, **args, &block) @host = host super(**args, &block) end # Display the log struct and the text formatted output def log(log) # Display the raw log structure p log # Display the formatted output puts formatter.call(log, self) end # Optional def flush puts "Flush :)" end # Optional def close puts "Closing :)" end end ~~~ Register the appender by passing an instance to `add_appender`: ~~~ruby SemanticLogger.add_appender(appender: SimpleAppender.new) ~~~ Look at the [existing appenders](https://github.com/reidmorrison/semantic_logger/tree/main/lib/semantic_logger/appender) for good examples. To have a custom appender included in the standard list, submit it with complete working tests; see the [Graylog Appender Test](https://github.com/reidmorrison/semantic_logger/blob/main/test/appender/graylog_test.rb) for an example. --- ## Managing appenders and lifecycle ### Adding and removing appenders `SemanticLogger.add_appender` returns the appender it created, which can be used to remove that appender later: ~~~ruby appender = SemanticLogger.add_appender(file_name: "development.log") # ... later SemanticLogger.remove_appender(appender) ~~~ Other appender management methods: ~~~ruby # The list of currently active appenders SemanticLogger.appenders # Remove and close every appender SemanticLogger.clear_appenders! # Flush all appenders, then close them ( called automatically at process exit ) SemanticLogger.close ~~~ ### Flushing Semantic Logger automatically flushes all appenders (log files, etc.) when a process exits. The `flush` method is not defined on individual logger instances, since there may be many of them. To perform a global flush of all appenders and wait for any queued messages to be written: ~~~ruby SemanticLogger.flush ~~~ ### Capturing context with `on_log` Register a block to be called for every log entry, just before it is placed on the queue. The block runs inline on the thread that created the entry, so it can capture request-scoped or thread-local context that would otherwise be lost once the entry is handed off to the background thread: ~~~ruby SemanticLogger.on_log do |log| log.set_context(:request_id, Thread.current[:request_id]) end ~~~ Because these callbacks run on the application's own thread, keep them fast. The captured context is available to appenders and formatters as `log.context`. --- ## Appenders An **appender** is a destination that log entries are written to: a file, the screen, a database, or a remote service. Semantic Logger can write to several appenders at the same time, each with its own log level and format, all from the same log call. For example you might log colorized text to the screen, JSON to a file, and errors to an external service simultaneously. ## Step 1: Add a destination Every destination is added through one method, `SemanticLogger.add_appender`, usually once at startup. The keyword you pass selects the kind of destination: ~~~ruby # A text file SemanticLogger.add_appender(file_name: "development.log") # An IO stream, such as the screen SemanticLogger.add_appender(io: $stdout) # A built-in appender, selected by name SemanticLogger.add_appender(appender: :elasticsearch, url: "http://localhost:9200") # An existing Ruby or Rails logger SemanticLogger.add_appender(logger: Logger.new($stdout)) # A metrics destination (see the Metrics guide) SemanticLogger.add_appender(metric: :statsd, url: "udp://localhost:8125") ~~~ In a Rails app using [rails_semantic_logger](rails.html), put `add_appender` calls in an initializer such as `config/initializers/semantic_logger.rb`. Otherwise add them wherever you configure Semantic Logger when the application boots. ## Step 2: Set the level, format, and filter In addition to its own settings, almost every appender accepts these common options, so each destination can keep a different subset of the logs in a different format: | Option | Description | |--------|-------------| | `level` | Only write entries at this level or higher to this appender. Defaults to `SemanticLogger.default_level`. | | `formatter` | How to format the output, for example `:default`, `:color`, or `:json`. See [Custom formatters](config.html#custom-formatters). | | `filter` | A `Regexp` or `Proc` selecting which entries this appender accepts. See [Filtering](config.html#filtering). | | `application`, `environment`, `host` | Override the global values for this appender only. | For example, write only warnings and above to a file, formatted as JSON: ~~~ruby SemanticLogger.add_appender(file_name: "errors.log", level: :warn, formatter: :json) ~~~ ## Step 3: Pick your destinations The tables below list every built-in destination. Pick the ones you need and jump to their section for a full example. Appenders for third-party services need their backing gem (shown in the "Gem" column). Add it to your `Gemfile` and run `bundle install`, or `gem install` it directly. The gem is loaded lazily the first time the appender is used, and is never a hard dependency of Semantic Logger itself. **Files and streams** | Destination | `add_appender` argument | Gem | |-------------|-------------------------|-----| | [Text file](#text-file) | `file_name:` | | | [IO stream](#io-streams) (`$stdout`, `$stderr`, ...) | `io:` | | | [Another Ruby or Rails logger](#logger-log4r-etc) | `logger:` | | **Network protocols** | Destination | `add_appender` argument | Gem | |-------------|-------------------------|-----| | [HTTP(S)](#https) | `appender: :http` | | | [TCP (+SSL)](#tcp-appender-ssl) | `appender: :tcp` | `net_tcp_client` | | [UDP](#udp-appender) | `appender: :udp` | | | [Syslog](#syslog) | `appender: :syslog` | `syslog_protocol`, `net_tcp_client` (remote) | **Centralized logging and log aggregators** | Destination | `add_appender` argument | Gem | |-------------|-------------------------|-----| | [Elasticsearch](#elasticsearch) | `appender: :elasticsearch` | `elasticsearch` | | [OpenSearch](#opensearch) | `appender: :opensearch` | `opensearch-ruby` | | [Graylog](#graylog) | `appender: :graylog` | `gelf` | | [Splunk over HTTP](#splunk-http) | `appender: :splunk_http` | | | [Splunk SDK](#splunk-sdk) | `appender: :splunk` | `splunk-sdk-ruby` | | [Grafana Loki](#grafana-loki) | `appender: :loki` | | | [CloudWatch Logs](#cloudwatch-logs) | `appender: :cloudwatch_logs` | `aws-sdk-cloudwatchlogs` | | [OpenTelemetry](#opentelemetry) | `appender: :open_telemetry` | `opentelemetry-logs-sdk` | | [Logstash](#logstash) | `logger:` | `logstash-logger` | | [logentries.com](#logentriescom) | `appender: :tcp` | `net_tcp_client` | | [loggly.com](#logglycom) | `appender: :http` | | | [Papertrail](#papertrail) | `appender: :syslog` | `syslog_protocol`, `net_tcp_client` | **Error and exception monitoring** | Destination | `add_appender` argument | Gem | |-------------|-------------------------|-----| | [Bugsnag](#bugsnag) | `appender: :bugsnag` | `bugsnag` | | [Sentry](#sentry) | `appender: :sentry_ruby` | `sentry-ruby` | | [Honeybadger](#honeybadger-and-honeybadger-insights) | `appender: :honeybadger` | `honeybadger` | | [Honeybadger Insights](#honeybadger-and-honeybadger-insights) | `appender: :honeybadger_insights` | `honeybadger` | | [New Relic](#newrelic) | `appender: :new_relic` | `newrelic_rpm` | | [Rollbar](#rollbar) | via `SemanticLogger.on_log` | `rollbar` | **Databases and message queues** | Destination | `add_appender` argument | Gem | |-------------|-------------------------|-----| | [MongoDB](#mongodb) | `appender: :mongodb` | `mongo` | | [Apache Kafka](#apache-kafka) | `appender: :kafka` | `ruby-kafka` | | [RabbitMQ](#rabbitmq-amqp) | `appender: :rabbitmq` | `bunny` | For metrics destinations such as Statsd, SignalFx, and New Relic, see [Metrics](metrics.html). > **Tip:** To ensure no log messages are lost, prefer TCP over UDP. Because of Semantic Logger's > asynchronous design, the performance difference between the two will not impact your application. ## Files and streams ### Text File Log to a file, choosing a formatter to suit the reader: ~~~ruby # Standard text: SemanticLogger.add_appender(file_name: "development.log") # Colorized text, for a terminal: SemanticLogger.add_appender(file_name: "development.log", formatter: :color) # JSON, for a machine: SemanticLogger.add_appender(file_name: "development.log", formatter: :json) ~~~ The file is opened when the appender is added, so a bad path or insufficient permissions raise immediately from `add_appender` instead of failing later on the logging thread. For performance the log file is not re-opened on every call, so rotate it with a copy-truncate operation rather than deleting the file. See [Log rotation](operations.html#log-rotation). Log files frequently contain sensitive information. By default the file is created using the process umask (the standard Ruby behavior). To restrict access, supply `permissions:`, applied both when the file is created and to an existing log file: ~~~ruby # Owner read/write, group read, no access for others: SemanticLogger.add_appender(file_name: "production.log", permissions: 0o640) ~~~ ### IO Streams Log to any IO stream instance, such as `$stdout` or `$stderr`: ~~~ruby # Log errors and above to standard error: SemanticLogger.add_appender(io: $stderr, level: :error) ~~~ #### Splitting output across stdout and stderr A common pattern routes lower severity entries to `$stdout` and warnings and errors to `$stderr`. Semantic Logger allows one appender per console stream, so add one for each and use `level:` and/or `filter:` to control what each writes: ~~~ruby stdout_filter = ->(log) { %i[trace debug info].include?(log.level) } # Informational messages to stdout: SemanticLogger.add_appender(io: $stdout, formatter: :color, level: :trace, filter: stdout_filter) # Warnings and above to stderr: SemanticLogger.add_appender(io: $stderr, formatter: :color, level: :warn) ~~~ Adding a second appender for a console stream that already has one is ignored (to avoid duplicate console output), but `$stdout` and `$stderr` are tracked separately. ### Logger, log4r, etc. Semantic Logger can write to another logging library, either to gain the Semantic Logger interface while still writing to an existing destination, or to reach a destination it does not support natively: ~~~ruby ruby_logger = Logger.new($stdout) # Log to an existing Ruby Logger instance SemanticLogger.add_appender(logger: ruby_logger) ~~~ Note: `:trace` level messages are mapped to `:debug`. ## Structured output formats The file, IO, and HTTP appenders can emit machine-readable output by choosing a structured `formatter`. Each format below produces a single line per entry. ### JSON `formatter: :json` produces output with this layout: ~~~json { "timestamp": "ISO-8601", "application": "Application name", "environment": "Custom Environment name", "host": "Host name", "pid": "Process Id", "thread": "Thread name or id", "file": "filename", "line": "line number", "level": "trace|debug|info|warn|error|fatal", "level_index": "0|1|2|3|4|5", "message": "The message text without any colorization", "name": "Name of the class that generated the log message. Including namespace, if any.", "tags": ["tag_name 1", "tag_name 2"], "duration": "Human readable duration", "duration_ms": "Duration in milliseconds", "metric": "Name of the metric", "metric_amount": "Size of the metric, usually 1", "named_tags": { "tag1": "any named tags will be inside this named_tags tag", "tag2": "any named tags will be inside this named_tags tag" }, "payload": { "field1": "any custom payload fields will be inside this payload tag", "field2": "any custom payload fields will be inside this payload tag" }, "exception": { "name": "Exception class name", "message": "Exception message", "stack_trace": ["line 1", "line 2"], "cause": { "name": "Exception class name", "message": "Exception message", "stack_trace": ["line 1", "line 2"] } } } ~~~ Notes: * The layout above is formatted for readability. The real output is a single line terminated by one newline, with no embedded newlines. * A field with a `nil` value is excluded from the output. ### Fluentd `formatter: :fluentd` is the same as `:json`, except it renames the log level fields to `severity` and `severity_index` so they are recognized by the Kubernetes Fluentd log collector: ~~~ruby SemanticLogger.add_appender(io: $stdout, formatter: :fluentd) ~~~ Differences from `:json`: * `level` / `level_index` become `severity` / `severity_index`. * `host` is excluded by default (under Fluentd it is usually the not-very-useful container id). Pass `log_host: true` to include it. * The process fields `pid`, `thread`, `file`, and `line` are excluded by default. Pass `need_process_info: true` to include them. Construct the formatter explicitly to override these defaults: ~~~ruby formatter = SemanticLogger::Formatters::Fluentd.new(log_host: true, need_process_info: true) SemanticLogger.add_appender(io: $stdout, formatter: formatter) ~~~ ### ECS (Elastic Common Schema) `formatter: :ecs` emits each entry using the nested field names of the [Elastic Common Schema](https://www.elastic.co/docs/reference/ecs) (targeting ECS 8.x), so logs integrate cleanly with Filebeat and the Elastic stack without an ingest pipeline to rename fields. The typical deployment writes ECS JSON to stdout or a file and lets Filebeat or Elastic Agent ship it to Elasticsearch: ~~~ruby SemanticLogger.add_appender(io: $stdout, formatter: :ecs) SemanticLogger.add_appender(file_name: "production.log", formatter: :ecs) ~~~ Semantic Logger fields map to ECS as follows: | Semantic Logger | ECS | | :--- | :--- | | `time` | `@timestamp` | | `level` | `log.level` | | `name` | `log.logger` | | `file` / `line` | `log.origin.file.name` / `log.origin.file.line` | | `message` | `message` | | `thread` | `process.thread.name` | | `pid` | `process.pid` | | `host` | `host.hostname` | | `application` | `service.name` | | `environment` | `service.environment` | | `exception` | `error.type` / `error.message` / `error.stack_trace` | | `duration` | `event.duration` (nanoseconds) | | `tags` | `tags` | | `named_tags` | `labels.*` | | `payload`, `metric`, `metric_amount` | nested under a custom namespace (see below) | ECS reserves the top-level field names it defines, so Semantic Logger data with no native ECS home (`payload`, `metric`, and `metric_amount`) is nested under a custom top-level namespace, `semantic_logger` by default. A proper-noun namespace is [the approach ECS recommends](https://www.elastic.co/docs/reference/ecs/ecs-custom-fields-in-ecs) for custom fields, since it never collides with a current or future ECS field. Rename it with `namespace:`: ~~~ruby formatter = SemanticLogger::Formatters::Ecs.new(namespace: "my_app") SemanticLogger.add_appender(io: $stdout, formatter: formatter) ~~~ Or set `namespace: nil` to merge the payload directly into ECS `labels` alongside the named tags: ~~~ruby formatter = SemanticLogger::Formatters::Ecs.new(namespace: nil) SemanticLogger.add_appender(io: $stdout, formatter: formatter) ~~~ ### Logfmt `formatter: :logfmt` emits each entry as a single line of `key=value` pairs in [logfmt](https://brandur.org/logfmt) format, common with Heroku and Grafana Loki tooling: ~~~ruby SemanticLogger.add_appender(io: $stdout, formatter: :logfmt) ~~~ ~~~ timestamp="2024-07-20T08:32:05.375276Z" level="info" name="MyClass" message="Hello World" tag="success" ~~~ Notes: * The timestamp is ISO 8601. All values are quoted and escaped, so a newline in the data cannot forge or split a record. * Named tags and payload fields are merged into the top-level `key=value` pairs. On a name conflict the payload wins. * Unnamed tags are emitted as keys with a `true` value. * `tag="success"` is emitted on every entry, becoming `tag="exception"` when the entry carries an exception (the exception class, message, and backtrace are then included as fields). ## Network protocols ### HTTP(S) The HTTP appender sends JSON to most services that accept log messages over HTTP or HTTPS: ~~~ruby SemanticLogger.add_appender(appender: :http, url: "http://localhost:8088/path") # For HTTPS, just change the scheme: SemanticLogger.add_appender(appender: :http, url: "https://localhost:8088/path") ~~~ The JSON being sent can be customized with a formatter: ~~~ruby formatter = Proc.new do |log, logger| h = log.to_h(logger.host, logger.application) # Change time from iso8601 to seconds since epoch h[:timestamp] = log.time.utc.to_f # Render to JSON h.to_json end SemanticLogger.add_appender(appender: :http, url: "https://localhost:8088/path", formatter: formatter) ~~~ #### Batching By default each entry is sent in its own HTTP request. To send multiple entries in one request as a JSON array, enable batching with `batch: true`. This suits endpoints that accept an array and create one document per element, such as the [Filebeat http_endpoint input](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-input-http_endpoint.html): ~~~ruby SemanticLogger.add_appender(appender: :http, url: "http://localhost:8088/path", batch: true) ~~~ With batching the appender runs on its own thread and flushes once `batch_size` entries have accumulated (default 300) or `batch_seconds` have elapsed (default 5), whichever comes first: ~~~ruby SemanticLogger.add_appender( appender: :http, url: "http://localhost:8088/path", batch: true, batch_size: 100, batch_seconds: 10 ) ~~~ ### TCP Appender (+SSL) The TCP appender sends JSON or other formatted messages to services that accept log messages over TCP, optionally with SSL: ~~~ruby # Plain TCP: SemanticLogger.add_appender(appender: :tcp, server: "localhost:8088") # TCP with SSL: SemanticLogger.add_appender(appender: :tcp, server: "localhost:8088", ssl: true) # With self-signed certificates, or to disable server certificate verification: SemanticLogger.add_appender( appender: :tcp, server: "localhost:8088", ssl: {verify_mode: OpenSSL::SSL::VERIFY_NONE} ) ~~~ Customize the message with a formatter: ~~~ruby formatter = Proc.new do |log, logger| h = log.to_h(logger.host, logger.application) h[:timestamp] = log.time.utc.to_f # seconds since epoch h.to_json end SemanticLogger.add_appender(appender: :tcp, server: "localhost:8088", formatter: formatter) ~~~ See [Net::TCPClient](https://github.com/reidmorrison/net_tcp_client) for the remaining connection options. The TCP and UDP appenders separate records with a newline, so they default to the JSON formatter, which escapes embedded newlines and is safe with untrusted data. If you switch to a text formatter such as `:default` or `:color`, enable `escape_control_chars` so a newline in the data cannot forge or split a record: ~~~ruby SemanticLogger.add_appender( appender: :tcp, server: "localhost:8088", formatter: {default: {escape_control_chars: true}} ) ~~~ ### UDP Appender The UDP appender sends JSON or other formatted messages over UDP: ~~~ruby SemanticLogger.add_appender(appender: :udp, server: "localhost:8088") ~~~ Customize the message with a formatter: ~~~ruby formatter = Proc.new do |log, logger| h = log.to_h(logger.host, logger.application) h[:timestamp] = log.time.utc.to_f # seconds since epoch h.to_json end SemanticLogger.add_appender(appender: :udp, server: "localhost:8088", formatter: formatter) ~~~ ### Syslog Log to a local Syslog daemon: ~~~ruby SemanticLogger.add_appender(appender: :syslog) ~~~ Log to a remote Syslog server over TCP (the `net_tcp_client` and `syslog_protocol` gems are required). The default packet size is 1024 bytes: ~~~ruby SemanticLogger.add_appender(appender: :syslog, url: "tcp://myloghost:514", max_size: 2048) ~~~ Or over UDP (the `syslog_protocol` gem is required): ~~~ruby SemanticLogger.add_appender(appender: :syslog, url: "udp://myloghost:514") ~~~ Add a filter to exclude noisy entries such as health checks: ~~~ruby SemanticLogger.add_appender( appender: :syslog, url: "udp://myloghost:514", filter: Proc.new { |log| log.message !~ /(health_check|Not logged in)/ } ) ~~~ Syslog frames each record, so embedded newlines or other control characters in untrusted data could forge or split records. The syslog formatters therefore escape control characters by default. To pass them through unchanged: ~~~ruby SemanticLogger.add_appender( appender: :syslog, url: "tcp://myloghost:514", formatter: {syslog: {escape_control_chars: false}} ) ~~~ Note: `:trace` level messages are mapped to `:debug`. ## Centralized logging and aggregators ### Elasticsearch Forward all log entries to Elasticsearch (requires the `elasticsearch` gem). By default entries are written to a daily index named `semantic_logger-YYYY.MM.DD`; override it with `index:`: ~~~ruby SemanticLogger.add_appender( appender: :elasticsearch, url: "http://localhost:9200", index: "my-index", data_stream: true ) ~~~ For an end-to-end walkthrough with Kibana, see [Centralized Logging](operations.html#centralized-logging). ### OpenSearch Forward all log entries to OpenSearch, for example AWS OpenSearch (requires the `opensearch-ruby` gem). OpenSearch is a fork of Elasticsearch and uses the same bulk indexing API, so this appender accepts the same options as [Elasticsearch](#elasticsearch). Use it instead of the Elasticsearch appender when talking to an OpenSearch server, since recent `elasticsearch` gems reject non-Elasticsearch servers with an `Elasticsearch::UnsupportedProductError`: ~~~ruby SemanticLogger.add_appender( appender: :opensearch, url: "http://localhost:9200", index: "my-index", data_stream: true ) ~~~ ### Graylog Send log entries to a [Graylog](https://www.graylog.org) server (requires the `gelf` gem). Data is sent as JSON, so all of the semantic structure is retained rather than being flattened into text. Over TCP: ~~~ruby SemanticLogger.add_appender(appender: :graylog, url: "tcp://localhost:12201") ~~~ Or over UDP: ~~~ruby SemanticLogger.add_appender(appender: :graylog, url: "udp://localhost:12201") ~~~ If not using Rails, the `facility` can be removed, or set to a custom string describing the application. Note: `:trace` level messages are mapped to `:debug`. ### Splunk HTTP To write to the Splunk HTTP Collector, follow the Splunk instructions to enable the [HTTP Event Collector](http://dev.splunk.com/view/event-collector/SP-CAAAE7F) and generate a token: ~~~ruby SemanticLogger.add_appender( appender: :splunk_http, url: "http://localhost:8088/services/collector/event", token: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ) ~~~ For HTTPS, change the URL scheme to `https`. Once entries have been sent, open the Splunk web interface, select Search, limit to the new host (`host=hostname`), switch to table view, and select the interesting columns: `host`, `duration`, `name`, `level`, `message`. **Performance:** against a local Splunk instance, the HTTP collector handled only about 30 entries per second. For much higher throughput, write to Splunk with the [TCP appender](#tcp-appender-ssl) instead, which reached about 1,400 entries per second (1,200 with SSL). ### Splunk SDK `appender: :splunk` submits entries through the official `splunk-sdk-ruby` gem to the Splunk management port (8089 by default). Authenticate with either a username and password, or a pre-authenticated `token:`: ~~~ruby SemanticLogger.add_appender( appender: :splunk, username: "username", password: "password", host: "localhost", port: 8089, scheme: :https, index: "main" ) ~~~ For most deployments the [HTTP Event Collector](#splunk-http) or the [TCP appender](#tcp-appender-ssl) is the simpler choice; use the SDK appender when the management API is the only ingress available. ### Grafana Loki Send log entries to [Grafana Loki](https://grafana.com/docs/loki) via its [HTTP push API](https://grafana.com/docs/loki/latest/reference/loki-http-api/#ingest-logs): ~~~ruby SemanticLogger.add_appender( appender: :loki, url: "https://logs-prod-001.grafana.net", username: "grafana_username", password: "grafana_token_here", compress: true ) ~~~ Set the URL, username, and password to match your Loki instance. Set `compress: true` to compress the log messages. ### CloudWatch Logs Forward all log entries to AWS CloudWatch Logs (requires the `aws-sdk-cloudwatchlogs` gem): ~~~ruby SemanticLogger.add_appender( appender: :cloudwatch_logs, client_kwargs: {region: "eu-west-1"}, group: "/my/application", create_stream: true ) ~~~ ### OpenTelemetry Send log entries to [OpenTelemetry](https://opentelemetry.io) through its [Logs API](https://opentelemetry.io/docs/specs/otel/logs/), so they can be exported to any OpenTelemetry-compatible backend (the OTLP collector, Honeycomb, Datadog, Grafana, and so on). This appender requires the `opentelemetry-logs-sdk` gem plus an exporter such as `opentelemetry-exporter-otlp-logs`: ~~~ruby gem "opentelemetry-logs-sdk" gem "opentelemetry-exporter-otlp-logs" ~~~ Configure the OpenTelemetry SDK once at startup, then add the appender. `OpenTelemetry::SDK.configure` reads the standard `OTEL_*` environment variables (for example `OTEL_EXPORTER_OTLP_ENDPOINT`) and installs a logger provider, which the appender picks up automatically: ~~~ruby require "opentelemetry-logs-sdk" require "opentelemetry-exporter-otlp-logs" OpenTelemetry::SDK.configure SemanticLogger.add_appender(appender: :open_telemetry) ~~~ Each entry is emitted with its level mapped to the matching OpenTelemetry severity number, the message as the record body, and the payload as record attributes. The appender registers a `SemanticLogger.on_log` subscriber that captures the current OpenTelemetry context as each entry is logged, so log records are correlated with the active trace and span. | Option | Description | |--------|-------------| | `name` | Instrumentation scope name reported to OpenTelemetry. Defaults to `"SemanticLogger"`. | | `version` | Instrumentation scope version. Defaults to the Semantic Logger gem version. | | `metrics` | Whether to forward metric-only log entries. Defaults to `true`. | ### Logstash Forward log entries to Logstash through the `logstash-logger` gem. Configure a `LogStashLogger` and hand it to Semantic Logger as a `logger:` appender: ~~~ruby require "logstash-logger" # See https://github.com/dwbutler/logstash-logger for further options log_stash = LogStashLogger.new(type: :tcp, host: "localhost", port: 5229) SemanticLogger.add_appender(logger: log_stash) ~~~ Note: `:trace` level messages are mapped to `:debug`. ### logentries.com Obtain a token by following the [logentries instructions](https://logentries.com/doc/input-token/), then prefix each JSON line with the token using a small custom formatter over the TCP appender: ~~~ruby module Logentries class Formatter < SemanticLogger::Formatters::Json attr_accessor :token def initialize(token) @token = token end def call(log, logger) "#{token} #{super(log, logger)}" end end end formatter = Logentries::Formatter.new("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") SemanticLogger.add_appender(appender: :tcp, server: "api.logentries.com:20000", ssl: true, formatter: formatter) ~~~ ### loggly.com After signing up with Loggly, obtain a token under `Source Setup` -> `Customer Tokens`, then post to the Loggly input URL with the HTTP appender: ~~~ruby SemanticLogger.add_appender( appender: :http, url: "https://logs-01.loggly.com/inputs/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/tag/semantic_logger/" ) ~~~ Once entries have been sent, open the Loggly web interface, select Search, and start with `tag:"semantic_logger"`. In the Field Explorer switch to Grid view and add the columns `host`, `duration`, `name`, `level`, and `message`. ### Papertrail Papertrail accepts log entries over TLS TCP in syslog format: ~~~ruby config.semantic_logger.add_appender( appender: :syslog, url: "tcp://something.papertrailapp.com:1234", tcp_client: { ssl: { ca_file: File.join(Rails.root, "config", "papertrail-bundle.pem") } } ) ~~~ See [Papertrail's documentation](http://help.papertrailapp.com/kb/configuration/encrypting-remote-syslog-with-tls-ssl/) for more. ## Error and exception monitoring > **Note:** These appenders forward the payload as supplied. Take care not to push sensitive > information in tags or a payload. ### Bugsnag Forward `:info`, `:warn`, or `:error` entries to Bugsnag (requires the `bugsnag` gem). Configure Bugsnag following the [Ruby Bugsnag documentation](https://bugsnag.com/docs/notifiers/ruby#sending-handled-exceptions), then add the appender, choosing the minimum level to forward: ~~~ruby # :error and above (the default) SemanticLogger.add_appender(appender: :bugsnag) # :warn and above SemanticLogger.add_appender(appender: :bugsnag, level: :warn) # :info and above SemanticLogger.add_appender(appender: :bugsnag, level: :info) ~~~ ### Sentry Use the `sentry-ruby` gem and the corresponding appender (the older `sentry-raven` gem works but is deprecated): ~~~ruby SemanticLogger.add_appender(appender: :sentry_ruby) ~~~ Some logging context is forwarded to Sentry: * From named tags, `transaction_name`. See . * From named tags or payload, `user` is built from the `user_id`, `username`, `user_email`, and `ip_address` keys, plus any `user` key that is itself a hash. See . * From the payload, `fingerprint` configures grouping granularity. See . * Named tags are sent as Sentry tags. See . * Unnamed tags are sent as the `:tag` tag, separated by commas. * Everything else from the payload and context is added to `extras`. ~~~ruby SemanticLogger.tagged(transaction_name: "foo", user_id: 42, baz: "quz") do logger.error("some message", username: "joe", fingerprint: ["bar"]) end ~~~ ### Honeybadger and Honeybadger Insights Forward errors to Honeybadger (requires the `honeybadger` gem): ~~~ruby SemanticLogger.add_appender(appender: :honeybadger) ~~~ Or forward all log entries to Honeybadger Insights as events: ~~~ruby SemanticLogger.add_appender(appender: :honeybadger_insights) ~~~ Both appenders use the Honeybadger [gem configuration](https://docs.honeybadger.io/lib/ruby/gem-reference/configuration/). ### NewRelic New Relic supports Error Events on both its paid and free plans. The appender sends `:error` and `:fatal` entries to New Relic as error events by default (requires the `newrelic_rpm` gem): ~~~ruby SemanticLogger.add_appender(appender: :new_relic) # To also send warnings: SemanticLogger.add_appender(appender: :new_relic, level: :warn) ~~~ ### Rollbar Rollbar needs its own current-thread context, so it cannot run as a regular appender unless logging is [synchronous](operations.html#synchronous-operation). Integrate it with an `on_log` subscriber instead (as recommended by @gingerlime): ~~~ruby SemanticLogger.on_log do |log| next unless log.try(:level) == :error err = RuntimeError.new(log.try(:message)) err.set_backtrace(log.backtrace) if log.backtrace Rollbar.error(err, :log_extra => log.to_h) end ~~~ ## Databases and message queues ### MongoDB Write log entries as documents into a MongoDB capped collection (requires the `mongo` gem): ~~~ruby appender = SemanticLogger::Appender::MongoDB.new( uri: "mongodb://127.0.0.1:27017/test", collection_size: 1024**3, # 1 gigabyte application: Rails.application.class.name ) SemanticLogger.add_appender(appender: appender) ~~~ Each entry is stored as a document: ~~~javascript > db.semantic_logger.findOne() { "_id" : ObjectId("53a8d5b99b9eb4f282000001"), "time" : ISODate("2014-06-24T01:34:49.489Z"), "host_name" : "appserver1", "pid" : null, "thread_name" : "2160245740", "name" : "Example", "level" : "info", "level_index" : 2, "application" : "my_application", "message" : "This message is written to mongo as a document" } ~~~ ### Apache Kafka Publish log entries to an Apache Kafka broker (requires the `ruby-kafka` gem): ~~~ruby SemanticLogger.add_appender( appender: :kafka, seed_brokers: ["kafka1:9092", "kafka2:9092"] ) ~~~ ### RabbitMQ (AMQP) Stream log entries through a queue on a RabbitMQ broker (requires the `bunny` gem): ~~~ruby SemanticLogger.add_appender( appender: :rabbitmq, queue_name: "semantic_logger", rabbitmq_host: "localhost", username: "the-username", password: "the-password" ) ~~~ ## Logging to several destinations at once Add as many appenders as you like; every entry is written to all of them. Use a per-appender `level` so each destination keeps a different subset. ~~~ruby require "semantic_logger" SemanticLogger.default_level = :info # Everything at :warn and above to one file: SemanticLogger.add_appender(file_name: "log/warnings.log", level: :warn) # Everything at :trace and above to another: SemanticLogger.add_appender(file_name: "log/trace.log", level: :trace) logger = SemanticLogger["MyClass"] logger.level = :trace logger.trace "This is a trace message" logger.info "This is an info message" logger.warn "This is a warning message" ~~~ Each file receives only the entries at or above its level: ~~~ ==> trace.log <== 2013-08-02 14:15:56.733532 T [35669:70176909690580] MyClass -- This is a trace message 2013-08-02 14:15:56.734273 I [35669:70176909690580] MyClass -- This is an info message 2013-08-02 14:15:56.735273 W [35669:70176909690580] MyClass -- This is a warning message ==> warnings.log <== 2013-08-02 14:15:56.735273 W [35669:70176909690580] MyClass -- This is a warning message ~~~ A common combination is colorized text to a local file plus a remote aggregator: ~~~ruby SemanticLogger.add_appender(file_name: "development.log", formatter: :color) SemanticLogger.add_appender(appender: :syslog, url: "tcp://myloghost:514") ~~~ ## Standalone appenders An appender can be created and logged to directly, using the same API as any logger. This is useful to send specific activity on the current thread to a separate destination, without writing it to the appenders registered with Semantic Logger: ~~~ruby require "semantic_logger" appender = SemanticLogger::Appender::File.new("separate.log", level: :info, formatter: :color) appender.warn "Only send this to separate.log" appender.measure_info "Called supplier" do # Call supplier ... end ~~~ Note: once an appender has been registered with Semantic Logger, do not also call it directly. Non-deterministic concurrency issues arise when it is used across threads. --- ## Log Event Every call to a logger (`logger.info`, `logger.measure_error`, and so on) builds a single **Log event** object. That object is the unit of work that flows through the whole pipeline: it is handed to each [filter](config.html#filtering), then to each appender's [formatter](config.html#custom-formatters), and finally written out by the [appender](appenders.html) itself, usually on a background thread so the calling application is not blocked. You meet the Log event whenever you customize Semantic Logger: * A **filter** Proc receives it and returns `true` to keep the entry. See [Filtering](config.html#filtering). * A **formatter** receives it and turns it into the final output (text, JSON, ...). See [Custom formatters](config.html#custom-formatters). * A custom **appender**'s `#log(log)` method receives it and writes it to a destination. In every one of those places you are reading (and may modify) the same object documented here. ### The event is mutable The Log event is a plain mutable Ruby object: every attribute below has both a reader and a writer. Filters and formatters are allowed to change it before it is written, for example to redact a message or enrich the payload: ```ruby filter: ->(log) do log.message = log.message.gsub(/\d{16}/, "[REDACTED]") true # keep the (now edited) entry end ``` Because all appenders share one event instance per log call, keep any mutation deliberate: a change made in a filter is visible to every appender that runs afterwards. ### Which fields are always present A handful of attributes are populated for **every** event: * `level`, `level_index`, `name`, `time`, `thread_name`, `tags`, `named_tags`. The rest are **situational**: they are set only when relevant to that particular call. For example `duration` is present only for `measure_*` calls, `exception` only when an exception was logged, `metric`/`metric_amount`/`dimensions` only for metrics, and `backtrace` only when backtrace capture is enabled (`SemanticLogger.backtrace_level`). Always guard against `nil` when reading a situational field. ### Attributes |Attribute|Type|Description| |---------|----|-----------| |`level`|`Symbol`|Log level of the call: `:trace`, `:debug`, `:info`, `:warn`, `:error`, `:fatal`.| |`level_index`|`Integer`|Numeric form of the level for fast comparisons: `:trace=>0`, `:debug=>1`, `:info=>2`, `:warn=>3`, `:error=>4`, `:fatal=>5`.| |`name`|`String`|Class or component name supplied to the logger, for example the `"MyClass"` in `SemanticLogger["MyClass"]`.| |`message`|`String`|The text message. May be `nil` (for example, a metric-only event).| |`payload`|`Hash`|Structured data logged alongside the message. `nil` when none was supplied.| |`time`|`Time`|The moment the event was created.| |`thread_name`|`String`|Name (or id) of the thread that made the log call.| |`tags`|`Array`|Tags active on the thread when the call was made.| |`named_tags`|`Hash`|Named tags active on the thread when the call was made.| |`context`|`Hash`|Named contexts captured in-line at the point the event was created. `nil` when none.| |`duration`|`Float`|Time in milliseconds taken by a `measure_*` call. `nil` for ordinary log calls.| |`exception`|`Exception`|The Ruby exception that was logged. `nil` when none. Use `each_exception` to walk a `cause` chain.| |`backtrace`|`Array`|Backtrace captured at the call site, present only when the level is at or above `SemanticLogger.backtrace_level`.| |`metric`|`String`|The metric name, present for metric and `measure_*` calls. See [Metrics](metrics.html).| |`metric_amount`|`Float`|Numeric amount for a counter or gauge metric, for example the quantity purchased.| |`dimensions`|`Hash`|Dimensions (key/value labels) supplied for a metric.| ### Helper methods These read-only helpers derive convenient values from the attributes above: |Method|Returns|Description| |------|-------|-----------| |`payload?`|`true`/`false`|Whether the event carries a non-empty payload.| |`payload_to_s`|`String` or `nil`|The payload rendered with `inspect`, or `nil` when absent.| |`metric_only?`|`true`/`false`|`true` when the event has a metric but no message and no exception. Text appenders typically skip these; machine-readable (JSON) appenders usually keep them.| |`duration_to_s`|`String` or `nil`|The duration in milliseconds as a short string, for example `"12ms"`. `nil` when there is no duration.| |`duration_human`|`String` or `nil`|The duration in human readable form, scaling up to seconds, minutes, hours, or days as needed.| |`level_to_s`|`String`|The level as a single upper-case character, for example `"I"` for `:info`.| |`cleansed_message`|`String`|The message with Rails/ANSI color escape codes stripped. Used to keep terminal codes out of structured output.| |`each_exception`|Enumerator|Iterates the exception and its nested `cause` chain, yielding `(exception, depth)`.| |`backtrace_to_s`|`String`|The exception backtrace as a string, including every exception in the `cause` chain.| |`file_name_and_line(short_name = false)`|`[String, Integer]` or `nil`|The file name and line number from the event's backtrace or exception. Pass `true` for just the base file name.| |`to_h`|`Hash`|The event as a plain Hash (via the Raw formatter), including `host`, `application`, and `environment`. Handy for inspecting an event or building a custom formatter.| |`set_context(key, value)`|`Hash`|Lazily initializes `context` and stores a key/value pair on it.| ### Inspecting an event When writing a filter or formatter, the quickest way to see what a real event contains is to dump it with `to_h`: ```ruby SemanticLogger.add_appender( io: $stdout, filter: ->(log) do pp log.to_h # one-off: inspect the event during development true end ) ``` --- ## Metrics A **metric** is a named number that Semantic Logger emits alongside a log entry, so the same call that records *what happened* can also feed your dashboards and alerts. Use metrics to track things like: * How often an event happens (an error, a sign-up, a cache miss). * How long something takes (an external API call, a database query). * Running totals, such as the amount purchased per department over time. There are two parts to using metrics: 1. **Emit** a metric by adding the `:metric` option to any log or `measure_` call. This is covered in Steps 1 to 3 below. 2. **Subscribe** a destination (Statsd, New Relic, SignalFx, and so on) that receives those metrics. Until a subscriber is registered, the `:metric` option is harmless and the number goes nowhere. See [Send metrics to a backend](#send-metrics-to-a-backend). Metric subscribers are notified asynchronously on the background log thread, so emitting a metric never slows the calling thread. A metric rides along with its log entry, so it obeys the same level and [filtering](config.html#filtering) rules: a metric on a `:trace` call is not emitted while the level is `:info`. > **Not to be confused with operational stats.** The metrics on this page are *application* metrics > that your code emits about what it is doing. To monitor Semantic Logger's *own* health instead, > such as queue sizes and the number of log entries processed or dropped, use > [`SemanticLogger.stats`](operations.html#monitoring-the-background-thread). ## Step 1: Count something The simplest metric counts how often something happens. Add `:metric` with a name to any log call, and the metric is incremented by 1: ~~~ruby logger.info(message: "Signed up", metric: "users/signup") ~~~ Supply `:metric_amount` to change the amount. To decrement: ~~~ruby logger.info(message: "Item removed", metric: "cart/items", metric_amount: -1) ~~~ To add to a running total, such as the dollar amount of purchases per department: ~~~ruby logger.info(message: "Purchase complete", metric: "departments/clothing", metric_amount: 189.42) ~~~ Note: Statsd counters are integers, so float amounts are rounded to the nearest integer. ## Step 2: Measure how long something takes A duration metric records elapsed time. The easiest way is to add `:metric` to a `measure_` block (see [Measure how long something takes](api.html#step-6-measure-how-long-something-takes) in the Guide). The measured duration becomes the metric: ~~~ruby logger.measure_info("Called supplier", metric: "supplier/add_user") do # Code to call the external service ... end ~~~ When you already have the duration, emit it on a plain log call by supplying both `:metric` and `:duration` (in milliseconds): ~~~ruby logger.info(message: "Called supplier", metric: "supplier/add_user", duration: 100.23) ~~~ ## Step 3: Break a metric down with dimensions Dimensions are key/value labels attached to a metric, so a backend that supports them (such as SignalFx) can slice it, for example by user, action, or state. Add them with the `:dimensions` option: ~~~ruby # A counter, broken down by user: logger.info(metric: "filters.count", dimensions: {user: "jbloggs"}) # A gauge with an amount and dimensions: logger.info(metric: "filters.average", metric_amount: 1.2, dimensions: {user: "jbloggs"}) ~~~ Not every backend supports dimensions. **Statsd** and **New Relic** ignore any metric that carries dimensions; **SignalFx** is built around them. See each subscriber below. ## Send metrics to a backend A metric goes nowhere until a **metric subscriber** is registered. Add one with `SemanticLogger.add_appender(metric: ...)`, usually when your application starts. A subscriber receives every logged entry that has a `:metric`, asynchronously on the background thread. ### Statsd Send metrics to [Statsd](https://github.com/statsd/statsd) over UDP, which can roll them up and forward them to [Graphite](https://graphiteapp.org), MongoDB, and others (requires the `statsd-ruby` gem): ~~~ruby SemanticLogger.add_appender(metric: :statsd, url: "udp://localhost:8125") ~~~ Counters are integers (float amounts are rounded). Does not support dimensions. ### New Relic Forward metrics to New Relic so they can be displayed on custom dashboards (requires the `newrelic_rpm` gem): ~~~ruby SemanticLogger.add_appender(metric: :new_relic) ~~~ Does not support dimensions. ### SignalFx Forward metrics to [SignalFx](https://www.splunk.com/en_us/products/infrastructure-monitoring.html), which is built around dimensions: ~~~ruby SemanticLogger.add_appender(metric: :signalfx, token: "SIGNALFX_ORG_ACCESS_TOKEN") ~~~ `application` and `host` are always sent as dimensions. To also forward specific named tags as dimensions whenever they are present on a log entry, list them: ~~~ruby SemanticLogger.add_appender( metric: :signalfx, token: "SIGNALFX_ORG_ACCESS_TOKEN", dimensions: [:user_id, :state] ) ~~~ When a duration metric has no dimensions, SignalFx receives both a gauge and a counter, so you can chart both the timing and the number of occurrences. When dimensions are present, the metric is sent as-is. ### Elasticsearch and Splunk These are ordinary log [appenders](appenders.html), not metric subscribers, but a metric is part of the log entry, so `metric`, `metric_amount`, and any dimensions are written into the document automatically. You can build dashboards on those fields directly, without registering a separate metric subscriber. ## How metrics behave **Asynchronous.** Subscribers run on the background logging thread, so emitting a metric never slows down the thread that logged it. **Follows the log level.** A metric is emitted only when its log entry is actually logged, so it obeys the same log level and filtering as any other entry. Use this to your advantage: leave detailed `:trace` level metrics in the code where they stay dormant under an `:info` level, then turn them on when needed, for example by sending the `SIGUSR2` [signal](operations.html#linux-signals) to lower the log level on a running process. --- ## Rails [rails_semantic_logger](https://github.com/reidmorrison/rails_semantic_logger) is a companion gem that wires Semantic Logger into Rails for you. Once installed it: * Replaces the default Rails logger with Semantic Logger, so Rails, your application code, and many common gems all log through it. * Collapses the several lines Rails normally logs per request into a single, structured "Completed" line, while keeping the individual fields (controller, action, status, durations, and so on) searchable. * Lets you send logs anywhere Semantic Logger supports: the standard Rails log file, standard out as JSON for a container platform, a centralized log service, or several of these at once. This page is a step-by-step guide that assumes no prior knowledge of either gem. Work through it top to bottom: each section builds on the previous one. For the underlying logging API (logging methods, tags, metrics, and so on) see the [Programmer's Guide](api.html), and for the full catalog of log destinations see [Appenders](appenders.html). > **Upgrading from v4?** The way appenders (log destinations) are configured changed in v5. Jump to > [Migrating from v4 to v5](#migrating-from-v4-to-v5), then come back here. ### Requirements Rails Semantic Logger v5 requires **Ruby 3.2 or later** and **Rails 7.2 or later**. For the exact list of tested Ruby and Rails versions, see the [CI workflow](https://github.com/reidmorrison/rails_semantic_logger/blob/main/.github/workflows/ci.yml). ### Installation Add the following lines to your `Gemfile`: ~~~ruby gem "rails_semantic_logger" gem "amazing_print" # optional ~~~ `amazing_print` is optional but recommended: it produces colorized, readable output of the structured data (the Hash payload) in development. Install with bundler: bundle install That is all that is required. Rails Semantic Logger automatically replaces the standard Rails logger with Semantic Logger and writes to the usual Rails log file. #### Remove conflicting gems Remove the following gems if present. They conflict with or duplicate what Rails Semantic Logger already does: * `lograge` * `rails_stdout_logging` * `rails_12factor` ### Out of the box With no configuration at all, Rails Semantic Logger: * Writes to `log/.log` (for example `log/development.log`), the same file Rails uses. * Colorizes that output when Rails colorized logging is enabled (the default in development). * Logs to **standard out** when you run `rails server`, so you see requests in your terminal. * Logs to **standard error** when you run `rails console`, so log lines do not get mixed up with the return values of the commands you type. * Replaces the multi-line Rails request log with a single structured "Completed" line. Standard Rails log output for a single page request: The same request after adding the `rails_semantic_logger` gem: The rest of this page shows how to change **where** logs go and **how** they are formatted (the appenders block), and then how to fine-tune **what** Rails logs. Configuration goes in `config/application.rb` (for all environments) or in an environment file under `config/environments/` (for one environment). Use those two places, not `config/initializers/*`. Rails builds the logger **before** it loads initializers, so the appenders block and the `semantic`, `replace_sidekiq_logger`, and `replace_solid_queue_logger` options have no effect from an initializer; Rails Semantic Logger prints a warning when it detects this. The output-tuning options consumed later in boot (`started`, `processing`, `rendered`, `quiet_assets`, `action_message_format`) do still work from an initializer, but not once the application has finished booting. --- ## Configuring where logs go: the appenders block An **appender** is a destination for log output: a file, standard out, a centralized log service, and so on. You declare the appenders you want inside a single block: ~~~ruby config.rails_semantic_logger.appenders do |appenders| appenders.add(file_name: "log/#{Rails.env}.log", formatter: :color) end ~~~ There are three ways to declare an appender. **The method name says _when_ the appender is created; the arguments say _where_ it writes and _how_ it is formatted.** | Method | Created when… | Default destination | |--------|---------------|---------------------| | `add` | Always, during Rails initialization | (you must specify one) | | `add_server` | Only when serving requests: `rails server` (see the note under [Step 3](#step-3-log-to-the-screen-only-while-serving)) and Sidekiq in server mode | `$stdout` | | `add_console` | Only inside a `rails console` session | `$stderr` | The arguments to all three are exactly the arguments to `SemanticLogger.add_appender` (covered in detail in [the next section](#appender-options-and-destinations)), so anything Semantic Logger can log to, any of these can declare. One default of note: since `add_server` and `add_console` are screen appenders, their `formatter:` defaults to `:color` when not specified; `add` uses the Semantic Logger default of plain text. > **Important:** As soon as you declare **any** appender in this block, Rails Semantic Logger stops > adding **all** of its automatic appenders: the default `log/.log` file, the standard-out > logger it normally adds under `rails server`, and the standard-error logger it normally adds in > `rails console`. The block becomes the single source of truth for every destination. So declare > what you want: `add` for an always-on destination (such as the file log), `add_server` for screen > output while serving, and `add_console` for the Rails console. ### Step 1: a single log file ~~~ruby config.rails_semantic_logger.appenders do |appenders| appenders.add(file_name: "log/#{Rails.env}.log", formatter: :color) end ~~~ ### Step 2: add a JSON file for a log aggregator Keep the human-readable color log and *also* write a JSON file for ingestion by Elasticsearch, Splunk, Datadog, and the like: ~~~ruby config.rails_semantic_logger.appenders do |appenders| appenders.add(file_name: "log/#{Rails.env}.log", formatter: :color) appenders.add(file_name: "log/#{Rails.env}.json", formatter: :json) end ~~~ You can declare as many appenders as you like; every log entry is sent to all of them. ### Step 3: log to the screen only while serving `add_server` declares an appender that is created **only** when the application is actually serving requests (under `rails server` or Sidekiq in server mode), and never during rake tasks, runners, or generators. It defaults to `$stdout`: ~~~ruby config.rails_semantic_logger.appenders do |appenders| appenders.add(file_name: "log/#{Rails.env}.log", formatter: :json) appenders.add_server(formatter: :color) # → $stdout, only when serving end ~~~ > **Note:** Under `rails server`, the appender is created when Rails itself would log to standard > out: in development, when not daemonized. To get it in another environment, pass > `--log-to-stdout` to `rails server`. App servers started directly (bare `puma`, `rackup`, and so > on) need a one-line boot hook; see > [Other app servers](#other-app-servers-puma-rackup-passenger-unicorn). ### Step 4: a dedicated console logger `add_console` declares an appender created **only** inside a `rails console` session. It defaults to `$stderr` so log output does not interleave with the results of the expressions you type: ~~~ruby config.rails_semantic_logger.appenders do |appenders| appenders.add(file_name: "log/#{Rails.env}.log", formatter: :json) appenders.add_server(formatter: :color) # $stdout while serving appenders.add_console(formatter: :color) # $stderr inside `rails console` end ~~~ ### Several appenders in one context Because each call simply appends to its context, a context can have more than one appender. For example, write a color stream *and* a JSON file, but only while serving: ~~~ruby config.rails_semantic_logger.appenders do |appenders| appenders.add_server(io: $stdout, formatter: :color) appenders.add_server(file_name: "log/#{Rails.env}.log", formatter: :json) end ~~~ --- ## Appender options and destinations Every `add`, `add_server`, and `add_console` call accepts the same arguments as `SemanticLogger.add_appender`. This section is the one-stop reference for those arguments, written as Rails examples. For the complete list of destinations and their service-specific options, see [Appenders](appenders.html). ### Common options In addition to a destination, most appenders accept these options: | Option | Description | |--------|-------------| | `level` | Only write entries at this level or higher to this appender. Defaults to `SemanticLogger.default_level` (which Rails Semantic Logger sets from `config.log_level`). | | `formatter` | How to format the output: `:default`, `:color`, `:json`, `:logfmt`, `:one_line`, or a custom formatter (see [Output formats](#output-formats)). | | `filter` | A `Regexp` or `Proc` selecting which entries this appender accepts. See [Filtering](config.html#filtering). | | `application`, `environment`, `host` | Override the global values for this appender only. | For example, send only warnings and above to a separate JSON file: ~~~ruby config.rails_semantic_logger.appenders do |appenders| appenders.add(file_name: "log/#{Rails.env}.log", formatter: :color) appenders.add(file_name: "log/#{Rails.env}_errors.json", formatter: :json, level: :warn) end ~~~ ### Destinations The destination is chosen by the argument you pass: | Destination | Argument | Notes | |-------------|----------|-------| | Text or JSON file | `file_name:` | A path under `log/`. | | IO stream | `io:` | `$stdout`, `$stderr`, or any `IO`. | | Built-in appender | `appender: :name` | Selects a packaged appender by name (syslog, elasticsearch, http, bugsnag, and many more). | | Existing Ruby/Rails logger | `logger:` | Wrap another logger instance. | | Metrics destination | `metric:` | See [Metrics](metrics.html). | A few common examples, all inside the appenders block: ~~~ruby config.rails_semantic_logger.appenders do |appenders| # Local file appenders.add(file_name: "log/#{Rails.env}.log", formatter: :color) # Local Syslog appenders.add(appender: :syslog) # Remote syslog such as syslog-ng over TCP appenders.add(appender: :syslog, url: "tcp://myloghost:514") # Elasticsearch appenders.add(appender: :elasticsearch, url: "http://localhost:9200") # A generic HTTP(S) endpoint appenders.add(appender: :http, url: "https://logs.example.com/ingest") # Bugsnag (errors and above) appenders.add(appender: :bugsnag, level: :error) end ~~~ Appenders for third-party services require their backing gem to be installed. See [Appenders](appenders.html) for the full list of destinations, their gems, and their options. ### Output formats The `formatter:` option controls how each appender renders a log entry. Because it is per appender, you can write color to the screen and JSON to a file at the same time. | Formatter | Output | |-----------|--------| | `:default` | Plain text, no color. | | `:color` | Plain text with color (uses Amazing Print for the payload when installed). | | `:json` | One JSON object per entry. | | `:logfmt` | `key=value` logfmt output. | | `:one_line` | Each entry reduced to a single line. | | A class instance | Any instance of a class derived from `SemanticLogger::Formatters::Base`. | | A `Proc` | Called with the `log` entry; returns the formatted output. | JSON example: ~~~ruby config.rails_semantic_logger.appenders do |appenders| appenders.add(file_name: "log/#{Rails.env}.json", formatter: :json) end ~~~ Custom formatter. Create `app/lib/my_formatter.rb`: ~~~ruby # A custom colorized formatter class MyFormatter < SemanticLogger::Formatters::Color # Return the complete log level name in uppercase def level "#{color}#{log.level.upcase}#{color_map.clear}" end end ~~~ Then use it: ~~~ruby config.rails_semantic_logger.appenders do |appenders| appenders.add(file_name: "log/#{Rails.env}.log", formatter: MyFormatter.new) end ~~~ See [SemanticLogger::Formatters::Color](https://github.com/reidmorrison/semantic_logger/blob/main/lib/semantic_logger/formatters/color.rb) for the methods you can override, and [Custom formatters](config.html#custom-formatters) for more on formatters. #### Amazing Print options for the color formatter The color formatter renders the payload Hash with Amazing Print. To pass options to it, give the `:color` formatter a Hash: ~~~ruby config.rails_semantic_logger.appenders do |appenders| appenders.add( file_name: "log/#{Rails.env}.log", formatter: {color: {ap: {multiline: false}}} ) end ~~~ See the [Amazing Print documentation](https://github.com/amazing-print/amazing_print) for the available options (or set defaults in a `~/.aprc` file). This has no effect if Amazing Print is not installed. --- ## Common recipes ### Development Color to both the log file and the screen: ~~~ruby # config/environments/development.rb config.rails_semantic_logger.appenders do |appenders| appenders.add(file_name: "log/development.log", formatter: :color) end ~~~ (The default already does this, so in development you often need no configuration at all.) ### Production on a container platform (Docker, Kubernetes, Heroku) On a container platform the convention is to log JSON to standard out and let the platform collect it. Use `add` (not `add_server`) so that rake tasks and one-off processes also log to stdout: ~~~ruby # config/environments/production.rb config.rails_semantic_logger.appenders do |appenders| appenders.add(io: $stdout, formatter: :json) end ~~~ Because the block disables the automatic appenders, this JSON-to-stdout appender is the *only* destination: there is no default file log and no separate color logger under `rails server`, which is exactly what a container platform wants. To keep one configuration that works both locally and in-cluster, gate the JSON appender on an environment variable that is only set in the container platform. For example, in `config/application.rb` (so it applies to every environment), switch to JSON on standard out only when running inside Kubernetes: ~~~ruby # config/application.rb config.semantic_logger.application = "my_application" config.semantic_logger.environment = ENV["STACK_NAME"] || Rails.env config.log_level = ENV["LOG_LEVEL"] || :info if ENV["LOG_TO_CONSOLE"] || ENV["KUBERNETES_SERVICE_HOST"] config.rails_semantic_logger.appenders do |appenders| appenders.add(io: $stdout, formatter: :json) end end ~~~ Outside the cluster the block is skipped, so the default `log/.log` file and the usual screen loggers apply; inside the cluster JSON to stdout becomes the only destination. On Heroku, also allow the log level to be set from the environment: ~~~ruby config.log_level = ENV["LOG_LEVEL"].presence&.downcase&.to_sym || :info ~~~ `heroku config:set LOG_LEVEL=debug` ### Production writing to files plus an error service ~~~ruby # config/environments/production.rb config.rails_semantic_logger.appenders do |appenders| appenders.add(file_name: "log/production.json", formatter: :json) appenders.add(appender: :bugsnag, level: :error) end ~~~ ### Other app servers: puma, rackup, Passenger, Unicorn `add_server` appenders are created automatically under `rails server` (following Rails' own log-to-stdout rule: development and not daemonized, or the `--log-to-stdout` flag) and under Sidekiq in server mode, because those have a definitive startup hook. App servers started **directly** (bare `puma`, `rackup`, Passenger, Unicorn) have no such first-party hook, and Rails Semantic Logger deliberately does **not** guess (a detection that only sometimes works is worse than none). If you start your app with one of those servers and want your `add_server` appenders created, call the helper from that server's own boot hook. For example, in `config/puma.rb`: ~~~ruby on_booted { RailsSemanticLogger.add_server_appenders } ~~~ Alternatively, if you simply want a destination created in every context, declare it with `add` instead of `add_server`. ### Sidekiq Sidekiq in server mode is treated as a serving context, so `add_server` appenders are created automatically. No extra configuration is required. This wiring is part of the Sidekiq logger integration, so setting `config.rails_semantic_logger.replace_sidekiq_logger = false` also turns off the automatic creation. In that case, call `RailsSemanticLogger.add_server_appenders` yourself from a Sidekiq startup hook: ~~~ruby # config/initializers/sidekiq.rb Sidekiq.configure_server do |config| config.on(:startup) { RailsSemanticLogger.add_server_appenders } end ~~~ --- ## Tuning what Rails logs The options below adjust Rails' own log output. They are independent of the appenders block and can be combined with it. ### Log level ~~~ruby # One of :trace, :debug, :info, :warn, :error, :fatal config.log_level = :debug ~~~ To change the level inside a running `rails console`: ~~~ruby SemanticLogger.default_level = :debug ~~~ ### Re-enable Started, Processing, and Rendered messages By default these messages are logged at `:debug` so they do not appear in production. To show them: ~~~ruby config.rails_semantic_logger.started = true # Rack "Started" line config.rails_semantic_logger.processing = true # Controller "Processing" line config.rails_semantic_logger.rendered = true # Action View render lines ~~~ ### Keep Rails' original wording By default Action Controller and Active Record messages are converted to structured data: ~~~ Rack -- Started -- { :ip => "127.0.0.1", :method => "GET", :path => "/users" } UserController -- Completed #index -- { :action => "index", :db_runtime => 54.64, :format => "HTML", :method => "GET", :path => "/users", :status => 200, :status_message => "OK", :view_runtime => 709.88 } ~~~ To keep Rails' original text messages (with Semantic Logger formatting) instead: ~~~ruby config.rails_semantic_logger.semantic = false config.rails_semantic_logger.started = true config.rails_semantic_logger.processing = true config.rails_semantic_logger.rendered = true ~~~ ### Quiet asset logging Rails logs asset requests at the debug level, which can clutter development logs: ~~~ Rack -- Started -- {:ip => "127.0.0.1", :method => "GET", :path => "/assets/application.css"} ~~~ To silence them: ~~~ruby config.rails_semantic_logger.quiet_assets = true ~~~ ### Color output Color is chosen per appender with the `formatter:` option: use `:color` for colorized output and `:default` for plain text (see [Output formats](#output-formats)). For example, color on screen and plain text in a file: ~~~ruby config.rails_semantic_logger.appenders do |appenders| appenders.add(file_name: "log/#{Rails.env}.log", formatter: :default) appenders.add_server(formatter: :color) end ~~~ The Rails `config.colorize_logging` setting does **not** affect appenders declared in the block; it only influences the deprecated default file appender (see [Deprecated configuration options](#deprecated-configuration-options)). ### Named tags Add tags to every log entry on a per-request basis by setting `config.log_tags` to a Hash: ~~~ruby config.log_tags = { request_id: :request_id, ip: :remote_ip, user: ->(request) { request.cookie_jar["login"] } } ~~~ Notes: * If a value returns `nil`, that key is omitted for that request. * To turn named tags off in development, set `config.log_tags = nil` in `config/environments/development.rb`. ### Source file name and line number To include the file and line number where each message originated: ~~~ruby config.semantic_logger.backtrace_level = :info ~~~ **Warning:** capturing a backtrace for every log entry allocates many objects. In production set this to `nil` (disabled) or to a high level such as `:error`. By default backtraces are only captured for `:error` and `:fatal`. This feature is best used in development. ### Add custom data to the Completed message Add an `append_info_to_payload` method to a controller to include extra fields in its Completed message: ~~~ruby class ThingController < ApplicationController private def append_info_to_payload(payload) super payload[:user_id] = current_user&.id end end ~~~ ### Customize the Completed message text Provide a Proc to build the Action Controller message from the message and payload: ~~~ruby config.rails_semantic_logger.action_message_format = ->(message, payload) do "#{message} - #{payload[:controller]}##{payload[:action]}" end ~~~ ### Background job loggers By default Rails Semantic Logger replaces the Sidekiq and SolidQueue loggers. To leave them alone: ~~~ruby config.rails_semantic_logger.replace_sidekiq_logger = false config.rails_semantic_logger.replace_solid_queue_logger = false ~~~ Sidekiq v7 and v8 are supported. Earlier Sidekiq versions were supported up to Rails Semantic Logger v5.0. #### Sidekiq job lifecycle messages For every Sidekiq job, Rails Semantic Logger emits a `Start #perform` and a `Completed #perform` entry (with the `sidekiq.queue.latency` and `sidekiq.job.perform` metrics). On very high job volumes these can add noise and cost in log aggregation tools. To turn them off (the job still runs and any exceptions are still logged): ~~~ruby # config/initializers/sidekiq.rb RailsSemanticLogger::Sidekiq::JobLogger.perform_messages = false ~~~ This defaults to `true`, so the messages are emitted unless you opt out. On Sidekiq 8, the standard Sidekiq setting has the same effect and is also honored: ~~~ruby Sidekiq.configure_server do |config| config[:skip_default_job_logging] = true end ~~~ #### Sidekiq job logging context Every log entry emitted while a job runs is tagged with the job's `jid`, class, and queue, plus its `bid` (batch id) and `tags` when present. On Sidekiq 8, the standard `logged_job_attributes` setting is honored, so additional job attributes can be added to the logging context: ~~~ruby Sidekiq.configure_server do |config| config[:logged_job_attributes] = %w[bid tags priority] end ~~~ ### Custom controller base class If your application uses a controller base class other than `ActionController::Base` or `ActionController::API`, Rails Semantic Logger falls back to the `ActionController::Base` logger, so those entries are named `ActionController::Base`. To give them the correct class name, include the mixin in your base class: ~~~ruby include SemanticLogger::Loggable ~~~ If the base class lives in a third-party gem, do it from an initializer: ~~~ruby CustomControllerBase.include(SemanticLogger::Loggable) ~~~ --- ## Metrics (prototype) > **Prototype:** metrics support and the metric names below are an early prototype and **subject to > change** in a future release. Do not hard-code these names into long-lived dashboards or alerts yet. In addition to the structured log entry, Rails Semantic Logger attaches a Semantic Logger [metric](metrics.html) to each entry that is logged at `:info`, `:warn`, or `:error`. When the entry carries a duration (a request, query, render, job run, ...) the metric records that timing; otherwise it acts as an event counter. Wire up a metrics appender (StatsD, Prometheus, ...) and these flow through automatically alongside the logs. Metric names follow `rails..`, where `` is the Rails component with its `action_`/`active_` prefix dropped: | Component | Metrics | | --- | --- | | Action Controller | `rails.controller.process_action`, `rails.controller.send_file`, `rails.controller.send_data`, `rails.controller.redirect_to`, `rails.controller.halted_callback`, `rails.controller.rescue_from_callback`, and the fragment-cache events (`rails.controller.write_fragment`, `read_fragment`, `exist_fragment`, `expire_fragment`, `expire_page`, `write_page`) | | Action View | `rails.view.render.template`, `rails.view.render.partial`, `rails.view.render.layout`, `rails.view.render.collection` | | Active Job | `rails.job.` for every job event (`enqueue`, `enqueue_at`, `enqueue_all`, `perform_start`, `perform`, `enqueue_retry`, `retry_stopped`, `discard`, and the Rails 8.1 Continuation events `interrupt`, `resume`, `step_skipped`, `step_started`, `step`) | | Action Mailer | `rails.mailer.deliver` | | Solid Queue | `rails.solid_queue.` for every Solid Queue event logged at info/warn/error (e.g. `rails.solid_queue.start_process`, `rails.solid_queue.thread_error`) | Entries logged at `:debug` carry **no** metric. Because ActiveRecord logs SQL at `:debug`, there is currently no `rails.db`/`rails.record` SQL metric, and Action View render metrics are only emitted when rendered events are raised to `:info` (see [Re-enable Started, Processing, and Rendered messages](#re-enable-started-processing-and-rendered-messages)). --- ## Operational notes ### Process forking If you use a forking server (Puma, Unicorn) or fork worker processes, see [Process Forking](operations.html#process-forking). With Semantic Logger v5 appenders are reopened automatically after a fork, so the manual `after_fork { SemanticLogger.reopen }` hook is usually no longer needed. ### Log rotation Because the log file is held open between writes, rotate it with a **copy-truncate** strategy rather than deleting and recreating the file. Example `logrotate` configuration for Linux: ~~~ /var/www/my_app/shared/log/*.log { daily missingok copytruncate rotate 14 compress delaycompress notifempty } ~~~ ### Loggers that are replaced automatically After they initialize, Rails Semantic Logger replaces the loggers of these libraries when present: - Action Cable - Bugsnag - Delayed Job - IOStreams - Mongo - Mongoid - Moped - Resque - Sidekiq (unless `replace_sidekiq_logger = false`) - Sidetiq - Solid Queue (unless `replace_solid_queue_logger = false`) --- ## Migrating from v4 to v5 ### Ruby and Rails minimums v5 requires Ruby 3.2+ and Rails 7.2+. It also depends on Semantic Logger v5; review the [Semantic Logger upgrade notes](upgrading.html) for changes there (the most relevant for Rails apps is that appenders are now reopened automatically after fork, so you can remove manual reopen hooks). ### Appender configuration is the main change In v4 the log file, its format, and any extra destinations were configured through several separate options (`format`, `add_file_appender`, `ap_options`, `filter`, `console_logger`) plus direct `config.semantic_logger.add_appender(...)` calls. In v5 all of that lives in one place, the [appenders block](#configuring-where-logs-go-the-appenders-block). These v4 options still work in v5 but emit a deprecation warning and will be **removed in v6**. Migrate them as follows: | v4 | v5 | |----|----| | `config.rails_semantic_logger.format = :json` | `appenders.add(file_name: "log/#{Rails.env}.log", formatter: :json)` | | `config.rails_semantic_logger.add_file_appender = false` then `config.semantic_logger.add_appender(...)` | Declare your destinations with `appenders.add(...)` (declaring any appender already replaces the default file appender) | | `config.rails_semantic_logger.ap_options = {multiline: false}` | `appenders.add(..., formatter: {color: {ap: {multiline: false}}})` | | `config.rails_semantic_logger.filter = /MyClass/` | `appenders.add(..., filter: /MyClass/)` | | `config.rails_semantic_logger.console_logger = false` | Omit `add_console` (declare a console appender only if you want one) | A v4 Heroku / standard-out configuration like: ~~~ruby # v4 if ENV["RAILS_LOG_TO_STDOUT"].present? $stdout.sync = true config.rails_semantic_logger.add_file_appender = false config.semantic_logger.add_appender(io: $stdout, formatter: config.rails_semantic_logger.format) end ~~~ becomes, in v5: ~~~ruby # v5 config.rails_semantic_logger.appenders do |appenders| appenders.add(io: $stdout, formatter: :json) end ~~~ ### Server standard-out behavior In v4, running `rails server` always added a standard-out logger, which you suppressed with `bin/rails s --daemon` or Puma's `--quiet`. In v5 this is the job of `add_server`: when you use the appenders block, declare an `add_server` appender to log to the screen while serving, or omit it to stay silent. If you do not use the appenders block at all, the v4 behavior is preserved. --- ## Migrating from earlier versions These notes apply to upgrades between older releases and are retained for reference. ### v4.16: Sidekiq metrics support Rails Semantic Logger added support for Sidekiq metrics, available when the JSON logging format is used: * `sidekiq.job.perform` — the duration of each Sidekiq job; `duration` contains the time in milliseconds that the job took to run. * `sidekiq.queue.latency` — the time between when a Sidekiq job was enqueued and when it was started; `metric_amount` contains the time in milliseconds that the job was waiting in the queue. ### v4.15 and v4.16: Sidekiq support Rails Semantic Logger introduced direct support for Sidekiq v4, v5, v6, and v7. Remove any previous custom patches or configurations used to make Sidekiq work with Semantic Logger. To see the complete list of patches and to contribute your own, see [Sidekiq Patches](https://github.com/reidmorrison/rails_semantic_logger/blob/v5.0.0/lib/rails_semantic_logger/extensions/sidekiq/sidekiq.rb). Support for Sidekiq v4, v5, and v6 ended after Rails Semantic Logger v5.0; Sidekiq v7 and v8 are supported and tested. ### v4.4 With some forking frameworks it was necessary to call `reopen` after the fork. As of v4.4 the workaround for Ruby 2.5 crashes is no longer needed. Remove the following line if it is called anywhere: ~~~ruby SemanticLogger::Processor.instance.instance_variable_set(:@queue, Queue.new) ~~~ --- ## Deprecated configuration options The following options still function in v5 for backward compatibility but emit deprecation warnings and will be **removed in v6**. Each is replaced by the [appenders block](#configuring-where-logs-go-the-appenders-block). | Deprecated option | Replacement | |-------------------|-------------| | `config.rails_semantic_logger.format` | `formatter:` on each appender, e.g. `appenders.add(file_name: ..., formatter: :json)` | | `config.rails_semantic_logger.ap_options` | `formatter: {color: {ap: {...}}}` on the appender | | `config.rails_semantic_logger.filter` | `filter:` on the appender | | `config.rails_semantic_logger.console_logger` | Declare (or omit) an `add_console` appender | | `config.rails_semantic_logger.add_file_appender` | Declare appenders in the block (doing so already replaces the default file appender) | --- ## Testing When testing application code, you often want to assert that it logged the right thing: the expected message, level, payload, or metric. Semantic Logger logs through a global, asynchronous pipeline, so you cannot simply read it back from a normal appender. Instead it ships a test helper that **captures log events in memory** during a block, so you can make assertions on them. The events are captured as raw `SemanticLogger::Log` objects, before any appender or formatter runs, so your assertions are not affected by how logging happens to be configured. Helpers are provided for both Minitest and RSpec. Other frameworks are covered further down. ## Minitest ### Step 1: install the helpers Add the helper methods to your tests, once, in `test_helper.rb`: ~~~ruby Minitest::Test.include SemanticLogger::Test::Minitest ~~~ ### Step 2: capture events Wrap the code under test in `semantic_logger_events`. It returns every log event created during the block: ~~~ruby messages = semantic_logger_events do User.new.enable! end ~~~ By default it captures events from every class. To capture only the events from one class, pass that class: ~~~ruby messages = semantic_logger_events(ApiClient) do # Only ApiClient log events created during this block are captured. end ~~~ ### Step 3: assert on the events Check how many events were produced, then assert on each one with `assert_semantic_logger_event`: ~~~ruby require_relative "test_helper" class UserTest < ActiveSupport::TestCase describe User do it "logs message" do messages = semantic_logger_events do User.new.enable! end # How many events were logged assert_equal 2, messages.count, messages # The first event assert_semantic_logger_event( messages[0], level: :info, message: "User enabled" ) # The second event assert_semantic_logger_event( messages[1], level: :debug, message: "Completed" ) end end end ~~~ Every argument other than the event itself is optional, so you assert only on what matters to the test. The available checks are: | Argument | Checks | |----------|--------| | `message` | Exact match of the text message. | | `message_includes` | Partial (substring) match of the message. | | `level` | Log level: `:trace`, `:debug`, `:info`, `:warn`, `:error`, `:fatal`. | | `payload` | Exact match of the payload Hash. | | `payload_includes` | Partial match: the given keys/values, ignoring any others. | | `tags` | Tags active on the thread when logged. | | `named_tags` | Named tags active on the thread when logged. | | `name` | Class name of the logger. | | `exception` | The Ruby exception that was logged. | | `metric` | The metric name. | | `metric_amount` | The metric amount. | | `dimensions` | The metric dimensions. | | `context` | Named contexts captured with the entry. | | `time` | When the entry was created. | | `duration` | Duration of a measure call, in milliseconds. | | `backtrace` | The captured backtrace, if any. | | `thread_name` | Name of the thread that logged the entry. | ### Match part of a message or payload For assertions that should not break on incidental detail, match partially. Use `message_includes` for a substring of the message: ~~~ruby assert_semantic_logger_event( messages[0], level: :info, message_includes: "enabled" ) ~~~ Use `payload_includes` to assert specific payload keys while ignoring any extras. Compare with `payload`, which must match the whole Hash exactly: ~~~ruby # Exact: the payload must be exactly these keys and values assert_semantic_logger_event( messages[0], level: :info, message: "User enabled", payload: {first_name: "Jack", last_name: "Jones"} ) # Partial: first_name must be present, other keys are ignored assert_semantic_logger_event( messages[0], level: :info, message: "User enabled", payload_includes: {first_name: "Jack"} ) ~~~ For more examples, see the [Rails Semantic Logger tests](https://github.com/reidmorrison/rails_semantic_logger/blob/main/test/active_record_test.rb). ## RSpec The RSpec helpers mirror the Minitest ones: a capture helper plus matchers for asserting on the captured events. ### Step 1: install the helpers Require the helpers and include them, once, in `spec_helper.rb`: ~~~ruby require "semantic_logger/test/rspec" RSpec.configure do |config| config.include SemanticLogger::Test::RSpec end ~~~ ### Step 2: capture events Wrap the code under test in `capture_semantic_logger_events`. It returns every log event created during the block, regardless of the global default log level: ~~~ruby events = capture_semantic_logger_events do User.new.enable! end ~~~ By default it captures events from every class. To capture only the events from one class, pass that class: ~~~ruby events = capture_semantic_logger_events(ApiClient) do # Only ApiClient log events created during this block are captured. end ~~~ ### Step 3: assert on the events Use `be_a_semantic_logger_event` to assert on a single captured event: ~~~ruby RSpec.describe User do it "logs message" do events = capture_semantic_logger_events do User.new.enable! end expect(events.count).to eq(2) expect(events[0]).to be_a_semantic_logger_event( level: :info, message: "User enabled" ) expect(events[1]).to be_a_semantic_logger_event( level: :debug, message: "Completed" ) end end ~~~ Every argument is optional, so you assert only on what matters to the test. The available checks are the same as the [Minitest table above](#step-3-assert-on-the-events), plus `message_includes`, `payload_includes`, and `exception_includes` for partial matches: ~~~ruby expect(events[0]).to be_a_semantic_logger_event( level: :info, message_includes: "enabled", payload_includes: {first_name: "Jack"} ) ~~~ An expected value that is a Class matches by type, and `:nil` asserts the attribute is `nil`: ~~~ruby expect(events[0]).to be_a_semantic_logger_event(time: Time, exception: :nil) ~~~ ### Match against a list of events `a_semantic_logger_event` is a composable alias, so it works inside `include` and other matchers: ~~~ruby expect(events).to include( a_semantic_logger_event(message: "User enabled") ) ~~~ ### Assert directly on a block `log_semantic_logger_event` captures the events for you and passes if any of them match. Pass `on:` to capture only one class's events: ~~~ruby expect { User.new.enable! }.to( log_semantic_logger_event(level: :info, message: "User enabled") ) expect { ApiClient.new.call }.to( log_semantic_logger_event(on: ApiClient, metric: "ApiClient/call") ) ~~~ ## Other test frameworks The same approach works anywhere: replace the logger with a `SemanticLogger::Test::CaptureLogEvents` instance. It looks and behaves like a normal logger, but keeps the raw log events in memory instead of writing them, so your assertions are unaffected by the configured appenders or their formats. ~~~ruby logger = SemanticLogger::Test::CaptureLogEvents.new ~~~ Stub it onto a single class to capture just that class's logging: ~~~ruby User.stub(:logger, logger) do # Capture all logging calls to the User logger. end ~~~ Or stub the global processor to capture logging from every class: ~~~ruby SemanticLogger::Logger.stub(:processor, logger) do # Capture all log events during the block. end ~~~ Either way, the captured events are available as `logger.events`. If you add helper methods for another framework like the Minitest ones, a pull request would be welcome. --- ## Operations This page covers running Semantic Logger in production: keeping logging alive across process forks, rotating log files, tuning the background pipeline, controlling a running process with signals, and shipping logs to a centralized system. The defaults are good for most applications, so reach for these topics when you have a specific operational need. For first-time setup (global settings, appenders, formatters, and filtering), see [Configuration](config.html). ## Process forking Frameworks such as Puma, Unicorn, and Resque **fork** the process: they start a worker by cloning the parent. A forked child does not inherit a working copy of the parent's log file handles or background thread, so unless those are re-opened in the child, logging quietly stops. ### It is automatic (default) As of v5 you do not need to do anything. Semantic Logger installs a `Process._fork` hook (Ruby 3.1 and later) that calls `SemanticLogger.reopen` in the child after `fork`, `Process.daemon`, `IO.popen`, `Kernel#system`, and backticks. That covers every forking framework (Puma, Unicorn, Resque, Spring, Phusion Passenger, parallel tests, and so on). `reopen` runs only once per process after a fork, so it is safe even if something else also calls it. ### Reopen manually You only need the steps below if you turned the automatic hook off, or you rotated logs in a way that did not fork (see [Log rotation](#log-rotation)). 1. Disable the automatic hook during boot, if you want full manual control: ~~~ruby SemanticLogger.reopen_on_fork = false ~~~ 2. Call `reopen` yourself after each fork, or after an in-process log rotation. Within the same process, pass `force: true` to bypass the once-per-process guard: ~~~ruby SemanticLogger.reopen # after a fork SemanticLogger.reopen(force: true) # same process, e.g. after external log rotation ~~~ You might opt out if another library forks in ways you do not want to trigger a reopen, or you need to control exactly when the appender thread restarts. ## Log rotation For performance the log file is **not** re-opened on every write, so a log file must be rotated with a **copy-truncate** strategy (copy the file aside, then truncate the original in place). Deleting or renaming the file would leave Semantic Logger writing to a handle that no longer points at the live file. Linux's `logrotate` does this well. To set it up: 1. Create a config file for your application, for example `/etc/logrotate.d/my_app`. 2. Point it at your log directory and include `copytruncate`. For daily rotation: ~~~ /var/www/rails/my_app/log/*.log { daily missingok copytruncate rotate 14 compress delaycompress notifempty } ~~~ Or, to rotate by size for very high volume logging: ~~~ /var/www/rails/my_app/log/*.log { size 2G missingok copytruncate rotate 7 compress nodelaycompress notifempty dateformat .%Y%m%d } ~~~ Other rotation tools work too, as long as they use copy-truncate. If your tool cannot copy-truncate and instead moves the file, reopen the handles in-process afterwards with `SemanticLogger.reopen(force: true)` (see [Process forking](#process-forking)). ## Performance and reliability tuning Every logger hands its events to one shared background thread through an in-memory queue. That thread writes each event to every appender in turn, so the call to `logger.info` returns immediately. The knobs below tune that pipeline. The defaults suit most applications; reach for a knob when you have a specific throughput, availability, or reliability requirement. ### Drop messages instead of blocking By default the queue is capped (`max_queue_size`, default `10,000`). When it fills (for example because an appender cannot keep up), `logger.info` **blocks** until there is room, guaranteeing no message is lost at the cost of briefly slowing the application. When availability matters more than complete logs, set `non_blocking: true` so that messages are **dropped** instead of blocking once the queue is full: ~~~ruby SemanticLogger.add_appender( file_name: "production.log", async: true, non_blocking: true ) ~~~ Dropped messages are counted and reported at most once every `dropped_message_report_seconds` (default `30`) so they do not go unnoticed: ~~~ruby SemanticLogger.add_appender( file_name: "production.log", async: true, non_blocking: true, dropped_message_report_seconds: 60 ) ~~~ `non_blocking` applies only to a capped queue. An uncapped queue (`max_queue_size: -1`) never blocks and never drops, but can grow without bound. ### Retry a failing appender If an appender raises while the worker thread is writing, the thread logs the error and restarts, so a transient failure (such as a brief network blip to a remote appender) does not permanently stop logging. Each restart sleeps with an increasing back-off (1 second, then 2, ...), reset as soon as a message is processed successfully. After `async_max_retries` (default `100`) consecutive failed restarts the worker thread gives up, rather than spinning forever on a persistent failure: ~~~ruby SemanticLogger.add_appender( appender: :http, url: "https://example.com/log", async: true, async_max_retries: 20 ) ~~~ Set `async_max_retries: -1` to retry indefinitely. The back-off still applies and still resets after a successful message. ### Give a slow appender its own thread If one destination is slow, such as a remote HTTP service, run just that appender on its own thread and queue so it cannot hold up the others: ~~~ruby SemanticLogger.add_appender(appender: :http, url: "https://example.com/log", async: true) ~~~ ### Monitoring the background thread The background thread can occasionally fall behind, for example when an appender is slow or a sudden burst of logging occurs. Check the queue at runtime: ~~~ruby # Number of log entries still waiting to be written SemanticLogger.queue_size ~~~ For a fuller operational picture, including per-appender queues, use `SemanticLogger.stats`. It returns a Hash describing the main pipeline and every appender, handy for exporting Semantic Logger's own health to a monitoring system such as Prometheus or statsd: ~~~ruby SemanticLogger.stats # => { # queue_size: 0, # entries waiting on the main pipeline queue # capped: true, # whether the main queue has a maximum size # max_queue_size: 10_000, # nil when uncapped # thread_active: true, # whether the main pipeline thread is running # processed: 1_532, # cumulative entries processed since startup # dropped: 0, # cumulative entries dropped at the main queue # appenders: [ # { name: "SemanticLogger::Appender::File", async: false }, # { name: "SemanticLogger::Appender::Http", # async: true, # this appender has its own thread and queue # thread_active: true, # queue_size: 3, # capped: true, # max_queue_size: 10_000, # processed: 1_529, # dropped: 0 } # ] # } ~~~ The `processed` and `dropped` counters are cumulative since process startup. Reading `stats` is thread-safe and adds no locking to the logging hot path. Semantic Logger also warns when an entry has waited on the queue too long. Tune the threshold and how often it is checked: ~~~ruby # Warn when an entry has been on the queue longer than this many seconds ( default: 30 ) SemanticLogger.lag_threshold_s # Number of messages to process between lag checks ( default: 1,000 ) SemanticLogger.lag_check_interval = 1_000 ~~~ If a sustained burst is overwhelming logging, reduce the volume by raising the log level, reduce the number of appenders, or speed up the slow appender. ### Synchronous operation Synchronous mode bypasses the background thread and logs inline on the calling thread. This disables a core design principle of Semantic Logger and slows the calling thread, so it is not recommended for most applications. It can suit short-lived or single-threaded programs, or forked environments where you would rather not re-create the logging thread. Enable it **before** adding any appenders: ~~~ruby SemanticLogger.sync! ~~~ To guarantee it is set early enough, replace the require with the synchronous variant: ~~~ruby require "semantic_logger/sync" ~~~ Or, in a Gemfile: ~~~ruby gem "semantic_logger", require: "semantic_logger/sync" ~~~ ## Linux signals On Linux, Unix, and Mac, Semantic Logger can respond to signals, for example to change the log level of a running process without restarting it. It registers no signal handlers on startup, so as not to interfere with any your application already uses. **Step 1: enable signal handling** during boot: ~~~ruby # config/initializers/semantic_logger.rb, or during startup of a standalone app SemanticLogger.add_signal_handler ~~~ **Step 2: send the signal** you need. The capabilities are below. ### Change the log level (USR2) Send `SIGUSR2` to rotate the global default level, without restarting. Each signal moves the level one step through this sequence, wrapping from `:trace` back to `:fatal`: :fatal :error :warn :info :debug :trace ~~~ kill -SIGUSR2 1234 ~~~ This changes only the global default level. Loggers whose level was set explicitly in the application are unaffected. ### Dump all threads (TTIN) Send `TTIN` to write every thread, with its backtrace where available, to the log. Naming your threads (`Thread.current.name = "My Worker"`) makes the dump far more useful: ~~~ kill -TTIN 1234 ~~~ On JRuby this differs from the standard `QUIT`-triggered Java thread dump, which includes system threads and Java stack traces. ### JRuby garbage collection logging On JRuby, any garbage collection that takes longer than 100ms is logged as a warning to the regular appenders, giving visibility into GC pauses that could affect active requests. ### Choose your own signals Pass different signals, or set one to `nil` to skip it. Set the GC threshold to `nil` to skip the JRuby garbage collection logging: ~~~ruby # Log level change on USR1, thread dump on USR2, GC threshold of 100,000 micro-seconds SemanticLogger.add_signal_handler("USR1", "USR2", 100000) ~~~ ## Centralized logging Once you run more than one process or server, reading log files one at a time stops scaling. A **centralized logging** system collects the events from every process into one place where you can search, filter, and build dashboards across all of them at once. This is where Semantic Logger's structured output pays off: the payload, tags, duration, and metrics on each entry arrive as real fields, not text that has to be re-parsed. This walks through one popular stack end to end as a concrete example: * **Semantic Logger** forwards structured events from your application. * **Elasticsearch** stores and indexes them. * **Kibana** provides search and dashboards on top of Elasticsearch. The same shape applies to other aggregators (Graylog, Splunk, Loki, Logstash, Syslog). See [Other destinations](#other-destinations) and [Appenders](appenders.html). ### Step 1: Run Elasticsearch and Kibana Install and start both. Any installation method works; these notes use [homebrew](https://brew.sh) on macOS, follow the product links for other platforms. ~~~ brew install elasticsearch brew install kibana ~~~ Start each one (follow the on-screen instructions to auto-start them), and confirm Elasticsearch is reachable, by default at `http://localhost:9200`, and Kibana at `http://localhost:5601`. ### Step 2: Forward your application's logs Add the Elasticsearch appender so Semantic Logger ships every entry to Elasticsearch. In a Rails app using [rails_semantic_logger](rails.html), put this in an initializer; otherwise add it where you configure Semantic Logger at startup: ~~~ruby SemanticLogger.add_appender( appender: :elasticsearch, url: "http://localhost:9200" ) ~~~ By default entries are written to a daily index named `semantic_logger-YYYY.MM.DD`, so the index pattern to search in Kibana is `semantic_logger-*`. See [Elasticsearch](appenders.html#elasticsearch) for options such as a custom index name or data streams. Restart the application and exercise it so it generates a few log entries. If nothing appears later, check the application's own log (for example `log/development.log`) for connection errors. ### Step 3: View the logs in Kibana 1. Open Kibana at [http://localhost:5601](http://localhost:5601). 2. Create an **index pattern** (called a "data view" in newer Kibana versions) that matches `semantic_logger-*`. 3. When asked for the time field, choose `timestamp`. 4. Open **Discover**. Your application's log entries appear. If the list is empty, widen the time range in the top right. 5. Add a few columns so each entry is readable at a glance, for example `host`, `level`, `name`, and `message`. The exact menu names vary between Kibana versions, but the three things you need are always the same: an index pattern of `semantic_logger-*`, a time field of `timestamp`, and the Discover view. ### Step 4: Search In Discover, query against the structured fields directly. A few examples: ~~~ # Only error level entries level: error # Only entries from one host host: mymachine # Find a value in the logging tags tags: 17262353 ~~~ Because the payload, tags, and metrics are real fields, you can filter and build dashboards on them without writing log-parsing expressions. ### Other destinations Elasticsearch and Kibana are just one option. Semantic Logger also forwards to other centralized logging systems and aggregators, including: * Logstash * Graylog * Splunk * Grafana Loki * Loggly * Syslog See [Appenders](appenders.html) to configure any of these as a destination. --- ## Security Logging frameworks sit on a sensitive boundary: they take data from all over an application, including data that originated from untrusted users, and write it to files, terminals, and centralized logging systems. This page describes the security properties of Semantic Logger and how to configure it when logging untrusted data. ### Log injection and forging When a log message, tag, or exception message contains untrusted, attacker-controlled data (for example a user name, request parameter, or `User-Agent` header), control characters in that data can be abused: * A newline can forge an additional, fake log entry ("log forging"), or split one record into two at a collector that frames records with a separator. * An ANSI escape sequence can spoof or hide terminal output when the log is viewed in a terminal. The structured formatters, such as `:json`, are **not** affected, because JSON encoding always escapes control characters. They are the recommended choice when forwarding logs that may contain untrusted data to a centralized logging system. By design the human readable text formatters (`:default` and `:color`) preserve newlines and ANSI color codes, since multi-line and colorized output is useful when reading logs locally. When the text output may contain untrusted data, enable the `escape_control_chars` option to replace control characters with a printable, escaped form (for example a newline becomes `\n`): ~~~ruby SemanticLogger.add_appender(file_name: "production.log", formatter: {default: {escape_control_chars: true}}) ~~~ See [Escaping Control Characters](config.html#escaping-control-characters) for details. The record-framed network appenders are already safe by default: * The **TCP** and **UDP** appenders default to the JSON formatter. * The **Syslog** formatters escape control characters by default, since syslog frames each record. If you replace the formatter on any of these appenders with a text formatter, enable `escape_control_chars` as shown above. ### Redacting sensitive data Semantic Logger does not automatically redact secrets or personal information; it logs the message and payload it is given. Avoid logging passwords, tokens, full credit card numbers, and similar values in the first place. When sensitive values can reach the logs indirectly, redact them. A global `on_log` subscriber runs inline, once per log event, before the event is handed to any appender, so it can scrub the message or payload for every destination: ~~~ruby SECRET_KEYS = %i[password token authorization secret].freeze SemanticLogger.on_log do |log| log.payload&.each_key do |key| log.payload[key] = "[REDACTED]" if SECRET_KEYS.include?(key) end end ~~~ Redaction can also be applied to a single logger or appender with a `filter` that mutates the log and returns `true`. See [Filtering](config.html#filtering) for examples. Rails applications should use the sister gem [rails_semantic_logger](https://github.com/reidmorrison/rails_semantic_logger), which honours `config.filter_parameters` so that parameters Rails already considers sensitive are filtered out of the logs. ### Transport encryption Appenders that send logs over the network support TLS. The appenders built on the `:http` appender (`:http`, `:splunk_http`, and `:elasticsearch_http`) default to verifying the server certificate (`OpenSSL::SSL::VERIFY_PEER`) when an `https` URL is used. Do not disable certificate verification in production. Settings such as `ssl: {verify_mode: OpenSSL::SSL::VERIFY_NONE}` expose the connection to man-in-the-middle attacks and should be limited to local development against self-signed certificates. ### Log file permissions Log files frequently contain sensitive information. By default the file appender creates files using the process umask, the standard Ruby behavior. To restrict access, supply the `permissions` option, which is applied both when the file is created and to an existing log file: ~~~ruby # Owner read/write, group read, no access for others: SemanticLogger.add_appender(file_name: "production.log", permissions: 0o640) ~~~ ### Dependencies and supply chain Semantic Logger has a single runtime dependency, `concurrent-ruby`. Appenders for third-party services (Kafka, MongoDB, Sentry, etc.) keep their backing gem optional: it is required lazily inside the appender only when that appender is used, and is never added to the gemspec. This keeps the dependency surface of an application that only uses the built-in file and IO appenders very small. Auditing the resolved dependency versions for known advisories is the responsibility of the consuming application, which should run a tool such as [bundler-audit](https://github.com/rubysec/bundler-audit) against its own `Gemfile.lock`. Pinning dependencies with a committed lockfile, and verifying it in CI with `bundle install --frozen`, prevents an unexpected dependency from being pulled in. --- ## Upgrading ### Upgrading to Semantic Logger v5.0 #### Minimum Ruby version is now 3.2 Semantic Logger v5 requires Ruby 3.2 or later. Earlier Ruby versions are end-of-life and are no longer supported or tested. If you are on an older Ruby, upgrade Ruby before upgrading Semantic Logger, or stay on the v4.x series. #### `SemanticLogger::Appender::AsyncBatch` has been removed The internal asynchronous proxy classes have been consolidated. Batch processing now runs through the same `SemanticLogger::Appender::Async` proxy (backed by an internal `QueueProcessor`), so the separate `SemanticLogger::Appender::AsyncBatch` class no longer exists. This only affects code that referenced the class directly, which is uncommon since appenders are added through `SemanticLogger.add_appender`. The `batch:`, `batch_size:`, and `batch_seconds:` options are unchanged: ~~~ruby SemanticLogger.add_appender(appender: :http, url: "https://example.com/log", batch: true) ~~~ What changed: - `SemanticLogger.add_appender(..., batch: true)` now returns a `SemanticLogger::Appender::Async` (with `#batch?` returning `true`) instead of a `SemanticLogger::Appender::AsyncBatch`. - Referencing the constant `SemanticLogger::Appender::AsyncBatch` now raises `NameError`. If you have a custom appender or test asserting on the proxy class, change `instance_of?`/`is_a?(SemanticLogger::Appender::AsyncBatch)` checks to `SemanticLogger::Appender::Async` and, if needed, check `appender.batch?`. #### Appenders are reopened automatically after fork Previously, applications that fork (Puma, Unicorn, Spring, Resque, `Process.daemon`, etc.) had to call `SemanticLogger.reopen` themselves in an after-fork hook, otherwise the child shared the parent's file handles and background thread. In v5 this happens automatically: a `Process._fork` / `Process.daemon` hook calls `SemanticLogger.reopen` in the child process. The call is guarded to run once per process. For most applications you can now **remove** the manual reopen hook, for example: ~~~ruby # No longer needed in v5 — handled automatically: before_fork { SemanticLogger.flush } after_fork { SemanticLogger.reopen } ~~~ If you have a reason to manage this yourself (for example you reopen at a very specific point in your boot sequence), disable the automatic behaviour: ~~~ruby SemanticLogger.reopen_on_fork = false ~~~ #### Recommended: enable logger caching By default `SemanticLogger[SomeClass]` returns a **new** `Logger` instance on every call. This means that if one part of the code obtains a logger and later changes its `level` (or filter), other holders of "the same" logger do not see the change, because they hold different instances. v5 adds opt-in caching so that a `Class` or `Module` maps to a single shared `Logger` instance: ~~~ruby SemanticLogger.cache_loggers = true ~~~ With caching enabled: - `SemanticLogger[MyClass]` and the `Loggable` mixin's `MyClass.logger` return the **same** instance. - Changing that logger's level or filter is visible to every holder. - Strings (`SemanticLogger["Name"]`) always get a fresh instance, and anonymous classes (no `name`) are never cached. Enabling caching is recommended for most applications. It is opt-in (default `false`) to preserve existing behaviour for code that relied on getting an independent logger per call. If you need to discard cached loggers (for example in tests, or after redefining a class), call `SemanticLogger.clear_logger_cache`. #### Control characters are escaped in Syslog output To prevent log-injection via embedded control characters (newlines, escape sequences, etc.), the Syslog formatter now escapes control characters in records by default. If you relied on raw control characters reaching syslog, this output now differs. The text formatters (default, color) also gain an opt-in `escape_control_chars` option (default `false`) for the same protection. See the [Security](security.html) page for details. #### `Formatters::Base#cleanse` renamed The internal `SemanticLogger::Formatters::Base#cleanse` method was renamed to `#escape_control_characters`. This only affects custom formatters or subclasses that called the old method name directly; update them to call `#escape_control_characters`. #### Rails: appenders are now configured in one block If you use [rails_semantic_logger](rails.html), the companion gem's v5 release moves appender configuration into a single `config.rails_semantic_logger.appenders do |appenders| ... end` block. The v4 options (`format`, `add_file_appender`, `ap_options`, `filter`, `console_logger`) still work but are deprecated and will be removed in v6. See [Migrating from v4 to v5](rails.html#migrating-from-v4-to-v5) for the full before/after mapping. ### Upgrading to Semantic Logger v4.18 #### Async queue is now bounded by default The default `max_queue_size` for async appenders changed from `-1` (unbounded) to `10_000`. Under sustained high log volume, messages that exceed the queue limit will be dropped rather than causing memory to grow without bound. If your application logs at very high throughput and you were relying on the unbounded default, set the limit explicitly when adding each appender: ~~~ruby SemanticLogger.add_appender(io: $stdout, async: true, max_queue_size: -1) ~~~ Alternatively, tune the value to match your application's tolerance for log loss vs. memory usage. #### Invalid exception values are now wrapped Previously, passing a non-`Exception` object to the `exception:` keyword argument would silently misbehave. It is now wrapped in an `ArgumentError` with a description of the invalid value and a backtrace pointing to the call site: ~~~ruby # This previously caused undefined behaviour — now raises a descriptive ArgumentError logger.error("Something went wrong", exception: "a plain string") ~~~ #### New Relic formatter — trace and span metadata Previously, New Relic trace metadata (`trace.id`, `span.id`, `entity.name`, etc.) was captured at format time, so it worked with any appender type. In v4.18, this metadata is captured via an `on_log` callback that is only registered when `SemanticLogger::Appender::NewRelicLogs` is instantiated. If you use a plain IO or File appender with `SemanticLogger::Formatters::NewRelicLogs` as the formatter (rather than the dedicated `NewRelicLogs` appender), you must register the callback manually in your initializer, otherwise `trace.id`, `span.id`, and `entity.name` will be absent from all logs: ~~~ruby require "semantic_logger/appender/new_relic_logs" SemanticLogger.on_log(SemanticLogger::Appender::NewRelicLogs::CAPTURE_CONTEXT) ~~~ This line should be added before any log messages are emitted. --- ### Upgrading to Semantic Logger v4.17 #### New Relic formatter (breaking changes for New Relic users) The New Relic log formatter (`SemanticLogger::Formatters::NewRelicLogs`) has been significantly updated to produce a structure that New Relic can natively parse and index as individual attributes. Most users will benefit from richer log correlation in New Relic, but there are several breaking changes to be aware of. ##### Log output structure Previously, all semantic fields (`payload`, `tags`, `named_tags`, `metric`, etc.) were serialized as a JSON string inside the `message` key. They are now emitted as top-level keys: Before (v4.16): ~~~json { "message": "{\"message\":\"Order processed\",\"payload\":{\"order_id\":123},\"tags\":[],\"named_tags\":{}}", "timestamp": 1234567890000, "log.level": "INFO" } ~~~ After (v4.17): ~~~json { "message": "Order processed", "payload": { "order_id": 123 }, "timestamp": 1234567890000, "logger": { "name": "OrderService" }, "thread": { "name": "main" } } ~~~ **Impact**: Any New Relic alert queries, dashboards, or NRQL that references fields inside the old `message` JSON string (e.g. `message.payload.order_id`) must be updated to reference the new top-level keys (e.g. `payload.order_id`). Note that New Relic's log ingestion flattens nested objects. A `payload` value that is itself a nested Hash may not be fully indexed as individually searchable attributes. If you need payload fields to be queryable in New Relic, promote them to `named_tags` instead: ~~~ruby # Payload fields may not be individually queryable in New Relic: logger.info "Order processed", order_id: 123, amount: 49.99 # Use named_tags for fields you want to query directly in New Relic: SemanticLogger.tagged(order_id: 123, amount: 49.99) do logger.info "Order processed" end ~~~ ##### named_tags are now merged to the top level Previously `named_tags` were nested inside the `message` JSON string. They are now merged directly into the top-level log entry, making them first-class attributes in New Relic. If a named tag key conflicts with an existing top-level key (e.g. `message`, `timestamp`), it is dropped and the conflicting key names are recorded under `named_tag_conflicts`. ##### Error, logger, and thread fields restructured Before (v4.16): ~~~json { "error.message": "Something went wrong", "error.class": "RuntimeError", "error.stack": "...", "log.level": "ERROR", "logger.name": "OrderService", "thread.name": "main" } ~~~ After (v4.17): ~~~json { "error": { "message": "Something went wrong", "class": "RuntimeError", "stack": "..." }, "logger": { "name": "OrderService" }, "thread": { "name": "main" } } ~~~ Update any New Relic alert conditions or NRQL queries that reference the old dot-notation keys. #### Suppressing timestamps in formatters v4.17 added `:notime` as an explicit `time_format` value to suppress timestamp output. If you were previously passing `time_format: nil` expecting it to suppress output, use `:notime` instead: ~~~ruby SemanticLogger.add_appender(io: $stdout, formatter: MyFormatter.new(time_format: :notime)) ~~~ --- ### Upgrading to Semantic Logger v4.9 These changes should not be noticeable by the majority of users of Semantic Logger, since they are to the internal API. It is possible that advanced users may be using these internal API's directly. This does not affect any calls to the public api `SemanticLogger.add_appender`. File and IO are now separate appenders. When creating the File appender explicitly, its arguments have changed. For example, when requesting an IO stream, it needs to be changed from: ~~~ruby SemanticLogger::Appender::File.new(io: $stderr) ~~~ to: ~~~ruby SemanticLogger::Appender::IO.new($stderr) ~~~ Additionally, this needs to be changed from: ~~~ruby SemanticLogger::Appender::File.new(file_name: "file.log") ~~~ to: ~~~ruby SemanticLogger::Appender::File.new("file.log") ~~~ Rails Semantic Logger, if used, needs to be upgraded to v4.9 when upgrading to Semantic Logger v4.9. --- ### Upgrading to Semantic Logger v4.4 With some forking frameworks it is necessary to call `reopen` after the fork. With v4.4 the workaround for Ruby 2.5 crashes is no longer needed. I.e. Please remove the following line if being called anywhere: ~~~ruby SemanticLogger::Processor.instance.instance_variable_set(:@queue, Queue.new) ~~~ --- ### Upgrading to Semantic Logger v4.0 The following changes need to be made when upgrading to V4: - Ruby V2.3 / JRuby V9.1 is now the minimum runtime version. - Replace calls to `Logger#with_payload` with `SemanticLogger.named_tagged`. - Replace calls to `Logger#payload` with `SemanticLogger.named_tags`. - MongoDB Appender requires Mongo Ruby Client V2 or greater. - Appenders now write payload data in a separate `:payload` tag instead of mixing them directly into the root elements to avoid name clashes. As a result any calls like the following: ~~~ruby logger.debug foo: 'foo', bar: 'bar' ~~~ Must be replaced with the following in v4: ~~~ruby logger.debug payload: {foo: 'foo', bar: 'bar'} ~~~ Similarly, for measure blocks: ~~~ruby logger.measure_info('How long is the sleep', foo: 'foo', bar: 'bar') { sleep 1 } ~~~ Must be replaced with the following in v4: ~~~ruby logger.measure_info('How long is the sleep', payload: {foo: 'foo', bar: 'bar'}) { sleep 1 } ~~~ The common log call has not changed, and the payload is still logged directly: ~~~ruby logger.debug('log this', foo: 'foo', bar: 'bar') ~~~