timberio/timber-ruby

View on GitHub
lib/timber/current_context.rb

Summary

Maintainability
A
0 mins
Test Coverage
require "socket"

require "timber/config"

module Timber
  # Holds the current context in a thread safe memory storage. This context is
  # appended to every log line. Think of context as join data between your log lines,
  # allowing you to relate them and filter them appropriately.
  #
  # @note Because context is appended to every log line, it is recommended that you limit this
  #   to only neccessary data needed to relate your log lines.
  class CurrentContext
    THREAD_NAMESPACE = :_timber_current_context.freeze

    class << self
      # Impelements the Singleton pattern in a thread specific way. Each thread receives
      # its own context.
      def instance
        Thread.current[THREAD_NAMESPACE] ||= new
      end

      # Convenience method for {CurrentContext#add}. See {CurrentContext#add} for more info.
      def add(*args)
        instance.add(*args)
      end

      # Convenience method for {CurrentContext#fetch}. See {CurrentContext#fetch} for more info.
      def fetch(*args)
        instance.fetch(*args)
      end

      # Convenience method for {CurrentContext#remove}. See {CurrentContext#remove} for more info.
      def remove(*args)
        instance.remove(*args)
      end

      # Convenience method for {CurrentContext#reset}. See {CurrentContext#reset} for more info.
      def reset(*args)
        instance.reset(*args)
      end

      # Convenience method for {CurrentContext#with}. See {CurrentContext#with} for more info.
      def with(*args, &block)
        instance.with(*args, &block)
      end
    end

    # Adds contexts but does not remove them. See {#with} for automatic maintenance and {#remove}
    # to remove them yourself.
    #
    # @note Because context is included with every log line, it is recommended that you limit this
    #   to only neccessary data.
    def add(*objects)
      objects.each do |object|
        hash.merge!(object.to_hash)
      end
      expire_cache!
      self
    end

    # Fetch a specific context by key.
    def fetch(*args)
      hash.fetch(*args)
    end

    # Removes a context. If you wish to remove by key, or some other way, use {#hash} and
    # modify the hash accordingly.
    def remove(*keys)
      keys.each do |keys|
        hash.delete(keys)
      end
      expire_cache!
      self
    end

    def replace(hash)
      @hash = hash
      expire_cache!
      self
    end

    # Resets the context to be blank. Use this carefully! This will remove *any* context,
    # include context that is automatically included with Timber.
    def reset
      hash.clear
      expire_cache!
      self
    end

    # Snapshots the current context so that you get a moment in time representation of the context,
    # since the context can change as execution proceeds. Note that individual contexts
    # should be immutable, and we implement snapshot caching as a result of this assumption.
    def snapshot
      @snapshot ||= hash.clone
    end

    # Adds a context and then removes it when the block is finished executing.
    #
    # @note Because context is included with every log line, it is recommended that you limit this
    #   to only neccessary data.
    #
    # @example Adding a custom context
    #   Timber::CurrentContext.with({build: {version: "1.0.0"}}) do
    #     # ... anything logged here will include the context ...
    #   end
    #
    # @note Any custom context needs to have a single root key to be valid. i.e. instead of:
    #   Timber::CurrentContext.with(job_id: "123", job_name: "Refresh User Account")
    #
    # do
    #
    #   Timber::CurrentContext.with(job: {job_id: "123", job_name: "Refresh User Account"})
    #
    # @example Adding multiple contexts
    #   Timber::CurrentContext.with(context1, context2) { ... }
    def with(*objects)
      old_hash = hash.clone
      begin
        add(*objects)
        yield
      ensure
        replace(old_hash)
      end
    end

    private
      # The internal hash that is maintained. Use {#with} and {#add} for hash maintenance.
      def hash
        @hash ||= build_initial_hash
      end

      # Builds the initial hash. This is extract into a method to support a threaded
      # environment. Each thread holds it's own context and also needs to instantiate
      # it's hash properly.
      def build_initial_hash
        new_hash = {}

        # Release context
        release_context = Util::NonNilHashBuilder.build do |h|
          h.add(:commit_hash, ENV['RELEASE_COMMIT'] || ENV['HEROKU_SLUG_COMMIT'])
          h.add(:created_at, ENV['RELEASE_CREATED_AT'] || ENV['HEROKU_RELEASE_CREATED_AT'])
          h.add(:version, ENV['RELEASE_VERSION'] || ENV['HEROKU_RELEASE_VERSION'])
        end

        if release_context != {}
          new_hash.merge!({release: release_context})
        end

        # System context
        hostname = Socket.gethostname
        pid = Process.pid
        system_context = Contexts::System.new(hostname: hostname, pid: pid)
        new_hash.merge!(system_context.to_hash)

        # Runtime context
        thread_object_id = Thread.current.object_id
        runtime_context = {thread_id: thread_object_id}
        new_hash.merge!({runtime: runtime_context})

        new_hash
      end

      # Hook to clear any caching implement in this class
      def expire_cache!
        @snapshot = nil
      end
  end
end