leucos/ramaze-fnordmetric

View on GitHub
lib/ramaze/helper/fnordmetric.rb

Summary

Maintainability
A
0 mins
Test Coverage
# encode: utf-8
#
# FNordmetric helper
#
# 
require 'ramaze'
require 'fnordmetric'
require 'redis'

module Ramaze
  module Helper
    ##
    # This helper provides a convenience wrapper for sending events to 
    # Fnordmetric.
    #
    # events can be anything, its just an indication that something happened.
    # Fnordmetric can then make some agregates on events received per period.
    #
    # Since events can carry arbitrary data, this helper adds methods that send
    # performance data to Fnordmetric, so one can easily measure code execution
    # times.
    #
    # events are associated to the Innate session id, and thus are linked to 
    # visitors of your site. this is really usefull since you can, for instance,
    # see how long a controller action took for a particular user.
    #
    # If you want so use a redis server other than the usual localhost:6379, you
    # need to define :fnord_redis_url trait, e.g. :
    #
    #   trait :fnord_redis_url => "redis://redis.example.com:6332"
    #
    # TODO: @example Basic usage here...
    # TODO: Implement optional with_id that uses specific id instead of innate.sid in conjunction with...
    # TODO: simple keys instead of list, for the above
    #
    module Fnordmetric
      # @@fnord will hold Fnordmetric API instance
      # @@redis holds a Redis connection
      # A timer is an Array holding the event name, a Hash of arguments and a timestamp
      @@fnord           = nil
      @@redis           = nil
      @@sstack_key_root = "fnordmetric.%s.%s.%s" % [ ENV['HOSTNAME'] || "localhost", ENV['USER'], Ramaze.options.app.name.to_s ]

      ##
      # We need clock as a class method
      # Let's extend the includer when it includes us
      def self.included(base)
        Ramaze::Log.debug("Fnordmetric helper is being included in %s" % base.name)
        base.extend(ClassMethods)
      end

      ##
      # Sends an event to Fnordmetric
      # 
      # This helper method sends an event to Fnordmetric, populating the 
      # :_session field with the current innate sid.
      #
      # @param  [Symbol] evt the name of the event to send to Fnordmetric.
      # @param  [Hash] args a hash of supplemental data to send
      #
      def event(evt, args = {})
        # Let's connect first, it will have to be done anyway
        return unless evt
        _connect unless @@fnord

        evt = { :_type => evt.to_s, :_session => session.sid }

        evt.merge!(args)
        
        Ramaze::Log.debug("Logging Fnordmetric event %s" % evt.inspect)        
        @@fnord.event(evt)
      end

      ##
      # All in one timing function for a block
      #
      # This method will send an event containing the execution time of
      # the passed block.
      #
      # @example Block style usage
      #
      #   times(:performance, :method => :whatever) do
      #     # do stuff to be measured
      #   end
      #
      # @param  [Symbol] event_name the name of the event to send to Fnordmetric.
      # @param  [Hash] args a hash of supplemental data to send
      # @param  [Block] block code to be executed and timed
      #
      def times(event_name, args = {}, &block) 
        push_timer(event_name, args)
        # THINK: may be raise since there is no point in using times without a
        # block
        yield if block_given?
        ensure
          pop_timer
      end

      ##
      # Starts a timer and pushes it to the timers stack
      #
      # @param  [Symbol] event_name the name of the event to send to Fnordmetric.
      # @param  [Hash] args a hash of supplemental data to send
      #
      # @example Push/Pop style usage
      #
      #   push_timer(:performance, :field => :whatever)
      #   # some code
      #   pop_timer
      #
      def push_timer(event_name, args = {})
        _connect unless @@redis
        @@redis.lpush(_key, [event_name, args, Time.now.to_f].to_json)
        @@redis.expire(_key, _ttl)
        Ramaze::Log.debug("Timer pushed and TTL set to %s for %s to %s (stack level is now %s)" % 
          [ _ttl,
            event_name, 
            _key,
            @@redis.llen(_key) ])
      end

      ##
      # Pops a timer and sends an event
      #
      # This method pops the last pushed timer and sends an event
      # No arguments are needed, since they were stored by push_timer
      #
      def pop_timer
        len = @@redis.llen(_key)
        if len > 0
          json = @@redis.lpop(_key)

          wat, args, wen = JSON.parse(json)
          Ramaze::Log.debug("Timer popped for %s (stack level is now %s)" % [ wat, len - 1])
          # We log millisecs
          time = Time.now-Time.at(wen)
          time *= 1000
          event(wat, args.merge(:time => time.to_i))
        else
          Ramaze::Log.error("Unable to pop timer in %s (no event in stack)" % _key)
          #raise RuntimeError, "Unable to pop timer in %s (no event in stack)" % _key
        end
      end

      ## 
      # Removes all timers in the stack
      #
      def clear_timers
        Ramaze::Log.debug("Cleared %s timers for %s" % [ @@redis.llen(_key), _key ])
        @@redis.del _key
      end

      ##
      # Sends a _pageview Fnordmetric event
      #
      # This method sends a specific _pageview event  Fnordmetric event
      # This event is treated in a special way by Fnordmetric (see doc).
      # 
      # @param [String] url the URl that is accessed. Defaults to request.env['REQUEST_PATH']
      #
      # @example Logging all page views
      #
      # If all your controllers inherit 'Controller', you can log all page view
      # very easily :
      #
      #   class Controller < Ramaze::Controller
      #     helper :fnordmetric
      #
      #     before_all do
      #       pageview
      #     end
      #
      def pageview(url=request.env['REQUEST_PATH'])
        event(:_pageview, :url => url)
      end

      ##
      # Sets username for the current session
      #
      # This manually sets a user name for the current session. It calls the 
      # specific :_set_name Fnordmetric event
      # This comes handy for user tracking
      #
      # @params [String] name the user name
      #
      def set_name(name)
        event(:_set_name, :name => name)
      end

      ##
      # Sets the picture URL for the user
      #
      # This manually sets a user picture for the current session. It calls the 
      # specific :_set_picture Fnordmetric event.
      # Using this method, you'll be able to have a picture associated to the user
      # in Fnordmetric's user tracking panel 
      # 
      # @param [String] url Picture url
      #
      # @example Using Gravatar to set user picture
      #
      #
      #   class Users < Controller
      #     helper :user, :gravatar, :fnordmetric     
      #     ...
      #     def login
      #       ...
      #       redirect_referrer if logged_in?
      #       user_login(request.subset(:email, :password))
      #       if logged_in?
      #         set_name("#{user.name} #{user.surname}")
      #         set_picture(gravatar(user.email.to_s)) if user.email 
      #       end 
      #       ...
      #     end
      #
      def set_picture(url="http://placekitten.com/80/80")
        url = url.to_s if url.class.to_s == 'URI::HTTP'
        event(:_set_picture, :url => url)
      end

      private 

      ##
      # Creates a fnordmetric instance, holding the Redis connection
      #
      #:nocov:
      def _connect 
        Ramaze::Log.debug("In connect")
        begin
          url = ancestral_trait[:fnord_redis_url]
        rescue
          url = "redis://localhost:6379"
        ensure
          @@fnord = FnordMetric::API.new(:redis_url => url)
          @@redis = Redis.new(:url => url)
          Ramaze::Log.debug("Connected to FnordMetric")
        end
      end
      #:nocov:
      
      ##
      # Returns the Redis key
      #
      def _key
        "%s.%s" % [ @@sstack_key_root, session.sid ]
      end

      ##
      # Returns the ttl to use for internal keys
      #
      def _ttl
        ancestral_trait[:fnord_helper_key_ttl] || Innate::Session.options.ttl
      end

      ##
      # Holds class methods
      # 
      # This is used to extend the calling controller so these methods are 
      # available at the class level
      # Since helpers are only included, extending the calling controller is 
      # done via the 'included' hook.
      # 
      module ClassMethods
        ##
        # This method replaces the original controller method with a times
        # call that yields the original method.
        # This allows to measure execution time for the method without manually
        # modifying the method itself
        #
        # @param [Symbol] method the method measure
        # @param [Symbol] event_name the name of the event to send to Fnordmetric.
        # @param [Hash] args a hash of supplemental data to send
        #
        # @example Measuring execution time for a controller action
        #
        #   class Users < Controller
        #       helper :user, :gravatar, :fnordmetric     
        #     ...
        #     def login
        #       ...
        #       # whatever login does
        #       ...
        #     end
        #     clock :login, :performance, :some_field => "some value"
        #
        def clock(method, event_name, args = {})
          # Let's alias the original controller method to original_name
          original = "__fnordmetric_%s" % method

          # We merge the method name in the args that will be sent in the event
          args.merge!(:method => "#{self.name}##{method}")

          self.class_eval do 
            # Let's create a shiny new method replacing the old one
            alias_method original, method
            private  original
            define_method(method) { |*a| times(event_name, args) do send(original, *a) end }
          end
          Ramaze::Log.debug("Clo(a)cking enabled for %s (renamed as %s)" % [ method, original ])
        end
      end

    end
  end
end