Observer

class
Superclass
Object
Extended With
Loggability

Constants

SNAKE_CASE_SEPARATOR

Pattern for finding places for underscores when changing a camel-cased string to a snake-cased one.

Attributes

sender[R]

The Observability::Sender used to deliver events

Public Class Methods

anchor
new( sender_type=nil )

Create a new Observer that will send events via the specified sender.

# File lib/observability/observer.rb, line 23
def initialize( sender_type=nil )
        @sender = self.configured_sender( sender_type )
        @event_stack = Concurrent::ThreadLocalVar.new( &Array.method(:new) )
        @context_stack = Concurrent::ThreadLocalVar.new( &Array.method(:new) )
end

Public Instance Methods

anchor
add( object=nil, **fields )

Add an object and/or a Hash of fields to the current event.

# File lib/observability/observer.rb, line 102
def add( object=nil, **fields )
        self.log.debug "Adding %p" % [ object || fields ]
        event = @event_stack.value.last or return

        if object
                object_fields = self.fields_from_object( object )
                fields = fields.merge( object_fields )
        end

        event.merge( fields )
end
anchor
add_context( object=nil, **fields )

Add the specified fields to the current event and any that are created before the current event is finished.

# File lib/observability/observer.rb, line 117
def add_context( object=nil, **fields )
        self.log.debug "Adding context from %p" % [ object || fields ]
        current_context = @context_stack.value.last or return

        if object
                object_fields = self.fields_from_object( object )
                fields = fields.merge( object_fields )
        end

        current_context.merge!( fields )
end
anchor
event( type, **options, &block )

Create a new event with the specified type and make it the current one.

# File lib/observability/observer.rb, line 52
def event( type, **options, &block )
        type = self.type_from_object( type )
        fields = self.fields_from_options( options )

        event = Observability::Event.new( type, **fields )
        @event_stack.value.push( event )

        new_context = @context_stack.value.last&.dup || {}
        @context_stack.value.push( new_context )

        return self.finish_after_block( event.object_id, &block ) if block
        return event.object_id
end
anchor
finish( event_marker=nil )

Finish the and send the current event, comparing it against event_marker

and raising an exception if it's provided but doesn't match.

:TODO: Instead of raising, dequeue events up to the given marker if it exists in the queue?

# File lib/observability/observer.rb, line 72
def finish( event_marker=nil )
        raise "Event mismatch" if
                event_marker && @event_stack.value.last.object_id != event_marker

        event = @event_stack.value.pop
        context = @context_stack.value.pop
        self.log.debug "Adding context %p (%d left) to finishing event." %
                [ context, @context_stack.value.length ]
        event.merge( context )

        self.log.debug "Finishing event: %p" % [ event ]
        self.sender.enqueue( event )

        return event
end
anchor
finish_after_block( event_marker=nil, &block )

Call the given block, then when it returns, finish the event that corresponds to the given marker.

# File lib/observability/observer.rb, line 91
def finish_after_block( event_marker=nil, &block )
        block.call( self )
rescue Exception => err
        self.add( err )
        raise
ensure
        self.finish( event_marker )
end
anchor
has_pending_events?()

Returns true if the Observer has events that are under construction.

# File lib/observability/observer.rb, line 137
def has_pending_events?
        return self.pending_event_count.nonzero? ? true : false
end
Also aliased as: pending_events?
anchor
pending_event_count()

Return the depth of the stack of pending events.

# File lib/observability/observer.rb, line 131
def pending_event_count
        return @event_stack.value.length
end
anchor
pending_events?()
Alias for: has_pending_events?
anchor
start()

Start recording events and sending them.

# File lib/observability/observer.rb, line 40
def start
        self.sender.start
end
anchor
stop()

Stop recording and sending events.

# File lib/observability/observer.rb, line 46
def stop
        self.sender.stop
end

Protected Instance Methods

anchor
configured_sender( sender_type )

Create an instance of the given sender_type, or the type specified in the configuration if sender_type is nil.

# File lib/observability/observer.rb, line 289
def configured_sender( sender_type )
        return Observability::Sender.create( sender_type ) if sender_type
        return Observability::Sender.configured_type
end
anchor
fields_from_exception( exception )

Return a Hash of fields to add to the current event derived from the given exception object.

# File lib/observability/observer.rb, line 272
def fields_from_exception( exception )
        trace_frames = exception.backtrace_locations.map do |loc|
                { label: loc.label, path: loc.absolute_path, lineno: loc.lineno }
        end

        return {
                error: {
                        type: exception.class.name,
                        message: exception.message,
                        backtrace: trace_frames
                }
        }
end
anchor
fields_from_object( object )

Return a Hash of fields to add to the current event derived from the given object.

# File lib/observability/observer.rb, line 256
def fields_from_object( object )
        case object
        when ::Exception
                return self.fields_from_exception( object )
        else
                if object.respond_to?( :to_h )
                        return object.to_h
                else
                        raise "don't know how to derive fields from a %p" % [ object.class ]
                end
        end
end
anchor
fields_from_options( options )

Extract fields specified by the specified options and return them all merged into one Hash. :TODO: Handle options like :model, :timed, etc.

# File lib/observability/observer.rb, line 234
def fields_from_options( options )
        fields = {}

        options.each do |key, value|
                self.log.debug "Applying option %p: %p" % [ key, value ]
                case key
                when :add
                        fields.merge!( value )
                when :timed
                        duration_callback = lambda {|ev| Concurrent.monotonic_time - ev.start }
                        fields.merge!( duration: duration_callback )
                else
                        raise "unknown event option %p" % [ key ]
                end
        end

        return fields
end
anchor
type_from_context( context, *details )

Derive an event type from the specified Method and a detail.

# File lib/observability/observer.rb, line 207
def type_from_context( context, *details )
        prefix = self.type_from_object( context )
        suffix = details.map {|detail| detail.to_s }

        return ([prefix] + suffix).join( '.' )
end
anchor
type_from_context_proc( object )

Derive an event type from the specified Proc, which should be a block passed to an observed method.

# File lib/observability/observer.rb, line 217
def type_from_context_proc( object )
        bind = object.binding
        recv = bind.receiver
        methname = bind.eval( "__method__" )
        meth = recv.method( methname )

        return self.type_from_object( meth )
end
anchor
type_from_method( object )

Derive an event type from the specified Method or UnboundMethod object.

# File lib/observability/observer.rb, line 198
def type_from_method( object )
        name = object.original_name
        mod = object.owner

        return [ self.type_from_module(mod), name.to_s ].join( '.' )
end
anchor
type_from_module( object )

Derive an event type from the specified Class or Module object.

# File lib/observability/observer.rb, line 182
def type_from_module( object )
        if ( name = object.name )
                name = name.split( '::' ).collect do |part|
                        part.gsub( SNAKE_CASE_SEPARATOR ) do |m|
                                "%s_%s" % [ m[0], m[1] ]
                        end
                end.join( '.' )

                return name.downcase
        else
                return "anonymous_%s_%d" % [ object.class.name.downcase, object.object_id ]
        end
end
anchor
type_from_object( object, block=nil )

Derive an event type from the specified object and an optional block that provides additional context.

# File lib/observability/observer.rb, line 153
def type_from_object( object, block=nil )
        case object
        when Module
                self.log.debug "Deriving a type from module %p" % [ object ]
                return self.type_from_module( object )

        when Method, UnboundMethod
                self.log.debug "Deriving a type from method %p" % [ object ]
                return self.type_from_method( object )

        when Array
                self.log.debug "Deriving a type from context %p" % [ object ]
                return self.type_from_context( *object )

        when Proc
                self.log.debug "Deriving a type from context proc %p" % [ object ]
                return self.type_from_context_proc( object )

        when String
                self.log.debug "Using string %p as type" % [ object ]
                return object

        else
                raise "don't know how to derive an event type from a %p" % [ object.class ]
        end
end