lib/elastic_apm/transaction.rb



# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

# frozen_string_literal: true

module ElasticAPM
  # @api private
  class Transaction
    # @api private
    class Outcome
      FAILURE = "failure"
      SUCCESS = "success"
      UNKNOWN = "unknown"

      def self.from_http_status(code)
        code.to_i >= 500 ? FAILURE : SUCCESS
      end
    end

    extend Forwardable
    include ChildDurations::Methods

    def_delegators :@trace_context,
      :trace_id, :parent_id, :id, :ensure_parent_id

    DEFAULT_TYPE = 'custom'
    MUTEX = Mutex.new

    # rubocop:disable Metrics/ParameterLists
    def initialize(
      name = nil,
      type = nil,
      config:,
      sampled: true,
      sample_rate: 1,
      context: nil,
      trace_context: nil
    )
      @name = name
      @type = type || DEFAULT_TYPE
      @config = config

      # Cache these values in case they are changed during the
      # transaction's lifetime via the remote config
      @span_frames_min_duration = config.span_frames_min_duration
      @collect_metrics = config.collect_metrics?
      @breakdown_metrics = config.breakdown_metrics?
      @framework_name = config.framework_name
      @transaction_max_spans = config.transaction_max_spans
      @default_labels = config.default_labels

      @sampled = sampled
      @sample_rate = sample_rate

      @context = context || Context.new # TODO: Lazy generate this?
      if @default_labels
        Util.reverse_merge!(@context.labels, @default_labels)
      end

      unless (@trace_context = trace_context)
        @trace_context = TraceContext.new(
          traceparent: TraceContext::Traceparent.new(recorded: sampled),
          tracestate: TraceContext::Tracestate.new(
            sample_rate: sampled ? sample_rate : 0
          )
        )
      end

      @started_spans = 0
      @dropped_spans = 0

      @notifications = [] # for AS::Notifications
    end
    # rubocop:enable Metrics/ParameterLists

    attr_accessor :name, :type, :result, :outcome

    attr_reader(
      :breakdown_metrics,
      :collect_metrics,
      :context,
      :dropped_spans,
      :duration,
      :framework_name,
      :notifications,
      :self_time,
      :sample_rate,
      :span_frames_min_duration,
      :started_spans,
      :timestamp,
      :trace_context,
      :transaction_max_spans
    )

    alias :collect_metrics? :collect_metrics

    def sampled?
      @sampled
    end

    def stopped?
      !!duration
    end

    # life cycle

    def start(clock_start = Util.monotonic_micros)
      @timestamp = Util.micros
      @clock_start = clock_start
      self
    end

    def stop(clock_end = Util.monotonic_micros)
      raise 'Transaction not yet start' unless timestamp
      @duration = clock_end - @clock_start
      @self_time = @duration - child_durations.duration

      self
    end

    def done(result = nil, clock_end: Util.monotonic_micros)
      stop clock_end
      self.result = result if result
      self
    end

    # spans

    def inc_started_spans!
      MUTEX.synchronize do
        @started_spans += 1
        if @started_spans > transaction_max_spans
          @dropped_spans += 1
          return false
        end
      end
      true
    end

    # context

    def add_response(status = nil, **args)
      context.response = Context::Response.new(status, **args)
    end

    def set_user(user)
      context.user = Context::User.infer(@config, user)
    end

    def inspect
      "<ElasticAPM::Transaction id:#{id}" \
        " name:#{name.inspect} type:#{type.inspect}>"
    end
  end
end