lib/semantic_logger/formatters/new_relic_logs.rb



require "json"

begin
  require "newrelic_rpm"
rescue LoadError
  raise LoadError,
        'Gem newrelic_rpm is required for logging to New Relic. Please add the gem "newrelic_rpm" to your Gemfile.'
end

unless NewRelic::Agent.respond_to?(:linking_metadata)
  raise "NewRelic::Agent.linking_metadata is not defined. Please update newrelic_rpm gem version"
end

unless NewRelic::Agent::Tracer.respond_to?(:current_span_id)
  raise "NewRelic::Agent::Tracer.current_span_id is not defined. Please update newrelic_rpm gem version"
end

unless NewRelic::Agent::Tracer.respond_to?(:current_trace_id)
  raise "NewRelic::Agent::Tracer.current_trace_id is not defined. Please update newrelic_rpm gem version"
end

module SemanticLogger
  module Formatters
    # Formatter for reporting to NewRelic's Logger
    #
    # New Relic gracefully handles (and flattens) any JSON-based logs
    # We construct the JSON and pass it to New Relic for further processing.
    #
    # == Reference
    # * Logging specification
    #   * https://github.com/newrelic/newrelic-exporter-specs/tree/master/logging
    #
    # * Metadata APIs
    #   * https://www.rubydoc.info/gems/newrelic_rpm/NewRelic/Agent#linking_metadata-instance_method
    #   * https://www.rubydoc.info/gems/newrelic_rpm/NewRelic/Agent/Tracer#current_trace_id-class_method
    #   * https://www.rubydoc.info/gems/newrelic_rpm/NewRelic/Agent/Tracer#current_span_id-class_method
    #
    class NewRelicLogs < Raw
      def initialize(**args)
        args.delete(:time_key)
        args.delete(:time_format)

        super(time_key: :timestamp, time_format: :ms, **args)
      end

      def call(log, logger)
        hash = super

        result = {
          **newrelic_metadata,
          message:       hash[:message].to_s,
          tags:          hash[:tags],
          metric:        hash[:metric],
          metric_amount: hash[:metric_amount],
          environment:   hash[:environment],
          application:   hash[:application],
          payload:       hash[:payload],
          timestamp:     hash[:timestamp].to_i,
          logger:        {
            name: log.name
          },
          thread:        {
            name: log.thread_name.to_s
          }
        }.compact

        if hash[:duration_ms] || hash[:duration]
          result[:duration] = {
            ms:    hash[:duration_ms],
            human: hash[:duration]
          }.compact
        end

        if hash[:exception]
          result[:error] = {
            message: hash[:exception][:message],
            class:   hash[:exception][:name],
            stack:   hash[:exception][:stack_trace].join("\n")
          }
        end

        if hash[:file]
          result[:file] = {
            name: hash[:file]
          }
        end

        if hash[:line]
          result[:line] = {
            number: hash[:line].to_s
          }
        end

        # NOTE: Any named tags are merged directly into the result
        # unless there are conflicts with other keys. In that
        # case we clearly log this in the NR log entry so it can
        # be easily alerted on.
        if hash[:named_tags].is_a?(Hash)
          result_keys = result.keys.to_set
          named_tag_conflicts = []

          hash[:named_tags].each do |key, value|
            if result_keys.include?(key)
              named_tag_conflicts << key
            else
              result[key] = value
            end
          end

          result[:named_tag_conflicts] = named_tag_conflicts unless named_tag_conflicts.empty?
        end

        result
      end

      private

      # NOTE: This function will already include trace.id and span.id if they
      # are available so I believe the previous implementation of this is redundant
      # https://rubydoc.info/gems/newrelic_rpm/NewRelic/Agent#linking_metadata-instance_method
      def newrelic_metadata
        NewRelic::Agent.linking_metadata.transform_keys(&:to_sym)
      end
    end
  end
end