solid-process/solid-result

View on GitHub
lib/solid/result/event_logs/tracking/enabled.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module Solid::Result::EventLogs
  class Tracking::Enabled
    attr_accessor :tree, :records, :root_started_at, :listener

    private :tree, :tree=, :records, :records=, :root_started_at, :root_started_at=, :listener, :listener=

    def exec(name, desc)
      event_log_node, scope = start(name, desc)

      result = nil

      listener.around_event_logs(scope: scope) do
        result = EnsureResult[yield]
      end

      tree.move_to_root! if event_log_node.root?

      finish(result)

      result
    rescue ::Exception => e
      err!(e, event_log_node)
    end

    def err!(exception, event_log_node)
      if event_log_node.root?
        listener.before_interruption(exception: exception, event_logs: map_event_logs)

        reset!
      end

      raise exception
    end

    def reset!
      self.tree = Tracking::EMPTY_TREE
    end

    def record(result)
      return if tree.frozen?

      track(result, time: ::Time.now.getutc)
    end

    def record_and_then(type_arg, arg)
      return yield if tree.frozen?

      type = type_arg.instance_of?(::Method) ? :method : type_arg

      current_and_then = { type: type, arg: arg }
      current_and_then[:method_name] = type_arg.name if type == :method

      tree.current.value[1] = current_and_then

      scope, and_then = tree.current_value

      result = nil

      listener.around_and_then(scope: scope, and_then: and_then) { result = yield }

      result
    end

    def reset_and_then!
      return if tree.frozen?

      tree.current.value[1] = Tracking::EMPTY_HASH
    end

    private

    def start(name, desc)
      name_and_desc = [name, desc]

      tree.frozen? ? root_start(name_and_desc) : tree.insert!(name_and_desc)

      scope = tree.current.value[0]

      listener.on_start(scope: scope)

      [tree.current, scope]
    end

    def finish(result)
      node = tree.current

      tree.move_up!

      return unless node.root?

      event_logs = map_event_logs

      result.send(:event_logs=, event_logs)

      listener.on_finish(event_logs: event_logs)

      reset!
    end

    TreeNodeValueNormalizer = ->(id, (nam, des)) { [{ id: id, name: nam, desc: des }, Tracking::EMPTY_HASH] }

    def root_start(name_and_desc)
      self.root_started_at = now_in_milliseconds

      self.listener = build_listener

      self.records = []

      self.tree = Tree.new(name_and_desc, normalizer: TreeNodeValueNormalizer)
    end

    def track(result, time:)
      record = track_record(result, time)

      records << record

      listener.on_record(record: record)

      record
    end

    def track_record(result, time)
      result_data = result.data.to_h
      result_data[:source] = result.send(:source)

      root, = tree.root_value
      parent, = tree.parent_value
      current, and_then = tree.current_value

      { root: root, parent: parent, current: current, result: result_data, and_then: and_then, time: time }
    end

    def now_in_milliseconds
      ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
    end

    def map_event_logs
      duration = (now_in_milliseconds - root_started_at)

      trace_id = Config.instance.trace_id.call

      ids = { tree: tree.ids, matrix: tree.ids_matrix, level_parent: tree.ids_level_parent }

      metadata = { duration: duration, trace_id: trace_id, ids: ids }

      { version: Tracking::VERSION, records: records, metadata: metadata }
    end

    def build_listener
      Config.instance.listener.new
    rescue ::StandardError => e
      err = "#{e.message} (#{e.class}); Backtrace: #{e.backtrace&.join(', ')}"

      warn("Fallback to #{Listener::Null} because registered listener raised an exception: #{err}")

      Listener::Null.new
    end
  end
end