Semantic Logger

repository·main·Indexed 19 days ago

https://github.com/reidmorrison/semantic_logger

A high-performance, asynchronous structured logging framework for Ruby and Rails. It preserves data types by using payloads (Hashes) instead of string interpolation and prevents application blocking via an in-memory queue and background worker thread. Features include block-based logging for performance, execution time measurement with `measure_*` methods, thread-scoped and instance-scoped tagging, and support for numerous destinations including Elasticsearch, MongoDB, Splunk, and OpenTelemetry.

Tokens
40.9K
Snippets
149
Records
178
Agent score
76%

What's inside semantic_logger

  1. How rails_semantic_logger works out of the box

    main

    Once installed, rails_semantic_logger automatically replaces the standard Rails logger.

    Default Behavior:

    • Writes to log/<environment>.log (e.g., log/development.log).
    • Colorizes output in development.
    • Logs to standard out ($stdout) when running rails server.
    • Logs to standard error ($stderr) when running rails console (to prevent logs from mixing with command return values).
    • Collapses multi-line Rails request logs into a single, structured "Completed" line containing searchable fields like controller, action, status, and duration.
  2. Configure server standard-out behavior in v5

    main

    In v5, the behavior of rails server regarding standard-out logging is controlled via the appenders block.

    • To log to the screen while serving, declare an add_server appender within the appenders block.
    • To stay silent (suppress standard-out), omit the add_server appender.

    Note: If you do not use the appenders block at all, the v4 behavior (where rails server always adds a standard-out logger) is preserved.

  3. How testing log events works in Semantic Logger

    main

    Because Semantic Logger uses a global, asynchronous pipeline, you cannot reliably test logs by reading from standard appenders. Instead, the library provides test helpers that capture log events as raw SemanticLogger::Log objects in memory during a specific block of code.

    These captured events are captured before any appenders or formatters process them, ensuring your tests are not affected by your application's logging configuration. Helpers are available for Minitest and RSpec, or you can manually use SemanticLogger::Test::CaptureLogEvents for other frameworks.

  4. What are Appenders in Semantic Logger?

    main

    An appender is a destination where log entries are written, such as a file, the screen, a database, or a remote service.

    Semantic Logger supports multiple appenders simultaneously. This allows you to route the same log call to different destinations with different configurations. For example, you can output colorized text to the console, JSON to a file, and error logs to an external monitoring service all at once.

  5. Capture causal (nested) exceptions

    main

    Semantic Logger automatically captures and logs the full exception chain when using Ruby's cause mechanism (where one exception is raised while rescuing another). When you log an error with an exception, the output will include the primary exception and follow with the subsequent exceptions in the chain, prefixed with Cause:.

    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
  6. Understand Rails Semantic Logger metrics (Prototype)

    main

    Rails Semantic Logger attaches a metric to entries logged at :info, :warn, or :error. If an entry carries a duration (like a request or query), it records the timing; otherwise, it acts as an event counter.

    Note: This is a prototype. Metric names follow the pattern rails.<component>.<event> and are subject to change.

    Key Metric Components:

    • Action Controller: rails.controller.process_action, rails.controller.redirect_to, etc.
    • Action View: rails.view.render.template, rails.view.render.partial, etc.
    • Active Job: rails.job.enqueue, rails.job.perform_start, etc.
    • Action Mailer: rails.mailer.deliver
    • Solid Queue: rails.solid_queue.<event>

    Note: :debug level entries do not carry metrics.

  7. Add structured data (payloads) to log entries

    main

    Unlike traditional logging that interpolates data into a string, Semantic Logger allows you to pass a Hash as a "payload". This keeps the human-readable message separate from machine-readable data, allowing appenders (like Elasticsearch or MongoDB) to index the fields for searching and dashboarding.

    # Traditional (Avoid this):
    logger.info("Queried users in #{duration}ms, result #{result}")
    
    # Semantic (Recommended):
    logger.info("Queried users", duration: duration, result: result, table: "users")
  8. What are metrics in Semantic Logger

    main

    A metric is a named number emitted alongside a log entry. This allows a single call to record both what happened (the log message) and provide data for dashboards and alerts (the metric).

    Key Concepts

    • Two-part process: You must Emit a metric (by adding the :metric option to a log or measure_ call) and Subscribe a destination (like Statsd or SignalFx) to receive it. If no subscriber is registered, the :metric option is ignored.
    • Asynchronous: Metric subscribers are notified on the background log thread, ensuring that emitting a metric does not slow down your application's execution.
    • Log Level Sensitivity: Metrics obey the same log level and filtering rules as the log entry they accompany. For example, a metric attached to a :trace log will not be emitted if the current log level is set to :info.
    • Application vs. Operational Stats: These are application metrics (what your code is doing). To monitor the health of Semantic Logger itself (e.g., queue sizes), use SemanticLogger.stats instead.
  9. Tag related entries using thread-scoped or instance-scoped tags

    main

    Tags allow you to group related log entries (e.g., by request ID or user ID).

    Thread-scoped Tags Use SemanticLogger.tagged(...) with a block. All logs inside the block will carry these tags. Tags are scoped to the current thread and do not automatically propagate to new threads.

    Instance-scoped (Child) Loggers Call tagged (or with_tags) without a block on a logger instance. This returns a new "child" logger that permanently carries those tags. This is useful for objects like ActiveRecord models or background jobs where you want every log from that specific instance to be tagged without wrapping every method in a block.

    # Thread-scoped tags
    SemanticLogger.tagged(user: "Jack", zip_code: 12345) do
      logger.debug("Hello World") # carries user and zip_code
    end
    
    # Instance-scoped (Child) loggers
    class Cart
      include SemanticLogger::Loggable
    
      def initialize(id)
        @id     = id
        # Returns a new logger instance that always carries cart_id
        @logger = SemanticLogger["Cart"].tagged(cart_id: id)
      end
    
      attr_reader :logger
    
      def add_item(item_id)
        logger.info("Added item", item_id: item_id)
      end
    end
  10. How Semantic Logger works (Asynchronous Logging)

    main

    Semantic Logger is designed for high performance using an asynchronous model:

    1. In-memory Queue: Log calls from your application threads place log events into a shared in-memory queue.
    2. Background Thread: A single, dedicated background thread pulls events from the queue and writes them to all registered appenders (destinations).
    3. Non-blocking: Because the actual writing (to disk, network, etc.) happens in the background thread, your application threads return immediately after enqueuing the event.

    This design ensures that logging thousands of lines per second does not block your application's execution. The queue is capped by default to prevent memory exhaustion, blocking the application if the queue is full to ensure no logs are lost (though this behavior can be tuned).

  11. Understanding the Log Event object

    main

    In Semantic Logger, every logging call (e.g., logger.info, logger.measure_error) creates a single Log event object. This object is the fundamental unit of work that flows through the entire pipeline:

    1. Filters: A Proc receives the event and returns true to keep it or false to discard it.
    2. Formatters: Receives the event to transform it into a specific output format (e.g., JSON, text).
    3. Appenders: The #log(log) method receives the event to write it to a destination (e.g., stdout, a file, or a background thread).

    Because all appenders share the same event instance for a single log call, any mutations made by a filter or formatter are visible to all subsequent steps in the pipeline.

  12. How Semantic Logger works: Structured and Asynchronous logging

    main

    Semantic Logger is designed for high performance using two core principles:

    1. Structured Logging: Instead of flattening everything into a string, it carries a payload (Hash), exceptions, durations, metrics, and tags. This preserves the data types for downstream search engines.
    2. Asynchronous Execution: Log events are pushed to an in-memory queue. A separate background thread services this queue to write to destinations, preventing logging from blocking your application's main execution flow.