File: profiler.rb

package info (click to toggle)
ruby-sentry-ruby 5.28.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 672 kB
  • sloc: ruby: 6,118; makefile: 8; sh: 4
file content (132 lines) | stat: -rw-r--r-- 3,243 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# frozen_string_literal: true

require "securerandom"
require_relative "../profiler/helpers"
require_relative "output"
require "sentry/utils/uuid"

module Sentry
  module Vernier
    class Profiler
      EMPTY_RESULT = {}.freeze

      attr_reader :started, :event_id, :result

      def initialize(configuration)
        @event_id = Utils.uuid

        @started = false
        @sampled = nil

        @profiling_enabled = defined?(Vernier) && configuration.profiling_enabled?
        @profiles_sample_rate = configuration.profiles_sample_rate
        @project_root = configuration.project_root
        @app_dirs_pattern = configuration.app_dirs_pattern
        @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}")
      end

      def set_initial_sample_decision(transaction_sampled)
        unless @profiling_enabled
          @sampled = false
          return
        end

        unless transaction_sampled
          @sampled = false
          log("Discarding profile because transaction not sampled")
          return
        end

        case @profiles_sample_rate
        when 0.0
          @sampled = false
          log("Discarding profile because sample_rate is 0")
          return
        when 1.0
          @sampled = true
          return
        else
          @sampled = Random.rand < @profiles_sample_rate
        end

        log("Discarding profile due to sampling decision") unless @sampled
      end

      def start
        return unless @sampled
        return if @started

        @started = ::Vernier.start_profile

        log("Started")

        @started
      rescue RuntimeError => e
        # TODO: once Vernier raises something more dedicated, we should catch that instead
        if e.message.include?("Profile already started")
          log("Not started since running elsewhere")
        else
          log("Failed to start: #{e.message}")
        end
      end

      def stop
        return unless @sampled
        return unless @started

        @result = ::Vernier.stop_profile
        @started = false

        log("Stopped")
      rescue RuntimeError => e
        if e.message.include?("Profile not started")
          log("Not stopped since not started")
        else
          log("Failed to stop Vernier: #{e.message}")
        end
      end

      def active_thread_id
        Thread.current.object_id
      end

      def to_hash
        unless @sampled
          record_lost_event(:sample_rate)
          return EMPTY_RESULT
        end

        return EMPTY_RESULT unless result

        { **profile_meta, profile: output.to_h }
      end

      private

      def log(message)
        Sentry.sdk_logger.debug(LOGGER_PROGNAME) { "[Profiler::Vernier] #{message}" }
      end

      def record_lost_event(reason)
        Sentry.get_current_client&.transport&.record_lost_event(reason, "profile")
      end

      def profile_meta
        {
          event_id: @event_id,
          version: "1",
          platform: "ruby"
        }
      end

      def output
        @output ||= Output.new(
          result,
          project_root: @project_root,
          app_dirs_pattern: @app_dirs_pattern,
          in_app_pattern: @in_app_pattern
        )
      end
    end
  end
end