codebeacon-tracer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ require_relative 'call_tree'
2
+
3
+ module Codebeacon
4
+ module Tracer
5
+ class ThreadLocalCallTreeManager
6
+ attr_reader :trees, :trace_id
7
+
8
+ def initialize(trace_id)
9
+ @trees = []
10
+ @trace_id = trace_id
11
+ end
12
+
13
+ def current()
14
+ Thread.current[thread_key] ||= begin
15
+ CallTree.new(Thread.current).tap do |tree|
16
+ Codebeacon::Tracer.logger.debug("Creating new call tree for thread: #{Thread.current} with trace_id: #{trace_id}")
17
+ @trees << tree
18
+ end
19
+ end
20
+ end
21
+
22
+ def cleanup()
23
+ Thread.list.each do |thread|
24
+ if thread[thread_key]
25
+ thread[thread_key] = nil
26
+ end
27
+ end
28
+ end
29
+
30
+ private def thread_key()
31
+ "call_tree_#{trace_id}".to_sym
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,53 @@
1
+ module Codebeacon
2
+ module Tracer
3
+ class TPKlass
4
+ def initialize(tp)
5
+ @tp = tp
6
+ end
7
+
8
+ def tp_class
9
+ klass = @tp.self.class
10
+ while klass && klass.to_s =~ /Class:0x/
11
+ klass = klass.superclass
12
+ end
13
+ klass
14
+ end
15
+
16
+ def defined_class
17
+ klass = @tp.defined_class.to_s.sub("#<", "").sub(">", "")
18
+ if klass.match(/^(Class|Module):/)
19
+ klass = klass.split(":")[1..].join(":")
20
+ elsif klass.match(/:0x[0-9a-f]+$/)
21
+ klass = klass.split(":")[0..-2].join(":")
22
+ klass += " Singleton"
23
+ end
24
+ klass
25
+ end
26
+
27
+ def tp_class_name
28
+ if @tp.self.is_a?(Module)
29
+ @tp.self.name
30
+ else
31
+ klass = @tp.self.class
32
+ while klass && klass.to_s =~ /Class:0x/
33
+ klass = klass.superclass
34
+ end
35
+ klass.name
36
+ end
37
+ end
38
+
39
+ def type
40
+ case @tp.self
41
+ when Class
42
+ :Class
43
+ when Module
44
+ :Module
45
+ when Object
46
+ :Object
47
+ else
48
+ :Unknown
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,113 @@
1
+ require 'set'
2
+
3
+ # store node memory in a format that can be directly loaded 1:1 into sqlite3
4
+ # could use rocksdb or something else
5
+ # could use lmdb
6
+ # could use leveldb
7
+ # actually log if this speeds up developer time - would be really cool to show!
8
+ module Codebeacon
9
+ module Tracer
10
+ class TreeNode
11
+ class TraceStatus < Struct.new(:previous_line)
12
+ def any_lines_traced?
13
+ !previous_line.nil?
14
+ end
15
+ end
16
+ # attr_accessor :file, :line, :method, :depth, :caller, :gem_entry, :children, :parent, :block, :locals, :ast, :return_value, :linevars
17
+
18
+ # def initialize(file: nil, line: nil, method: nil, depth: 0, caller: "", gem_entry: false, parent: nil, block: false, locals: [], ast: nil, return_value: "")
19
+ # @file = file
20
+ # @line = line
21
+ # @method = method
22
+ # @depth = depth
23
+ # @caller = caller
24
+ # @gem_entry = gem_entry
25
+ # @parent = parent
26
+ # @block = block
27
+ # @locals = locals
28
+ # @ast = ast
29
+ # @return_value = return_value
30
+ # @linevars = {}
31
+ # @children = []
32
+ # end
33
+
34
+ attr_accessor :file, :line, :method, :object_id, :tp_class, :tp_defined_class, :tp_class_name, :self_type, :depth, :caller, :gem_entry, :children, :parent, :block, :locals, :return_value, :linevars, :node_source, :trace_status, :script, :backtrace_count, :backtrace_location, :script_binding, :script_self
35
+
36
+ def initialize(file: nil, line: nil, object_id: nil, method: nil, tp_class: nil, tp_defined_class: nil, tp_class_name: nil, self_type: nil, depth: 0, caller: "", gem_entry: false, parent: nil, block: false, locals: [], return_value: nil, node_source: nil, script: false)
37
+ @file = file
38
+ @line = line
39
+ @method = method
40
+ @object_id = object_id
41
+ @tp_class = tp_class
42
+ @tp_defined_class = tp_defined_class
43
+ @tp_class_name = tp_class_name
44
+ @self_type = self_type
45
+ @children = []
46
+ @depth = depth
47
+ @gem_entry = gem_entry
48
+ @caller = caller
49
+ @parent = parent
50
+ @block = block
51
+ @locals = locals
52
+ @return_value = return_value
53
+ @linevars = Hash.new { |h, k| h[k] = {} }
54
+ @node_source = node_source
55
+ @trace_status = TraceStatus.new(nil)
56
+ @script = script
57
+ @backtrace_count = 0
58
+ @backtrace_location = nil
59
+ @script_binding = nil
60
+ @script_self = nil
61
+ end
62
+
63
+ def add_line(lineno, variables)
64
+ @linevars[lineno] = @linevars[lineno].merge(variables)
65
+ end
66
+
67
+ def set_args(lineno, variables)
68
+ @linevars[lineno] = @linevars[lineno].merge(variables)
69
+ end
70
+
71
+ def inspect
72
+ ivar_inspect = instance_variables.reject { |ivar| [:@children].include?(ivar) }.map do |ivar|
73
+ "#{ivar.to_s}=#{instance_variable_get(ivar).inspect}"
74
+ end
75
+ ivar_inspect << "@children=#<#{@children.map(&:to_s)}>"
76
+ "#<#{self.class.name}:0x#{self.object_id.to_s} #{ivar_inspect.join(', ')}>"
77
+ end
78
+
79
+ def inspect_tree(attrs = [], depth = 0)
80
+ str = file.split("/").last + ":#{script ? "script" : method}"
81
+ if !attrs.empty?
82
+ attr_values = attrs.map { |k, v| "#{k}=#{v.inspect}" }.join(', ')
83
+ str += " " + attrs
84
+ end
85
+ str += children.map { |c| "\n" + " " * (depth + 1) * 2 + c.inspect_tree(attrs, depth + 1) }.join()
86
+ return str
87
+ end
88
+
89
+ def to_h
90
+ children = depth > Codebeacon::Tracer.config.max_depth ? nil : @children.map(&:to_h)
91
+ is_truncated = depth > Codebeacon::Tracer.config.max_depth ? true : false
92
+ {
93
+ file: @file,
94
+ line: @line,
95
+ method: @method,
96
+ class: @tp_class,
97
+ tp_defined_class: @tp_defined_class,
98
+ tp_class_name: @tp_class_name,
99
+ class_name: @class_name,
100
+ self_type: @self_type,
101
+ gemEntry: @gem_entry,
102
+ caller: @caller,
103
+ isDepthTruncated: is_truncated,
104
+ children: children
105
+ }
106
+ end
107
+
108
+ def depth_truncated?
109
+ @depth > Codebeacon::Tracer.config.max_depth && @children.count > 0
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,47 @@
1
+ module Codebeacon
2
+ module Tracer
3
+ class Middleware
4
+ def initialize(app)
5
+ @app = app
6
+ @first_run = true
7
+ end
8
+
9
+ def call(env)
10
+ response = nil
11
+ # tracer = Tracer.news
12
+ begin
13
+ Codebeacon::Tracer.config.set_query_config(env['QUERY_STRING'])
14
+ if !@first_run #Codebeacon::Tracer.config.trace_enabled? && !@first_run
15
+ Codebeacon::Tracer.trace do |tracer|
16
+ dry_run_log = Codebeacon::Tracer.config.dry_run? ? "--DRY RUN-- " : ""
17
+ Codebeacon::Tracer.logger.info(dry_run_log + "Tracing enabled for URI=#{env['REQUEST_URI']}")
18
+ response = @app.call(env).tap do |_|
19
+ Codebeacon::Tracer.logger.info("Tracing disabled for URI=#{env['REQUEST_URI']}")
20
+ end
21
+ begin
22
+ params = env['action_dispatch.request.parameters'].dup
23
+ tracer.name = "#{params.delete('controller')}##{params.delete('action')}"
24
+ tracer.description = params.to_json
25
+ rescue => e
26
+ Codebeacon::Tracer.logger.error("Error setting tracer metadata: #{e.message}")
27
+ end
28
+ response
29
+ end
30
+ else
31
+ if Codebeacon::Tracer.config.trace_enabled? && @first_run
32
+ Codebeacon::Tracer.logger.info("Bypassing first request for performance.")
33
+ end
34
+ @first_run = false if @first_run
35
+ response = @app.call(env)
36
+ end
37
+ rescue => e
38
+ Codebeacon::Tracer.logger.error("Error in middleware: #{e.message}")
39
+ Codebeacon::Tracer.logger.error(e.backtrace.join("\n")) if Codebeacon::Tracer.config.debug?
40
+ # Ensure the request is processed even if tracing fails
41
+ response = @app.call(env) if response.nil?
42
+ end
43
+ response
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,10 @@
1
+ $first_run = true
2
+ module Codebeacon
3
+ module Tracer
4
+ class Railtie < Rails::Railtie
5
+ initializer "codebeacon_tracer.middleware" do |app|
6
+ app.middleware.use Codebeacon::Tracer::Middleware
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,113 @@
1
+ require_relative 'models/node_builder'
2
+ require_relative 'models/thread_local_call_tree_manager'
3
+
4
+ module Codebeacon
5
+ module Tracer
6
+ class Tracer
7
+ attr_reader :id, :tree_manager
8
+ attr_accessor :name, :description
9
+
10
+ def initialize(name = nil, description = nil)
11
+ @progress_logger = Codebeacon::Tracer.logger.newProgressLogger("calls traced")
12
+ @traces = [trace_call, trace_b_call, trace_return, trace_b_return]
13
+ @name = name
14
+ @description = description
15
+ @trace_id = SecureRandom.uuid
16
+ @tree_manager = ThreadLocalCallTreeManager.new(@trace_id)
17
+ end
18
+
19
+ def id()
20
+ @trace_id
21
+ end
22
+
23
+ def call_tree()
24
+ @tree_manager.current()
25
+ end
26
+
27
+ def start()
28
+ @progress_logger = Codebeacon::Tracer.logger.newProgressLogger("calls traced")
29
+ start_traces
30
+ end
31
+
32
+ def stop()
33
+ stop_traces
34
+ @progress_logger.finish()
35
+ end
36
+
37
+ def cleanup()
38
+ @tree_manager.cleanup
39
+ end
40
+
41
+ def start_traces
42
+ dry_run_log = Codebeacon::Tracer.config.dry_run? ? "--DRY RUN-- " : ""
43
+ Codebeacon::Tracer.logger.info("#{dry_run_log}Starting trace: #{id}")
44
+ @traces.each do |trace|
45
+ trace.enable
46
+ end
47
+ end
48
+
49
+ def stop_traces
50
+ @traces.each do |trace|
51
+ trace.disable
52
+ end
53
+ Codebeacon::Tracer.logger.info("END tracing")
54
+ end
55
+
56
+ def enable_traces
57
+ start
58
+ return yield
59
+ ensure
60
+ stop
61
+ end
62
+
63
+ def trace_call
64
+ trace(:call) do |tp|
65
+ NodeBuilder.trace_method_call(call_tree, tp, Kernel.caller[2..])
66
+ ensure
67
+ @progress_logger.increment()
68
+ end
69
+ end
70
+
71
+ def trace_b_call
72
+ trace(:b_call) do |tp|
73
+ NodeBuilder.trace_block_call(call_tree, tp, Kernel.caller[2..])
74
+ ensure
75
+ @progress_logger.increment()
76
+ end
77
+ end
78
+
79
+ def trace_return
80
+ trace(:return) do |tp|
81
+ NodeBuilder.trace_return(call_tree, tp)
82
+ end
83
+ end
84
+
85
+ def trace_b_return
86
+ trace(:b_return) do |tp|
87
+ NodeBuilder.trace_return(call_tree, tp)
88
+ end
89
+ end
90
+
91
+ def trace(type)
92
+ TracePoint.new(type) do |tp|
93
+ paths = [tp.path]
94
+ # capture calls and returns to skipped paths from non skipped paths. All I need is the return value to display in recorded files, but the code doesn't yet support this without tracing the entire call and return.
95
+ if [:call, :b_call, :return, :b_return].include?(type)
96
+ paths << Kernel.caller(1..1)[0]
97
+ end
98
+ next if skip_methods?(paths)
99
+ yield tp
100
+ rescue => e
101
+ Codebeacon::Tracer.logger.error("TracePoint(#{type}) #{tp.path} #{e.message}")
102
+ end
103
+ end
104
+
105
+ def skip_methods?(paths)
106
+ paths.all? do |path|
107
+ path.nil? || Codebeacon::Tracer.config.exclude_paths.any?{ |exclude_path| path.start_with?(exclude_path) } ||
108
+ Codebeacon::Tracer.config.local_methods_only? && !path.start_with?(Codebeacon::Tracer.config.root_path)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Codebeacon
4
+ module Tracer
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+ require_relative "codebeacon/tracer/version"
5
+ require 'securerandom'
6
+
7
+ # Load all the source files
8
+ Dir[File.join(File.dirname(__FILE__), 'codebeacon', 'tracer', 'src', '*.rb')].each { |file| require file }
9
+ Dir[File.join(File.dirname(__FILE__), 'codebeacon', 'tracer', 'src', 'models', '*.rb')].each { |file| require file }
10
+ Dir[File.join(File.dirname(__FILE__), 'codebeacon', 'tracer', 'src', 'data', '*.rb')].each { |file| require file }
11
+ Dir[File.join(File.dirname(__FILE__), 'codebeacon', 'tracer', 'src', 'rails', '*.rb')].each { |file| require file } if defined?(Rails::Railtie)
12
+
13
+ module Codebeacon
14
+ # The Tracer module provides tools to trace and analyze the runtime performance
15
+ # of your Ruby applications. It captures method calls, execution times, and generates
16
+ # reports to help identify bottlenecks.
17
+ #
18
+ # @example Tracing a block of code
19
+ # Codebeacon::Tracer.trace("My Trace", "Description of what I'm tracing") do |tracer|
20
+ # # Your code to analyze goes here
21
+ # some_method_to_analyze
22
+ # end
23
+ #
24
+ module Tracer
25
+ class << self
26
+ # @return [ThreadLocalCallTreeManager] The current tree manager
27
+ attr_reader :tree_manager
28
+
29
+ # Returns the configuration object for Codebeacon::Tracer
30
+ # @return [Configuration] The configuration object
31
+ def config
32
+ @config ||= Configuration.new
33
+ end
34
+
35
+ # Returns the current call tree
36
+ # @return [CallTree] The current call tree
37
+ def current_tree
38
+ @tracer&.tree_manager&.current()
39
+ end
40
+
41
+ # Returns the logger instance
42
+ # @return [Logger] The logger instance
43
+ def logger
44
+ config.logger
45
+ end
46
+
47
+ # Traces a block of code and collects runtime information
48
+ #
49
+ # @param name [String, nil] Optional name for the trace
50
+ # @param description [String, nil] Optional description for the trace
51
+ # @yield [tracer] Yields the tracer object to the block
52
+ # @yieldparam tracer [Tracer] The tracer object
53
+ # @return [Object] The result of the block
54
+ def trace(name = nil, description = nil)
55
+ begin
56
+ setup
57
+ @tracer = Tracer.new(name, description)
58
+ result = @tracer.enable_traces do
59
+ yield @tracer
60
+ end
61
+ persist(@tracer.name, @tracer.description)
62
+ cleanup
63
+ result
64
+ rescue => e
65
+ Codebeacon::Tracer.logger.error("Error during tracing: #{e.message}")
66
+ Codebeacon::Tracer.logger.error(e.backtrace.join("\n")) if Codebeacon::Tracer.config.debug?
67
+ # Continue execution without crashing the application
68
+ yield nil if block_given?
69
+ end
70
+ end
71
+
72
+ # Starts tracing without a block
73
+ # @return [void]
74
+ def start
75
+ setup
76
+ @tracer = Tracer.new()
77
+ @tracer.start
78
+ end
79
+
80
+ # Stops tracing and persists the results
81
+ # @return [void]
82
+ def stop
83
+ @tracer.stop
84
+ persist
85
+ cleanup
86
+ end
87
+
88
+ private def setup
89
+ Codebeacon::Tracer.config.setup
90
+ @app_node = NodeSource.new('app', Codebeacon::Tracer.config.root_path)
91
+ @gem_node = NodeSource.new('gem', Codebeacon::Tracer.config.gem_path)
92
+ @rubylib_node = NodeSource.new('rubylib', Codebeacon::Tracer.config.rubylib_path)
93
+ end
94
+
95
+ private def persist(name = "", description = "")
96
+ unless Codebeacon::Tracer.config.dry_run?
97
+ begin
98
+ schema = DatabaseSchema.new
99
+ schema.create_tables
100
+ DatabaseSchema.trim_db_files
101
+ pm = PersistenceManager.new(schema.db)
102
+ ordered_sources = [ @app_node, @gem_node, @rubylib_node ]
103
+ pm.save_metadata(name, description)
104
+ pm.save_node_sources(ordered_sources)
105
+ pm.save_trees(@tracer.tree_manager.trees)
106
+ schema.create_indexes
107
+ schema.db.close
108
+ touch_refresh
109
+ rescue => e
110
+ Codebeacon::Tracer.logger.error("Error during persistence: #{e.message}")
111
+ Codebeacon::Tracer.logger.error(e.backtrace.join("\n")) if Codebeacon::Tracer.config.debug?
112
+ # Continue execution without crashing the application
113
+ end
114
+ end
115
+ end
116
+
117
+ private def cleanup
118
+ NodeSource.clear
119
+ @tracer.cleanup
120
+ end
121
+
122
+ private def touch_refresh
123
+ FileUtils.mkdir_p(Codebeacon::Tracer.config.tmp_dir) unless File.exist?(Codebeacon::Tracer.config.tmp_dir)
124
+ if File.exist?(Codebeacon::Tracer.config.refresh_path)
125
+ File.utime(Time.now, Time.now, Codebeacon::Tracer.config.refresh_path)
126
+ else
127
+ File.open(Codebeacon::Tracer.config.refresh_path, 'w') {}
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end