appsignal/appsignal

View on GitHub
lib/appsignal/minutely.rb

Summary

Maintainability
A
35 mins
Test Coverage
# frozen_string_literal: true

module Appsignal
  class Minutely
    class ProbeCollection
      include Appsignal::Utils::DeprecationMessage

      def initialize
        @probes = {}
      end

      # @return [Integer] Number of probes that are registered.
      def count
        probes.count
      end

      # Clears all probes from the list.
      # @return [void]
      def clear
        probes.clear
      end

      # Fetch a probe using its name.
      # @param key [Symbol/String] The name of the probe to fetch.
      # @return [Object] Returns the registered probe.
      def [](key)
        probes[key]
      end

      # @param probe [Object] Any object that listens to the `call` method will
      #   be used as a probe.
      # @deprecated Use {#register} instead.
      # @return [void]
      def <<(probe)
        deprecation_message "Deprecated `Appsignal::Minute.probes <<` " \
          "call. Please use `Appsignal::Minutely.probes.register` instead."
        register probe.object_id, probe
      end

      # Register a new minutely probe.
      #
      # Supported probe types are:
      #
      # - Lambda - A lambda is an object that listens to a `call` method call.
      #   This `call` method is called every minute.
      # - Class - A class object is an object that listens to a `new` and
      #   `call` method call. The `new` method is called when the Minutely
      #   probe thread is started to initialize all probes. This allows probes
      #   to load dependencies once beforehand. Their `call` method is called
      #   every minute.
      # - Class instance - A class instance object is an object that listens to
      #   a `call` method call. The `call` method is called every minute.
      #
      # @example Register a new probe
      #   Appsignal::Minutely.probes.register :my_probe, lambda {}
      #
      # @example Overwrite an existing registered probe
      #   Appsignal::Minutely.probes.register :my_probe, lambda {}
      #   Appsignal::Minutely.probes.register :my_probe, lambda { puts "hello" }
      #
      # @example Add a lambda as a probe
      #   Appsignal::Minutely.probes.register :my_probe, lambda { puts "hello" }
      #   # "hello" # printed every minute
      #
      # @example Add a probe instance
      #   class MyProbe
      #     def initialize
      #       puts "started"
      #     end
      #
      #     def call
      #       puts "called"
      #     end
      #   end
      #
      #   Appsignal::Minutely.probes.register :my_probe, MyProbe.new
      #   # "started" # printed immediately
      #   # "called" # printed every minute
      #
      # @example Add a probe class
      #   class MyProbe
      #     def initialize
      #       # Add things that only need to be done on start up for this probe
      #       require "some/library/dependency"
      #       @cache = {} # initialize a local cache variable
      #       puts "started"
      #     end
      #
      #     def call
      #       puts "called"
      #     end
      #   end
      #
      #   Appsignal::Minutely.probes.register :my_probe, MyProbe
      #   Appsignal::Minutely.start # This is called for you
      #   # "started" # Printed on Appsignal::Minutely.start
      #   # "called" # Repeated every minute
      #
      # @param name [Symbol/String] Name of the probe. Can be used with {[]}.
      #   This name will be used in errors in the log and allows overwriting of
      #   probes by registering new ones with the same name.
      # @param probe [Object] Any object that listens to the `call` method will
      #   be used as a probe.
      # @return [void]
      def register(name, probe)
        if probes.key?(name)
          logger.debug "A probe with the name `#{name}` is already " \
            "registered. Overwriting the entry with the new probe."
        end
        probes[name] = probe
      end

      # @api private
      def each(&block)
        probes.each(&block)
      end

      private

      attr_reader :probes

      def logger
        Appsignal.logger
      end
    end

    class << self
      # @see ProbeCollection
      # @return [ProbeCollection] Returns list of probes.
      def probes
        @probes ||= ProbeCollection.new
      end

      # @api private
      def start
        stop
        @thread = Thread.new do
          sleep initial_wait_time
          initialize_probes
          loop do
            logger = Appsignal.logger
            logger.debug("Gathering minutely metrics with #{probe_instances.count} probes")
            probe_instances.each do |name, probe|
              begin
                logger.debug("Gathering minutely metrics with '#{name}' probe")
                probe.call
              rescue => ex
                logger.error "Error in minutely probe '#{name}': #{ex}"
                logger.debug ex.backtrace.join("\n")
              end
            end
            sleep wait_time
          end
        end
      end

      # @api private
      def stop
        defined?(@thread) && @thread.kill
        probe_instances.clear
      end

      # @api private
      def wait_time
        60 - Time.now.sec
      end

      private

      def initial_wait_time
        remaining_seconds = 60 - Time.now.sec
        return remaining_seconds if remaining_seconds > 30
        remaining_seconds + 60
      end

      def initialize_probes
        probes.each do |name, probe|
          initialize_probe(name, probe)
        end
      end

      def initialize_probe(name, probe)
        if probe.respond_to? :new
          instance = probe.new
          klass = probe
        else
          instance = probe
          klass = instance.class
        end
        unless dependencies_present?(klass)
          Appsignal.logger.debug "Skipping '#{name}' probe, " \
            "#{klass}.dependency_present? returned falsy"
          return
        end
        probe_instances[name] = instance
      rescue => error
        logger = Appsignal.logger
        logger.error "Error while initializing minutely probe '#{name}': #{error}"
        logger.debug error.backtrace.join("\n")
      end

      def dependencies_present?(probe)
        return true unless probe.respond_to? :dependencies_present?
        probe.dependencies_present?
      end

      def probe_instances
        @probe_instances ||= {}
      end
    end
  end
end