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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cc0306f363cf17c02cf4e0de4c37df04d52b22f7b992b047767a47e243341a9a
4
+ data.tar.gz: ba785f9723112678105d34030f1ea3f9960630d6701a3f14041e5d82d554c6c6
5
+ SHA512:
6
+ metadata.gz: c59c1bd83110e7e4214988ca69fa6f08d9533c096b98e4e555c28d2700ef35d05e3c1fd154804d156bc9b022656dbf3b3a6c17e2592df1cc0f67abe73f3aef15
7
+ data.tar.gz: 7d2ae9552ea537767cb5ef93409565243674a75b65f3b9810ca5f3c894845743aa775def5dd519c4ac99687b057ab157e7b4e2e5b57ea234afb7aaaa84f9c96c
@@ -0,0 +1,199 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+
4
+ module Codebeacon
5
+ module Tracer
6
+ class Configuration
7
+ MAX_DB_FILES = 100
8
+ RETURN_VAL_MAX_LENGTH = 1000
9
+ MAX_CALL_COUNT = 100000000
10
+ MAX_DEPTH = 99999
11
+
12
+ def initialize()
13
+ @query = ""
14
+ @exclude_paths = []
15
+ ensure_db_path
16
+ end
17
+
18
+ def setup
19
+ exclude_paths << lib_root
20
+ reload_paths_to_record
21
+ load_main_config
22
+ end
23
+
24
+ def load_main_config
25
+ if File.exist?(config_path)
26
+ config_data = YAML.load_file(config_path)
27
+ load_exclude_paths(config_data['exclude'])
28
+ end
29
+ end
30
+
31
+ def load_exclude_paths(excludes)
32
+ return if excludes.nil?
33
+ (excludes['paths'] || []).each { |path| exclude_paths << path }
34
+ (excludes['gems'] || []).each do |gem_name|
35
+ Gem::Specification.find_all_by_name(gem_name).each do |gem_spec|
36
+ exclude_paths << gem_spec.gem_dir
37
+ end
38
+ end
39
+ end
40
+
41
+ def set_query_config(query)
42
+ @query = query || ""
43
+ # self.trace_enabled = @query.include?('rf__trace_enabled=true')
44
+ self.debug = @query.include?('rf__debug=true')
45
+ self.dry_run = @query.include?('rf__dry_run=true')
46
+ # self.local_methods_only = @query.include?('rf__local_methods_only=true')
47
+ # self.local_lines_only = @query.include?('rf__local_lines_only=true')
48
+ end
49
+
50
+ def ensure_db_path
51
+ FileUtils.mkpath(db_path)
52
+ end
53
+
54
+ # def load_ruby_flow_config
55
+ # if File.exist?('.code-beacon.yml')
56
+ # @config = YAML.load_file('.code-beacon.yml')
57
+ # end
58
+ # end
59
+
60
+ def lib_root
61
+ File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..'))
62
+ end
63
+
64
+ def data_dir
65
+ ".code-beacon"
66
+ end
67
+
68
+ def db_path
69
+ File.join(data_dir, "db")
70
+ end
71
+
72
+ def tmp_dir
73
+ File.join(data_dir, "tmp")
74
+ end
75
+
76
+ def refresh_path
77
+ File.join(data_dir, "tmp", "refresh")
78
+ end
79
+
80
+ def paths_path
81
+ File.join(data_dir, "paths.yml")
82
+ end
83
+
84
+ def config_path
85
+ File.expand_path(File.join(lib_root, 'config.yml'))
86
+ end
87
+
88
+ def read_paths
89
+ if File.exist?(paths_path)
90
+ YAML.load_file(paths_path)
91
+ end
92
+ end
93
+
94
+ def db_name
95
+ "codebeacon_tracer"
96
+ end
97
+
98
+ def max_db_files
99
+ MAX_DB_FILES
100
+ end
101
+
102
+ def gem_path
103
+ @gem_path ||= ENV['GEM_HOME'] || Gem.paths.home
104
+ end
105
+
106
+ def root_path
107
+ @root_path ||= defined?(Rails) ? Rails.root.to_s : Dir.pwd
108
+ end
109
+
110
+ def rubylib_path
111
+ @rubylib_path ||= RbConfig::CONFIG['rubylibdir']
112
+ end
113
+
114
+ def paths_to_record
115
+ @paths_to_record ||= [Codebeacon::Tracer.config.root_path, *Codebeacon::Tracer.config.read_paths]
116
+ end
117
+
118
+ def reload_paths_to_record
119
+ @paths_to_record = nil
120
+ paths_to_record
121
+ end
122
+
123
+ def exclude_paths(*paths)
124
+ @exclude_paths += paths
125
+ end
126
+
127
+ def trace_enabled?
128
+ # @trace_enabled
129
+ true
130
+ end
131
+
132
+ def trace_enabled=(value)
133
+ @trace_enabled = value
134
+ end
135
+
136
+ def debug?
137
+ @debug
138
+ end
139
+
140
+ def debug=(value)
141
+ Codebeacon::Tracer.logger.level = value ? ::Logger::DEBUG : ::Logger::INFO
142
+ @debug = value
143
+ end
144
+
145
+ def dry_run?
146
+ @dry_run
147
+ end
148
+
149
+ def dry_run=(value)
150
+ @dry_run = value
151
+ end
152
+
153
+ def local_methods_only?
154
+ # @local_methods_only
155
+ true
156
+ end
157
+
158
+ def local_methods_only=(value)
159
+ @local_methods_only = value
160
+ end
161
+
162
+ def local_lines_only?
163
+ # @local_lines_only
164
+ true
165
+ end
166
+
167
+ def local_lines_only=(value)
168
+ @local_lines_only = value
169
+ end
170
+
171
+ def skip_internal?
172
+ true
173
+ end
174
+
175
+ def max_value_length
176
+ RETURN_VAL_MAX_LENGTH
177
+ end
178
+
179
+ def max_call_count
180
+ MAX_CALL_COUNT
181
+ end
182
+
183
+ def max_depth
184
+ MAX_DEPTH
185
+ end
186
+
187
+ def logger
188
+ @logger ||= Codebeacon::Tracer::Logger.new()
189
+ end
190
+
191
+ def debug_something
192
+ #### debug return
193
+ # @return_count += 1
194
+ # Codebeacon::Tracer.logger.info("Call count: #{@return_count}")
195
+ # Codebeacon::Tracer.logger.info("Return @depth: #{@depth}, method: #{tp.method_id}, line: #{tp.lineno}, path: #{tp.path}")
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,54 @@
1
+ require 'sqlite3'
2
+ require 'fileutils'
3
+ require_relative 'tree_node_mapper'
4
+ require_relative 'node_source_mapper'
5
+
6
+ module Codebeacon
7
+ module Tracer
8
+ class DatabaseSchema
9
+ def initialize
10
+ @db = initialize_db
11
+ end
12
+
13
+ def db
14
+ @db
15
+ end
16
+
17
+ def initialize_db
18
+ db_path = Codebeacon::Tracer.config.db_path
19
+ timestamp = Time.now.strftime("%Y%m%d%H%M%S")
20
+ db_name = "#{Codebeacon::Tracer.config.db_name}_#{timestamp}.db"
21
+ db_symlink = File.join(db_path, "#{Codebeacon::Tracer.config.db_name}.db")
22
+
23
+ File.delete(db_symlink) if File.exist?(db_symlink)
24
+ FileUtils.ln_sf(db_name, db_symlink)
25
+ SQLite3::Database.new(File.join(db_path, db_name))
26
+ end
27
+
28
+ def create_tables
29
+ MetadataMapper.create_table(db)
30
+ TreeNodeMapper.create_table(db)
31
+ NodeSourceMapper.create_table(db)
32
+ end
33
+
34
+ def create_indexes
35
+ MetadataMapper.create_indexes(db)
36
+ TreeNodeMapper.create_indexes(db)
37
+ NodeSourceMapper.create_indexes(db)
38
+ end
39
+
40
+ def self.trim_db_files
41
+ db_path = Codebeacon::Tracer.config.db_path
42
+ db_files = Dir.glob(File.join(db_path, "*.db"))
43
+ db_files.reject! { |file| File.symlink?(file) }
44
+ db_files.sort_by! { |db_file| File.mtime(db_file) }
45
+ db_files.reverse!
46
+
47
+ db_files.each_with_index do |db_file, index|
48
+ next if index < Codebeacon::Tracer.config.max_db_files
49
+ File.delete(db_file)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,26 @@
1
+ module Codebeacon
2
+ module Tracer
3
+ class MetadataMapper
4
+ def initialize(database)
5
+ @db = database
6
+ end
7
+
8
+ def self.create_table(database)
9
+ database.execute <<-SQL
10
+ CREATE TABLE IF NOT EXISTS metadata (
11
+ id INTEGER PRIMARY KEY,
12
+ name TEXT,
13
+ description TEXT
14
+ );
15
+ SQL
16
+ end
17
+
18
+ def self.create_indexes(database)
19
+ end
20
+
21
+ def insert(name, description)
22
+ @db.execute("INSERT INTO metadata (name, description) VALUES (?, ?)", [name, description])
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ require_relative '../models/node_source'
2
+
3
+ module Codebeacon
4
+ module Tracer
5
+ class NodeSourceMapper
6
+ def initialize(database)
7
+ @db = database
8
+ end
9
+
10
+ def self.create_table(database)
11
+ database.execute <<-SQL
12
+ CREATE TABLE IF NOT EXISTS node_sources (
13
+ id INTEGER PRIMARY KEY,
14
+ name TEXT NOT NULL,
15
+ root_path TEXT NOT NULL
16
+ );
17
+ SQL
18
+ end
19
+
20
+ def self.create_indexes(database)
21
+ end
22
+
23
+ def insert(name, root_path)
24
+ @db.execute("INSERT INTO node_sources (name, root_path) VALUES (?, CAST(? AS TEXT))", [name, root_path])
25
+ @db.last_insert_row_id
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,107 @@
1
+ require_relative 'tree_node_mapper'
2
+ require_relative 'node_source_mapper'
3
+ require_relative 'metadata_mapper'
4
+
5
+ module Codebeacon
6
+ module Tracer
7
+ class PersistenceManager
8
+
9
+ def self.marshal(name, value, tree_node)
10
+ begin
11
+ return value.inspect[0..Codebeacon::Tracer.config.max_value_length]
12
+ rescue => e
13
+ begin
14
+ if Codebeacon::Tracer.config.debug?
15
+ Codebeacon::Tracer.logger.warn "Marshal inspect failure - attempting to_s fallback for: \"#{name}\", located at: \"#{tree_node.file}:#{tree_node.line}\"\nerror message: \"#{e.message}\", error_location: \"#{e.backtrace[0]}\""
16
+ end
17
+ return value.to_s[0..Codebeacon::Tracer.config.max_value_length]
18
+ rescue => e
19
+ Codebeacon::Tracer.logger.error "Marshal failure for: \"#{name}\", located at: \"#{tree_node.file}:#{tree_node.line}\"\nerror message: \"#{e.message}\", error_location: \"#{e.backtrace[0]}\""
20
+ return "--Codebeacon::Tracer ERROR-- could not marshall value. See logs."
21
+ end
22
+ end
23
+ end
24
+
25
+ def initialize(database)
26
+ @database = database
27
+ @tree_node_mapper = TreeNodeMapper.new(database)
28
+ @node_source_mapper = NodeSourceMapper.new(database)
29
+ @metadata_mapper = MetadataMapper.new(database)
30
+ @progress_logger = Codebeacon::Tracer.logger.newProgressLogger("nodes persisted")
31
+ end
32
+
33
+ def save_metadata(name, description)
34
+ @metadata_mapper.insert(name, description)
35
+ end
36
+
37
+ def save_node_sources(node_sources)
38
+ node_sources.each do |node_source|
39
+ next if node_source.nil?
40
+ node_source.id = @node_source_mapper.insert(node_source.name, node_source.root_path)
41
+ end
42
+ end
43
+
44
+ def save_trees(trees)
45
+ Codebeacon::Tracer.logger.info("BEGIN db persistence")
46
+ begin
47
+ trees.each do |tree|
48
+ save_tree(tree.root)
49
+ end
50
+ rescue => e
51
+ Codebeacon::Tracer.logger.error("Error during tree persistence: #{e.message}")
52
+ Codebeacon::Tracer.logger.error(e.backtrace.join("\n")) if Codebeacon::Tracer.config.debug?
53
+ # Continue execution without crashing the application
54
+ ensure
55
+ @progress_logger.finish()
56
+ Codebeacon::Tracer.logger.info("END db persistence")
57
+ end
58
+ end
59
+
60
+ def save_tree(tree_node, parent_id = nil)
61
+ _save_tree(tree_node, parent_id)
62
+ end
63
+
64
+ def _save_tree(tree_node, parent_id = nil)
65
+ @progress_logger.increment
66
+ return if tree_node.nil?
67
+
68
+ begin
69
+ node_id = @tree_node_mapper.insert(
70
+ tree_node.file,
71
+ tree_node.line,
72
+ tree_node.method.to_s,
73
+ tree_node.tp_class.to_s,
74
+ tree_node.tp_defined_class.to_s,
75
+ tree_node.tp_class_name.to_s,
76
+ tree_node.self_type.to_s,
77
+ tree_node.depth,
78
+ tree_node.caller,
79
+ tree_node.gem_entry,
80
+ parent_id,
81
+ tree_node.block,
82
+ tree_node.node_source&.id,
83
+ _return_value(tree_node)
84
+ )
85
+
86
+ unless tree_node.depth_truncated?
87
+ tree_node.children.each do |child|
88
+ _save_tree(child, node_id)
89
+ end
90
+ end
91
+ rescue => e
92
+ Codebeacon::Tracer.logger.error("Error saving tree node: #{e.message}")
93
+ Codebeacon::Tracer.logger.error("Node details: file=#{tree_node.file}, line=#{tree_node.line}, method=#{tree_node.method}") if Codebeacon::Tracer.config.debug?
94
+ # Continue with siblings and other nodes without crashing
95
+ end
96
+ end
97
+
98
+ def _return_value(node)
99
+ if node.method == :initialize
100
+ return nil
101
+ else
102
+ PersistenceManager.marshal(node.method, node.return_value, node)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,60 @@
1
+ require 'sqlite3'
2
+ require 'json'
3
+
4
+ module Codebeacon
5
+ module Tracer
6
+ class TreeNodeMapper
7
+ def initialize(database)
8
+ @db = database
9
+ end
10
+
11
+ def insert(file, line, method, tp_class, tp_defined_class, tp_class_name, self_type, depth, caller, gem_entry, parent_id, block, node_source_id, return_value)
12
+ @db.execute(<<-SQL,
13
+ INSERT INTO treenodes
14
+ (
15
+ file, line, method, tp_class, tp_defined_class, tp_class_name, self_type, depth, caller,
16
+ gemEntry, parent_id, block, node_source_id, return_value
17
+ )
18
+ VALUES
19
+ (
20
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
21
+ )
22
+ SQL
23
+ file, line, method, tp_class, tp_defined_class, tp_class_name, self_type, depth, caller,
24
+ gem_entry ? 1 : 0, parent_id, block ? 1 : 0, node_source_id, return_value)
25
+
26
+ @db.last_insert_row_id
27
+ end
28
+
29
+ def self.create_table(database)
30
+ database.execute <<-SQL
31
+ CREATE TABLE IF NOT EXISTS treenodes (
32
+ id INTEGER PRIMARY KEY,
33
+ file TEXT,
34
+ line INTEGER,
35
+ method TEXT,
36
+ tp_class TEXT,
37
+ tp_defined_class TEXT,
38
+ tp_class_name TEXT,
39
+ self_type TEXT,
40
+ depth INTEGER,
41
+ caller TEXT,
42
+ gemEntry INTEGER,
43
+ parent_id INTEGER,
44
+ block INTEGER,
45
+ node_source_id INTEGER,
46
+ return_value TEXT,
47
+ FOREIGN KEY (parent_id) REFERENCES treenodes(id),
48
+ FOREIGN KEY (node_source_id) REFERENCES node_sources(id)
49
+ )
50
+ SQL
51
+ end
52
+
53
+ def self.create_indexes(database)
54
+ database.execute("CREATE INDEX IF NOT EXISTS IDX_treenode_parent_id ON treenodes(parent_id)")
55
+ database.execute("CREATE INDEX IF NOT EXISTS IDX_treenode_node_source_id ON treenodes(node_source_id)")
56
+ database.execute("CREATE INDEX IF NOT EXISTS IDX_treenode_file ON treenodes(file)")
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,58 @@
1
+ require 'logger'
2
+
3
+ module Codebeacon
4
+ module Tracer
5
+ class Logger
6
+ FILENAME = "codebeacon_tracer.log"
7
+ attr_reader :logger
8
+
9
+ def initialize(level = nil)
10
+ level ||= Codebeacon::Tracer.config.debug? ? ::Logger::DEBUG : ::Logger::INFO
11
+ @logger ||= ::Logger.new(File.join(Codebeacon::Tracer.config.data_dir, FILENAME), 3, 104857600, level: level)
12
+ end
13
+
14
+ def newProgressLogger(*args)
15
+ ProgressLogger.new(@logger, *args)
16
+ end
17
+
18
+ def debug(message, *args, &block)
19
+ return unless Codebeacon::Tracer.config.debug?
20
+ @logger.debug(message, *args, &block)
21
+ end
22
+
23
+ def method_missing(method, *args, &block)
24
+ if @logger.respond_to?(method)
25
+ @logger.send(method, *args, &block)
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ def respond_to_missing?(method, include_private = false)
32
+ @logger.respond_to?(method, include_private) || super
33
+ end
34
+ end
35
+
36
+ class ProgressLogger
37
+ PROGRESS_LOG_INTERVAL = 1000
38
+
39
+ def initialize(logger, msg, interval = PROGRESS_LOG_INTERVAL)
40
+ @logger = logger
41
+ @msg = msg
42
+ @interval = interval
43
+ @count = 0
44
+ end
45
+
46
+ def increment()
47
+ @count += 1
48
+ if @count % @interval == 0
49
+ @logger.info(@count.to_s + " " + @msg)
50
+ end
51
+ end
52
+
53
+ def finish()
54
+ @logger.info("Finished: " + @count.to_s + " " + @msg)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,54 @@
1
+ module Codebeacon
2
+ module Tracer
3
+ class CallTree
4
+ @thread_id_mutex = Mutex.new
5
+ @thread_id = 0
6
+
7
+ attr_reader :thread, :root, :current_node, :depth, :call_count, :block_call_count
8
+
9
+ def self.next_thread_id
10
+ @thread_id_mutex.synchronize do
11
+ @thread_id += 1
12
+ end
13
+ end
14
+
15
+ def initialize(thread)
16
+ @thread = thread
17
+ root_name = (thread.name || "thread") + " (#{CallTree.next_thread_id})"
18
+ @root = TreeNode.new(method: root_name)
19
+ @root.file, @root.line = __FILE__, __LINE__
20
+ @current_node = @root
21
+ @depth = 0
22
+ @call_count = 0
23
+ @block_call_count = 0
24
+ end
25
+
26
+ def total_call_count
27
+ @call_count + @block_call_count
28
+ end
29
+
30
+ def add_call()
31
+ @call_count += 1
32
+ add_node()
33
+ end
34
+
35
+ def add_block_call()
36
+ @block_call_count += 1
37
+ add_node()
38
+ end
39
+
40
+ def add_node()
41
+ new_node = TreeNode.new()
42
+ @current_node.children << new_node
43
+ new_node.parent = @current_node
44
+ @depth += 1
45
+ @current_node = new_node
46
+ end
47
+
48
+ def add_return()
49
+ @depth -= 1
50
+ @current_node = @current_node.parent if @current_node
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,60 @@
1
+ module Codebeacon
2
+ module Tracer
3
+ class NodeBuilder
4
+ class << self
5
+ def backtrace_location_eql(loc1, loc2)
6
+ loc1.absolute_path == loc2.absolute_path && loc1.lineno == loc2.lineno && loc1.label == loc2.label
7
+ end
8
+
9
+ def trace_method_call(call_tree, tp, tp_caller)
10
+ call_tree.add_call
11
+ trace_call(call_tree, tp, tp_caller, :get_method_ast)
12
+ end
13
+
14
+ def trace_block_call(call_tree, tp, tp_caller)
15
+ current_context = call_tree.add_block_call
16
+ current_context.block = true
17
+ trace_call(call_tree, tp, tp_caller, :get_block_ast)
18
+ end
19
+
20
+ def trace_return(call_tree, tp)
21
+ begin
22
+ current_context = call_tree.current_node
23
+ variable_values = {}
24
+ current_context.return_value = "--Codebeacon::Tracer ERROR-- could not capture return value"
25
+ previous_line = current_context.trace_status.previous_line
26
+ current_context.return_value = tp.return_value
27
+ ensure
28
+ call_tree.add_return()
29
+ end
30
+ end
31
+
32
+ private def trace_call(call_tree, tp, tp_caller, ast_get_method)
33
+ current_context = call_tree.current_node
34
+
35
+ current_context.file = File.absolute_path(tp.path)
36
+ current_context.node_source = NodeSource.find(tp.path)
37
+ current_context.line = tp.lineno
38
+ current_context.object_id = tp.self.object_id
39
+ current_context.method = tp.method_id
40
+ klass = TPKlass.new(tp)
41
+
42
+ current_context.tp_class = klass.tp_class.to_s
43
+ current_context.tp_defined_class = klass.defined_class
44
+ current_context.tp_class_name = klass.tp_class_name
45
+ current_context.self_type = klass.type
46
+ current_context.depth = call_tree.depth
47
+
48
+ gem_entry = false
49
+ if Codebeacon::Tracer.config.gem_path \
50
+ && !Codebeacon::Tracer.config.gem_path.empty? \
51
+ && tp.path.start_with?(Codebeacon::Tracer.config.gem_path) # && caller[1].start_with?(Codebeacon::Tracer.config.root_path)
52
+ gem_entry = true
53
+ end
54
+ current_context.gem_entry = gem_entry
55
+ current_context.caller = ""
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,32 @@
1
+ require 'pathname'
2
+
3
+ module Codebeacon
4
+ module Tracer
5
+ class NodeSource
6
+ attr_accessor :id, :name, :root_path
7
+ @instances = []
8
+
9
+ class << self
10
+ attr_accessor :instances
11
+ end
12
+
13
+ def initialize(name, root_path)
14
+ @name = name
15
+ @root_path = root_path
16
+ self.class.instances << self
17
+ end
18
+
19
+ def self.find(path)
20
+ return nil if path.nil?
21
+ Pathname.new(path).ascend do |dir|
22
+ source = self.instances.find { |ns| dir.to_s == ns.root_path }
23
+ return source if source
24
+ end
25
+ end
26
+
27
+ def self.clear
28
+ self.instances = []
29
+ end
30
+ end
31
+ end
32
+ end