lib/semantic_logger/formatters/signalfx.rb



require "json"
module SemanticLogger
  module Formatters
    class Signalfx < Base
      attr_accessor :token, :dimensions, :hash, :gauge_name, :counter_name

      def initialize(token:,
                     dimensions: nil,
                     gauge_name: "Application.average",
                     counter_name: "Application.counter",
                     time_format: :ms,
                     **args)
        @token        = token
        @dimensions   = dimensions.map(&:to_sym) if dimensions
        @gauge_name   = gauge_name
        @counter_name = counter_name

        super(time_format: time_format, **args)
      end

      # Create SignalFx friendly metric.
      #   Strip leading '/'
      #   Convert remaining '/' to '.'
      def metric
        name = log.metric.to_s.sub(%r{\A/+}, "")
        if log.dimensions
          name.tr!("/", ".")
          hash[:metric] = name
        else
          # Extract class and action from metric name
          names = name.split("/")
          h     = (hash[:dimensions] ||= {})
          if names.size > 1
            h[:action] = names.pop
            h[:class]  = names.join("::")
          else
            h[:class]  = "Unknown"
            h[:action] = names.first || log.metric
          end

          hash[:metric] = log.duration ? gauge_name : counter_name
        end
      end

      # Date & time
      def time
        # 1 second resolution, represented as ms.
        hash[:timestamp] = log.time.to_i * 1000
      end

      # Value of this metric
      def value
        hash[:value] = log.metric_amount || log.duration || 1
      end

      # Dimensions for this metric
      def format_dimensions
        h = (hash[:dimensions] ||= {})
        if log.dimensions
          log.dimensions.each_pair do |name, value|
            value   = value.to_s
            h[name] = value unless value.empty?
          end
        else
          log.named_tags.each_pair do |name, value|
            name  = name.to_sym
            value = value.to_s
            next if value.empty?

            h[name] = value if dimensions&.include?(name)
          end
        end
        h[:host]        = logger.host if log_host && logger.host
        h[:application] = logger.application if log_application && logger.application
        h[:environment] = logger.environment if log_environment && logger.environment
      end

      # Returns [Hash] log message in Signalfx format.
      def call(log, logger)
        self.hash   = {}
        self.log    = log
        self.logger = logger

        metric
        time
        value
        format_dimensions

        # gauge, counter, or cumulative_counter
        data = {}
        if log.duration
          data[:gauge] = [hash]
          # Also send a count metric whenever it is a gauge so that it can be counted.
          unless log.dimensions
            count_hash          = hash.dup
            count_hash[:value]  = log.metric_amount || 1
            count_hash[:metric] = counter_name
            data[:counter]      = [count_hash]
          end
        else
          data[:counter] = [hash]
        end

        data.to_json
      end

      # Returns [Hash] a batch of log messages.
      # Signalfx has a minimum resolution of 1 second.
      # Metrics of the same type, time (second), and dimensions can be aggregated together.
      def batch(logs, logger)
        self.logger = logger

        data = {}
        logs.each do |log|
          self.hash = {}
          self.log  = log

          metric
          time
          value
          format_dimensions

          if log.duration
            gauges = (data[:gauge] ||= [])
            add_gauge(gauges, hash)

            # Also send a count metric whenever it is a gauge so that it can be counted.
            unless log.dimensions
              count_hash          = hash.dup
              count_hash[:value]  = log.metric_amount || 1
              count_hash[:metric] = counter_name
              counters            = (data[:counter] ||= [])
              add_counter(counters, count_hash)
            end
          else
            counters = (data[:counter] ||= [])
            add_counter(counters, hash)
          end
        end

        data.to_json
      end

      private

      def add_gauge(gauges, metric)
        gauges << metric
      end

      # Sum counters with the same time (second), name, and dimensions.
      def add_counter(counters, metric)
        existing = find_match(counters, metric)
        existing ? existing[:value] += metric[:value] : counters << metric
      end

      # Find Metrics with the same timestamp, metric name, and dimensions.
      def find_match(list, metric)
        list.find do |item|
          (item[:timestamp] == metric[:timestamp]) &&
            (item[:metric] == metric[:metric]) &&
            (item[:dimensions] == metric[:dimensions])
        end
      end
    end
  end
end