Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested Diagnostic Contexts #4

Merged
merged 8 commits into from
Jan 10, 2013
Merged
15 changes: 15 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Release notes

## 0.2.0

* Added nested diagnostic context and Rack middleware to clear it between
requests

### Note

The `Hatchet::Message` constructor has been altered, going forward it will take
a Hash of arguments instead of fixed arguments. It is currently backwards
compatible but this will likely be dropped for 1.0.0 so it is advised you update
your libraries now.

This should only affect custom formatters which may want to take advantage of
the nested diagnostic context which is now available anyway.

## 0.1.0

No changes from 0.0.20, just time for a minor version release.
Expand Down
6 changes: 3 additions & 3 deletions lib/hatchet.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# -*- encoding: utf-8 -*-

require 'logger'

require_relative 'hatchet/level_manager'
require_relative 'hatchet/backtrace_formatter'
require_relative 'hatchet/thread_name_formatter'
Expand All @@ -10,6 +8,8 @@
require_relative 'hatchet/hatchet_logger'
require_relative 'hatchet/logger_appender'
require_relative 'hatchet/message'
require_relative 'hatchet/middleware'
require_relative 'hatchet/nested_diagnostic_context'
require_relative 'hatchet/plain_formatter'
require_relative 'hatchet/simple_formatter'
require_relative 'hatchet/standard_formatter'
Expand Down Expand Up @@ -66,7 +66,7 @@ module Hatchet
# Returns a HatchetLogger for the object.
#
def logger
@_hatchet_logger ||= HatchetLogger.new self, Hatchet.configuration
@_hatchet_logger ||= HatchetLogger.new(self, Hatchet.configuration, Hatchet::NestedDiagnosticContext.current)
end

# Public: Returns a HatchetLogger for the object.
Expand Down
16 changes: 12 additions & 4 deletions lib/hatchet/hatchet_logger.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- encoding: utf-8 -*-

require 'logger'

module Hatchet

# Public: Class that handles logging calls and distributes them to all its
Expand Down Expand Up @@ -60,15 +62,21 @@ class HatchetLogger
Logger::FATAL => :fatal
}

# Public: Gets the NestedDiagnosticContext for the logger.
#
attr_reader :ndc

# Internal: Creates a new logger.
#
# host - The object the logger gains its context from.
# configuration - The configuration of Hatchet.
# ndc - The nested diagnostic context of the logger.
#
def initialize(host, configuration)
@context = context host
def initialize(host, configuration, ndc)
@context = host_name(host)
@configuration = configuration
@appenders = configuration.appenders
@ndc = ndc
end

[:debug, :info, :warn, :error, :fatal].each do |level|
Expand Down Expand Up @@ -103,7 +111,7 @@ def initialize(host, configuration)
#
define_method level do |message = nil, error = nil, &block|
return unless message or block
add level, Message.new(message, error, &block)
add level, Message.new(ndc: @ndc.context.clone, message: message, error: error, &block)
end

# Public: Returns true if any of the appenders will log messages for the
Expand Down Expand Up @@ -193,7 +201,7 @@ def add(level, message)
# Ruby, the host itself when the host is a module, otherwise the object's
# class.
#
def context(host)
def host_name(host)
if host.inspect == 'main'
'main'
elsif [Module, Class].include? host.class
Expand Down
55 changes: 46 additions & 9 deletions lib/hatchet/message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ module Hatchet
# If an error is associated with the message this will be available via the
# #error attribute.
#
# The nested diagnostic context of the message will be availble via the #ndc
# attribute.
#
# Blocks will be lazily evaluated once for all appenders when required.
#
class Message
Expand All @@ -16,27 +19,61 @@ class Message
#
attr_reader :error

# Internal: Creates a new message.
# Public: Gets the nested diagnostic context values.
#
attr_reader :ndc

# Public: Creates a new message.
#
# args - The Hash used to provide context for the message (default: {}):
# :ndc - An Array of nested diagnostic context values
# (default: []).
# :message - An already evaluated message, usually a String
# (default: nil).
# :error - An error that is associated with the message
# (default: nil).
# block - An optional block which will provide a message when invoked.
#
# Examples
#
# Message.new(ndc: [], message: "Evaluated message", error: e)
# Message.new(ndc: %w{Foo Bar}) { "Lazily evaluated message" }
#
# The signature of the constructor was originally:
#
# message - An already evaluated message, usually a String (default: nil).
# error - An error that is associated with the message (default: nil).
# block - An optional block which will provide a message when invoked.
#
# One of message or block must be provided. If both are provided then the
# block is preferred as it is assumed to provide more detail.
# This format is also supported for compatibility to version 0.1.0 and below
# and will be deprecated in the future.
#
# Examples
#
# Message.new "Evaluated message"
# Message.new("Evaluated message", e)
# Message.new { "Lazily evaluated message" }
#
def initialize(message = nil, error = nil, &block)
@block = block
@error = error
@message = message unless @block
# One of message or block must be provided. If both are provided then the
# block is preferred as it is assumed to provide more detail.
#
def initialize(args = {}, error = nil, &block)
if args.kind_of? Hash
# If args is a Hash then using new constructor format or no parameters
# specified. Either way, use the new format.
@ndc = args[:ndc] || []
@error = args[:error]
@message = args[:message] unless block
else
# Otherwise assume the old format and coerce args accordingly.
@ndc = []
@error = error
@message = args unless block
end

@block = block
end

# Internal: Returns the String representation of the message.
# Public: Returns the String representation of the message.
#
def to_s
@message ||= @block.call
Expand Down
34 changes: 34 additions & 0 deletions lib/hatchet/middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module Hatchet

# Public: Middleware for making sure the nested diagnostic context is cleared
# between requests.
#
class Middleware
include Hatchet

# Public: Creates a new instance of the middleware, wrapping the given
# application.
#
# app - The application to wrap.
#
def initialize(app)
@app = app
end

# Public: Calls the wrapped application with the given environment, ensuring
# the nested diagnostic context is cleared afterwards.
#
# env - The enviroment Hash for the request.
#
# Returns the status, headers, body Array returned by the wrapped
# application.
#
def call(env)
@app.call(env)
ensure
log.ndc.clear!
end

end

end
160 changes: 160 additions & 0 deletions lib/hatchet/nested_diagnostic_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
module Hatchet

# Public: Class that manages the nested diagnostic context for a thread.
#
# All access to this class is performed through internal classes.
#
class NestedDiagnosticContext

# Internal: Gets the current context stack.
#
attr_reader :context

# Internal: Gets the NestedDiagnosticContext for the current thread, lazily
# initializing it as necessary.
#
def self.current
Thread.current[:hatchet_ndc] ||= NestedDiagnosticContext.new
end

# Internal: Creates a new instance of the class.
#
def initialize
clear!
end

# Public: Adds one or more messages onto the context stack.
#
# values - One or more messages to add to the context stack.
#
# Returns nothing.
#
def push(*values)
@context.push(*values)
end

# Public: Removes one or more message from the context stack.
#
# n - The number of messages to remove from the context stack (default:
# nil). If no number is provided then one message will be removed.
#
# Returns the message or messages removed from the context stack. If n was
# not specified it will return a single message, otherwise it will return an
# Array of up to n messages.
#
def pop(n = nil)
if n
@context.pop(n)
else
@context.pop
end
end

# Public: Adds one more or message onto the context stack for the scope of
# the given block.
#
# values - One or more messages to add to the context stack for the scope of
# the given block.
# block - The block to execute with the additional messages.
#
# Returns the result of calling the block.
#
def scope(*values, &block)
before = @context.clone
push(*values)
block.call
ensure
@context = before
end

# Public: Clears all messages from the context stack.
#
# Intend for use when the current thread is, or may, be reused in the future
# and the accumlated context is no longer wanted.
#
# Returns nothing.
#
def clear!
@context = ContextStack.new
nil
end

# Public: Class for holding the context stack of a NestedDiagnosticContext.
#
# Deliberately intended to have a similar API to Array to make testing
# easier.
#
class ContextStack

# Internal: Gets the internal stack.
#
attr_reader :stack

# Internal: Creates a new instance.
#
# stack - An Array of values to initialize the stack with (default: []).
#
def initialize(stack = [])
@stack = stack
end

# Public: Returns true if the stack contains any messages, otherwise
# returns false.
#
def any?
@stack.size != 0
end

# Internal: Returns a clone of the stack.
#
def clone
ContextStack.new(@stack.clone)
end

# Public: Returns a String created by converting each message of the stack
# to a String, separated by separator.
#
# separator - The String to separate the messages of the stack with
# (default: $,).
#
# Returns a String created by converting each message of the stack to a
# String, separated by separator.
#
def join(separator = $,)
@stack.join(separator)
end

# Internal: Pushes the given messages onto the stack.
#
# values - One or more messages to add to the context stack.
#
# Returns nothing.
#
def push(*values)
@stack.push(*values)
nil
end

# Internal: Removes one or more message from the stack.
#
# n - The number of messages to remove from the cstack (default: nil). If
# no number is provided then one message will be removed.
#
# Returns the message or messages removed from the context stack. If n was
# not specified it will return a single message, otherwise it will return
# an Array of up to n messages.
#
def pop(n = nil)
if n
@stack.pop(n)
else
@stack.pop
end
end

end

end

end

7 changes: 7 additions & 0 deletions lib/hatchet/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ class Railtie < Rails::Railtie
#
self.config.hatchet = Hatchet.configuration

# Add the Hatchet::Middleware to the middleware stack to enable nested
# diagnostic context clearance between requests.
#
initializer "hatchet_railtie.insert_middleware" do |app|
app.config.middleware.use Hatchet::Middleware
end

# Wrap the default Rails.logger, Rails.application.assets.logger, and all
# log subscribers found in ActiveSupport::LogSubscriber.log_subscribers
# collection on initialization.
Expand Down
Loading