codebeacon-tracer 0.1.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc0306f363cf17c02cf4e0de4c37df04d52b22f7b992b047767a47e243341a9a
4
- data.tar.gz: ba785f9723112678105d34030f1ea3f9960630d6701a3f14041e5d82d554c6c6
3
+ metadata.gz: 47ec3d248b6f0358c3c36310a7b39c506982695565e453d11580ae20b1f28582
4
+ data.tar.gz: 8f6da9ed15ca33105c73fead83856a014c718853cbf30db5641719e5d6fdfc36
5
5
  SHA512:
6
- metadata.gz: c59c1bd83110e7e4214988ca69fa6f08d9533c096b98e4e555c28d2700ef35d05e3c1fd154804d156bc9b022656dbf3b3a6c17e2592df1cc0f67abe73f3aef15
7
- data.tar.gz: 7d2ae9552ea537767cb5ef93409565243674a75b65f3b9810ca5f3c894845743aa775def5dd519c4ac99687b057ab157e7b4e2e5b57ea234afb7aaaa84f9c96c
6
+ metadata.gz: 69b782857204580baf4b8251f478d0e16652d015fcbf5f79c65a47f691f9623a52ee9271c973d5539ed4d680d4c37c21f9571299225ce16ecab446e9bbd43ffd
7
+ data.tar.gz: 27dd8bdcb652b7db338e0ac8aba0862b6cb3106c00a76524265cec238714849d046c416c43c62448aa3ca4ca84be19a3c0651b53b347cdd79b9561fd3b7dca99
data/bin/codebeacon ADDED
@@ -0,0 +1,18 @@
1
+ #! /usr/bin/env ruby
2
+ require_relative '../lib/codebeacon-tracer'
3
+
4
+ script = ARGV.shift
5
+ if script.nil?
6
+ Codebeacon::Tracer.logger.error("No script provided")
7
+ exit 1
8
+ end
9
+
10
+ at_exit do
11
+ Codebeacon::Tracer.stop
12
+ end
13
+ Codebeacon::Tracer.start(
14
+ name: File.basename(script),
15
+ trigger_type: "script"
16
+ )
17
+
18
+ load File.expand_path(script)
@@ -1,6 +1,12 @@
1
1
  require 'fileutils'
2
2
  require 'yaml'
3
3
 
4
+ begin
5
+ require 'listen'
6
+ rescue LoadError
7
+ # Listen gem not available, file watching will be disabled
8
+ end
9
+
4
10
  module Codebeacon
5
11
  module Tracer
6
12
  class Configuration
@@ -8,6 +14,7 @@ module Codebeacon
8
14
  RETURN_VAL_MAX_LENGTH = 1000
9
15
  MAX_CALL_COUNT = 100000000
10
16
  MAX_DEPTH = 99999
17
+ SERIALIZATION_TIMEOUT_MS = 10
11
18
 
12
19
  def initialize()
13
20
  @query = ""
@@ -19,6 +26,7 @@ module Codebeacon
19
26
  exclude_paths << lib_root
20
27
  reload_paths_to_record
21
28
  load_main_config
29
+ start_config_file_watcher
22
30
  end
23
31
 
24
32
  def load_main_config
@@ -62,7 +70,7 @@ module Codebeacon
62
70
  end
63
71
 
64
72
  def data_dir
65
- ".code-beacon"
73
+ File.join(root_path, ".code-beacon")
66
74
  end
67
75
 
68
76
  def db_path
@@ -85,6 +93,10 @@ module Codebeacon
85
93
  File.expand_path(File.join(lib_root, 'config.yml'))
86
94
  end
87
95
 
96
+ def tracer_config_path
97
+ File.join(data_dir, "tracer_config.yml")
98
+ end
99
+
88
100
  def read_paths
89
101
  if File.exist?(paths_path)
90
102
  YAML.load_file(paths_path)
@@ -104,7 +116,7 @@ module Codebeacon
104
116
  end
105
117
 
106
118
  def root_path
107
- @root_path ||= defined?(Rails) ? Rails.root.to_s : Dir.pwd
119
+ @root_path ||= @config_root_path || (defined?(Rails) ? Rails.root.to_s : Dir.pwd)
108
120
  end
109
121
 
110
122
  def rubylib_path
@@ -125,12 +137,52 @@ module Codebeacon
125
137
  end
126
138
 
127
139
  def trace_enabled?
128
- # @trace_enabled
129
- true
140
+ if @donotcache
141
+ load_tracer_config_enabled
142
+ else
143
+ @trace_enabled ||= load_tracer_config_enabled
144
+ end
145
+ end
146
+
147
+ def reload_tracer_config
148
+ @trace_enabled = load_tracer_config_enabled
149
+ @config_root_path = load_tracer_config_root_path
150
+ @recording_meta_exclude_patterns = nil # Force reload of exclusion patterns
151
+ end
152
+
153
+ def reload_main_config
154
+ @root_path = nil # Force reload of root_path
155
+ load_main_config
130
156
  end
131
157
 
132
- def trace_enabled=(value)
133
- @trace_enabled = value
158
+ def recording_meta_exclude_patterns
159
+ if @donotcache
160
+ load_recording_meta_exclude_patterns
161
+ else
162
+ @recording_meta_exclude_patterns ||= load_recording_meta_exclude_patterns
163
+ end
164
+ end
165
+
166
+ def skip_tracing?(name, description)
167
+ return false unless recording_meta_exclude_patterns&.any?
168
+
169
+ name_str = name.to_s
170
+ description_str = description.to_s
171
+
172
+ excluded = recording_meta_exclude_patterns.any? do |pattern|
173
+ name_matches = File.fnmatch(pattern['name'], name_str, File::FNM_CASEFOLD)
174
+ description_matches = File.fnmatch(pattern['description'], description_str, File::FNM_CASEFOLD)
175
+ name_matches && description_matches
176
+ end
177
+
178
+ if excluded && debug?
179
+ logger.debug("Skipping trace due to metadata exclusion - name: '#{name_str}', description: '#{description_str}'")
180
+ end
181
+
182
+ excluded
183
+ rescue => e
184
+ logger.warn("Error checking skip_tracing patterns: #{e.message}")
185
+ false # Default to not skipping on error
134
186
  end
135
187
 
136
188
  def debug?
@@ -184,6 +236,10 @@ module Codebeacon
184
236
  MAX_DEPTH
185
237
  end
186
238
 
239
+ def serialization_timeout_ms
240
+ SERIALIZATION_TIMEOUT_MS
241
+ end
242
+
187
243
  def logger
188
244
  @logger ||= Codebeacon::Tracer::Logger.new()
189
245
  end
@@ -194,6 +250,77 @@ module Codebeacon
194
250
  # Codebeacon::Tracer.logger.info("Call count: #{@return_count}")
195
251
  # Codebeacon::Tracer.logger.info("Return @depth: #{@depth}, method: #{tp.method_id}, line: #{tp.lineno}, path: #{tp.path}")
196
252
  end
253
+
254
+ def start_config_file_watcher
255
+ return unless defined?(Listen) && !@config_listener
256
+ return unless File.directory?(data_dir)
257
+
258
+ begin
259
+ @config_listener = Listen.to(data_dir, only: /tracer_config\.yml$/) do |modified, added, removed|
260
+ if (modified + added + removed).any? { |path| File.basename(path) == 'tracer_config.yml' }
261
+ reload_tracer_config
262
+ end
263
+ end
264
+ @config_listener.start
265
+ rescue => e
266
+ @donotcache = true
267
+ logger.warn("Failed to start config file watcher: #{e.message}")
268
+ end
269
+ end
270
+
271
+ def stop_config_file_watcher
272
+ if @config_listener
273
+ @config_listener.stop
274
+ @config_listener = nil
275
+ end
276
+ end
277
+
278
+ private
279
+
280
+ def load_recording_meta_exclude_patterns
281
+ if File.exist?(tracer_config_path)
282
+ config_data = YAML.load_file(tracer_config_path)
283
+ patterns = config_data.dig('filters', 'recording_meta_exclude') || []
284
+
285
+ # Validate patterns
286
+ valid_patterns = patterns.select do |pattern|
287
+ if pattern.is_a?(Hash) && pattern.key?('name') && pattern.key?('description')
288
+ true
289
+ else
290
+ logger.warn("Invalid recording_meta_exclude pattern: #{pattern.inspect}")
291
+ false
292
+ end
293
+ end
294
+
295
+ valid_patterns
296
+ else
297
+ []
298
+ end
299
+ rescue => e
300
+ logger.warn("Error loading recording meta exclude patterns: #{e.message}")
301
+ []
302
+ end
303
+
304
+ def load_tracer_config_enabled
305
+ if File.exist?(tracer_config_path)
306
+ config_data = YAML.load_file(tracer_config_path)
307
+ config_data['tracing_enabled'] != false # Default to true if not specified
308
+ else
309
+ true
310
+ end
311
+ rescue => e
312
+ logger.warn("Error loading tracer config: #{e.message}")
313
+ true
314
+ end
315
+
316
+ def load_tracer_config_root_path
317
+ if File.exist?(tracer_config_path)
318
+ config_data = YAML.load_file(tracer_config_path)
319
+ @config_root_path = config_data['root_path'] if config_data['root_path']
320
+ end
321
+ rescue => e
322
+ logger.warn("Error loading tracer config root_path: #{e.message}")
323
+ end
197
324
  end
198
325
  end
199
326
  end
@@ -10,7 +10,17 @@ module Codebeacon
10
10
  CREATE TABLE IF NOT EXISTS metadata (
11
11
  id INTEGER PRIMARY KEY,
12
12
  name TEXT,
13
- description TEXT
13
+ description TEXT,
14
+ caller_file TEXT,
15
+ caller_method TEXT,
16
+ caller_line INTEGER,
17
+ caller_class TEXT,
18
+ caller_defined_class TEXT,
19
+ start_time TEXT,
20
+ end_time TEXT,
21
+ duration_ms REAL,
22
+ trigger_type TEXT,
23
+ language TEXT
14
24
  );
15
25
  SQL
16
26
  end
@@ -18,8 +28,31 @@ module Codebeacon
18
28
  def self.create_indexes(database)
19
29
  end
20
30
 
21
- def insert(name, description)
22
- @db.execute("INSERT INTO metadata (name, description) VALUES (?, ?)", [name, description])
31
+ def insert(metadata)
32
+ metadata_hash = metadata.to_hash
33
+
34
+ @db.execute(<<-SQL,
35
+ INSERT INTO metadata (
36
+ name, description, caller_file, caller_method, caller_line,
37
+ caller_class, caller_defined_class, start_time, end_time,
38
+ duration_ms, trigger_type, language
39
+ ) VALUES (
40
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
41
+ )
42
+ SQL
43
+ metadata_hash[:name],
44
+ metadata_hash[:description],
45
+ metadata_hash[:caller_file],
46
+ metadata_hash[:caller_method],
47
+ metadata_hash[:caller_line],
48
+ metadata_hash[:caller_class],
49
+ metadata_hash[:caller_defined_class],
50
+ metadata_hash[:start_time]&.iso8601,
51
+ metadata_hash[:end_time]&.iso8601,
52
+ metadata_hash[:duration_ms],
53
+ metadata_hash[:trigger_type],
54
+ metadata_hash[:language]
55
+ )
23
56
  end
24
57
  end
25
58
  end
@@ -1,27 +1,13 @@
1
1
  require_relative 'tree_node_mapper'
2
2
  require_relative 'node_source_mapper'
3
3
  require_relative 'metadata_mapper'
4
+ require_relative 'type_detector'
5
+ require_relative 'safe_serializer'
4
6
 
5
7
  module Codebeacon
6
8
  module Tracer
7
9
  class PersistenceManager
8
10
 
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
11
  def initialize(database)
26
12
  @database = database
27
13
  @tree_node_mapper = TreeNodeMapper.new(database)
@@ -30,8 +16,8 @@ module Codebeacon
30
16
  @progress_logger = Codebeacon::Tracer.logger.newProgressLogger("nodes persisted")
31
17
  end
32
18
 
33
- def save_metadata(name, description)
34
- @metadata_mapper.insert(name, description)
19
+ def save_metadata(metadata)
20
+ @metadata_mapper.insert(metadata)
35
21
  end
36
22
 
37
23
  def save_node_sources(node_sources)
@@ -69,6 +55,7 @@ module Codebeacon
69
55
  node_id = @tree_node_mapper.insert(
70
56
  tree_node.file,
71
57
  tree_node.line,
58
+ tree_node.called_method&.to_s, # Convert symbols to strings, keep nil as nil
72
59
  tree_node.method.to_s,
73
60
  tree_node.tp_class.to_s,
74
61
  tree_node.tp_defined_class.to_s,
@@ -80,7 +67,8 @@ module Codebeacon
80
67
  parent_id,
81
68
  tree_node.block,
82
69
  tree_node.node_source&.id,
83
- _return_value(tree_node)
70
+ return_type(tree_node),
71
+ return_value(tree_node)
84
72
  )
85
73
 
86
74
  unless tree_node.depth_truncated?
@@ -95,12 +83,14 @@ module Codebeacon
95
83
  end
96
84
  end
97
85
 
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
86
+ private def return_type(node)
87
+ return nil if node.method == :initialize
88
+ node.return_value.class.name
89
+ end
90
+
91
+ private def return_value(node)
92
+ return nil if node.method == :initialize
93
+ SafeSerializer.serialize(node.return_value, Codebeacon::Tracer.config.max_value_length)
104
94
  end
105
95
  end
106
96
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'type_detector'
4
+ require 'timeout'
5
+
6
+ module Codebeacon
7
+ module Tracer
8
+ class SafeSerializer
9
+ def self.serialize(value, max_length)
10
+ return nil unless TypeDetector.serializable_type?(value)
11
+ return "..." if max_length <= 0
12
+
13
+ timeout_ms = Codebeacon::Tracer.config.serialization_timeout_ms
14
+
15
+ begin
16
+ Timeout.timeout(timeout_ms / 1000.0) do
17
+ serialized = case value
18
+ when String
19
+ value
20
+ when Integer, Float
21
+ value.to_s
22
+ when Symbol, TrueClass, FalseClass
23
+ value.to_s
24
+ when NilClass
25
+ "nil"
26
+ else
27
+ nil
28
+ end
29
+
30
+ return nil if serialized.nil?
31
+
32
+ serialized.length <= max_length ? serialized : serialized[0...max_length] + "..."
33
+ end
34
+ rescue Timeout::Error
35
+ return "(serialization timeout)"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,78 @@
1
+ module Codebeacon
2
+ module Tracer
3
+ class TraceMetadata
4
+ attr_reader :name, :description, :caller_file, :caller_method, :caller_line,
5
+ :caller_class, :caller_defined_class, :start_time,
6
+ :end_time, :duration_ms, :trigger_type, :language
7
+
8
+ def initialize(name: nil, description: nil, caller_location: nil, trigger_type: nil)
9
+ @name = name
10
+ @description = description
11
+ @start_time = Time.now
12
+ @end_time = nil
13
+ @duration_ms = nil
14
+ @trigger_type = trigger_type
15
+ @language = "ruby"
16
+
17
+ if caller_location
18
+ capture_caller_info_from_location(caller_location)
19
+ end
20
+ end
21
+
22
+ def finish_trace
23
+ @end_time = Time.now
24
+ @duration_ms = ((@end_time - @start_time) * 1000).round(2)
25
+ end
26
+
27
+ def to_hash
28
+ {
29
+ name: @name,
30
+ description: @description,
31
+ caller_file: @caller_file,
32
+ caller_method: @caller_method,
33
+ caller_line: @caller_line,
34
+ caller_class: @caller_class,
35
+ caller_defined_class: @caller_defined_class,
36
+ start_time: @start_time,
37
+ end_time: @end_time,
38
+ duration_ms: @duration_ms,
39
+ trigger_type: @trigger_type,
40
+ language: @language
41
+ }
42
+ end
43
+
44
+ private
45
+
46
+ def capture_caller_info_from_location(location)
47
+ begin
48
+ @caller_file = location.absolute_path || location.path
49
+ @caller_line = location.lineno
50
+ @caller_method = location.label
51
+
52
+ # Try to determine the calling class/module from the location
53
+ @caller_class, @caller_defined_class = determine_caller_class_from_location(location)
54
+ rescue => e
55
+ Codebeacon::Tracer.logger.warn("Failed to capture caller info from location: #{e.message}") if Codebeacon::Tracer.config.debug?
56
+ end
57
+ end
58
+
59
+ def determine_caller_class_from_location(location)
60
+ caller_class = ""
61
+ caller_defined_class = ""
62
+
63
+ # Extract class information from the location label if possible
64
+ if location.label && location.label.include?('#')
65
+ # Instance method call - extract class name
66
+ class_method = location.label.split('#')
67
+ caller_class = class_method[0] if class_method.length > 1
68
+ elsif location.label && location.label.include?('.')
69
+ # Class method call - extract class name
70
+ class_method = location.label.split('.')
71
+ caller_class = class_method[0] if class_method.length > 1
72
+ end
73
+
74
+ [caller_class, caller_defined_class]
75
+ end
76
+ end
77
+ end
78
+ end
@@ -8,20 +8,20 @@ module Codebeacon
8
8
  @db = database
9
9
  end
10
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)
11
+ def insert(file, line, called_method, method, tp_class, tp_defined_class, tp_class_name, self_type, depth, caller, gem_entry, parent_id, block, node_source_id, return_type, return_value)
12
12
  @db.execute(<<-SQL,
13
13
  INSERT INTO treenodes
14
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
15
+ file, line, called_method, method, tp_class, tp_defined_class, tp_class_name, self_type, depth, caller,
16
+ gemEntry, parent_id, block, node_source_id, return_type, return_value
17
17
  )
18
18
  VALUES
19
19
  (
20
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
20
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
21
21
  )
22
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)
23
+ file, line, called_method, 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_type, return_value)
25
25
 
26
26
  @db.last_insert_row_id
27
27
  end
@@ -32,6 +32,7 @@ module Codebeacon
32
32
  id INTEGER PRIMARY KEY,
33
33
  file TEXT,
34
34
  line INTEGER,
35
+ called_method TEXT,
35
36
  method TEXT,
36
37
  tp_class TEXT,
37
38
  tp_defined_class TEXT,
@@ -43,6 +44,7 @@ module Codebeacon
43
44
  parent_id INTEGER,
44
45
  block INTEGER,
45
46
  node_source_id INTEGER,
47
+ return_type TEXT,
46
48
  return_value TEXT,
47
49
  FOREIGN KEY (parent_id) REFERENCES treenodes(id),
48
50
  FOREIGN KEY (node_source_id) REFERENCES node_sources(id)
@@ -54,6 +56,7 @@ module Codebeacon
54
56
  database.execute("CREATE INDEX IF NOT EXISTS IDX_treenode_parent_id ON treenodes(parent_id)")
55
57
  database.execute("CREATE INDEX IF NOT EXISTS IDX_treenode_node_source_id ON treenodes(node_source_id)")
56
58
  database.execute("CREATE INDEX IF NOT EXISTS IDX_treenode_file ON treenodes(file)")
59
+ database.execute("CREATE INDEX IF NOT EXISTS IDX_treenode_called_method ON treenodes(called_method)")
57
60
  end
58
61
  end
59
62
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Codebeacon
4
+ module Tracer
5
+ class TypeDetector
6
+ BASIC_TYPES = [
7
+ String, Integer, Float, Symbol, TrueClass, FalseClass, NilClass
8
+ ].freeze
9
+
10
+ def self.basic_type?(value)
11
+ return true if value.nil?
12
+ BASIC_TYPES.any? { |type| value.is_a?(type) }
13
+ end
14
+
15
+ def self.serializable_type?(value)
16
+ basic_type?(value)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -37,6 +37,10 @@ module Codebeacon
37
37
  current_context.line = tp.lineno
38
38
  current_context.object_id = tp.self.object_id
39
39
  current_context.method = tp.method_id
40
+ if tp.callee_id != tp.method_id
41
+ current_context.called_method = tp.callee_id
42
+ end
43
+
40
44
  klass = TPKlass.new(tp)
41
45
 
42
46
  current_context.tp_class = klass.tp_class.to_s
@@ -31,9 +31,9 @@ module Codebeacon
31
31
  # @children = []
32
32
  # end
33
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
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, :called_method
35
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)
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, called_method: nil)
37
37
  @file = file
38
38
  @line = line
39
39
  @method = method
@@ -58,6 +58,7 @@ module Codebeacon
58
58
  @backtrace_location = nil
59
59
  @script_binding = nil
60
60
  @script_self = nil
61
+ @called_method = called_method
61
62
  end
62
63
 
63
64
  def add_line(lineno, variables)
@@ -12,7 +12,8 @@ module Codebeacon
12
12
  begin
13
13
  Codebeacon::Tracer.config.set_query_config(env['QUERY_STRING'])
14
14
  if !@first_run #Codebeacon::Tracer.config.trace_enabled? && !@first_run
15
- Codebeacon::Tracer.trace do |tracer|
15
+ # For middleware, the caller is this method itself
16
+ Codebeacon::Tracer.trace(trigger_type: "middleware") do |tracer|
16
17
  dry_run_log = Codebeacon::Tracer.config.dry_run? ? "--DRY RUN-- " : ""
17
18
  Codebeacon::Tracer.logger.info(dry_run_log + "Tracing enabled for URI=#{env['REQUEST_URI']}")
18
19
  response = @app.call(env).tap do |_|
@@ -22,6 +23,9 @@ module Codebeacon
22
23
  params = env['action_dispatch.request.parameters'].dup
23
24
  tracer.name = "#{params.delete('controller')}##{params.delete('action')}"
24
25
  tracer.description = params.to_json
26
+ # Update the metadata with the Rails-specific information
27
+ tracer.metadata.instance_variable_set(:@name, tracer.name)
28
+ tracer.metadata.instance_variable_set(:@description, tracer.description)
25
29
  rescue => e
26
30
  Codebeacon::Tracer.logger.error("Error setting tracer metadata: #{e.message}")
27
31
  end
@@ -4,16 +4,26 @@ require_relative 'models/thread_local_call_tree_manager'
4
4
  module Codebeacon
5
5
  module Tracer
6
6
  class Tracer
7
- attr_reader :id, :tree_manager
8
- attr_accessor :name, :description
7
+ attr_reader :id, :tree_manager, :metadata, :name, :description
9
8
 
10
- def initialize(name = nil, description = nil)
9
+ def initialize(name: nil, description: nil, caller_location: nil, trigger_type: nil)
11
10
  @progress_logger = Codebeacon::Tracer.logger.newProgressLogger("calls traced")
12
11
  @traces = [trace_call, trace_b_call, trace_return, trace_b_return]
13
12
  @name = name
14
13
  @description = description
15
14
  @trace_id = SecureRandom.uuid
16
15
  @tree_manager = ThreadLocalCallTreeManager.new(@trace_id)
16
+ @metadata = TraceMetadata.new(name:, description:, caller_location:, trigger_type:)
17
+ end
18
+
19
+ def name=(new_name)
20
+ @name = new_name
21
+ @metadata.instance_variable_set(:@name, new_name)
22
+ end
23
+
24
+ def description=(new_description)
25
+ @description = new_description
26
+ @metadata.instance_variable_set(:@description, new_description)
17
27
  end
18
28
 
19
29
  def id()
@@ -32,6 +42,7 @@ module Codebeacon
32
42
  def stop()
33
43
  stop_traces
34
44
  @progress_logger.finish()
45
+ @metadata.finish_trace
35
46
  end
36
47
 
37
48
  def cleanup()
@@ -62,7 +73,7 @@ module Codebeacon
62
73
 
63
74
  def trace_call
64
75
  trace(:call) do |tp|
65
- NodeBuilder.trace_method_call(call_tree, tp, Kernel.caller[2..])
76
+ NodeBuilder.trace_method_call(call_tree, tp, "")
66
77
  ensure
67
78
  @progress_logger.increment()
68
79
  end
@@ -70,7 +81,7 @@ module Codebeacon
70
81
 
71
82
  def trace_b_call
72
83
  trace(:b_call) do |tp|
73
- NodeBuilder.trace_block_call(call_tree, tp, Kernel.caller[2..])
84
+ NodeBuilder.trace_block_call(call_tree, tp, "")
74
85
  ensure
75
86
  @progress_logger.increment()
76
87
  end
@@ -90,23 +101,16 @@ module Codebeacon
90
101
 
91
102
  def trace(type)
92
103
  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)
104
+ next if skip_methods?(tp.path)
99
105
  yield tp
100
106
  rescue => e
101
107
  Codebeacon::Tracer.logger.error("TracePoint(#{type}) #{tp.path} #{e.message}")
102
108
  end
103
109
  end
104
110
 
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
111
+ def skip_methods?(path)
112
+ path.nil? || Codebeacon::Tracer.config.exclude_paths.any?{ |exclude_path| path.start_with?(exclude_path) } ||
113
+ Codebeacon::Tracer.config.local_methods_only? && !path.start_with?(Codebeacon::Tracer.config.root_path)
110
114
  end
111
115
  end
112
116
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Codebeacon
4
4
  module Tracer
5
- VERSION = "0.1.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -48,17 +48,30 @@ module Codebeacon
48
48
  #
49
49
  # @param name [String, nil] Optional name for the trace
50
50
  # @param description [String, nil] Optional description for the trace
51
+ # @param trigger_type [String] The type of trigger that initiated the trace (default: "manual")
51
52
  # @yield [tracer] Yields the tracer object to the block
52
53
  # @yieldparam tracer [Tracer] The tracer object
53
54
  # @return [Object] The result of the block
54
- def trace(name = nil, description = nil)
55
+ def trace(name: nil, description: nil, trigger_type: "manual")
56
+ # Capture caller information immediately at entry point
57
+ caller_location = caller_locations(1, 1).first
58
+
59
+ unless config.trace_enabled?
60
+ logger.info("Tracing is disabled. Skipping trace: #{name} - #{description}")
61
+ return yield(nil)
62
+ end
63
+ if config.skip_tracing?(name, description)
64
+ logger.info("Exclusion rules matched. Skipping trace: #{name} - #{description}")
65
+ return yield(nil)
66
+ end
67
+
55
68
  begin
56
69
  setup
57
- @tracer = Tracer.new(name, description)
70
+ @tracer = Tracer.new(name:, description:, caller_location:, trigger_type:)
58
71
  result = @tracer.enable_traces do
59
72
  yield @tracer
60
73
  end
61
- persist(@tracer.name, @tracer.description)
74
+ persist(@tracer.metadata)
62
75
  cleanup
63
76
  result
64
77
  rescue => e
@@ -70,18 +83,28 @@ module Codebeacon
70
83
  end
71
84
 
72
85
  # Starts tracing without a block
86
+ # @param name [String, nil] Optional name for the trace
87
+ # @param description [String, nil] Optional description for the trace
88
+ # @param trigger_type [String] The type of trigger that initiated the trace (default: "manual")
73
89
  # @return [void]
74
- def start
90
+ def start(name: nil, description: nil, trigger_type: "manual")
91
+ # Capture caller information immediately at entry point
92
+ caller_location = caller_locations(1, 1).first
93
+
94
+ return unless config.trace_enabled?
95
+
75
96
  setup
76
- @tracer = Tracer.new()
97
+ @tracer = Tracer.new(name:, description:, caller_location:, trigger_type:)
77
98
  @tracer.start
78
99
  end
79
100
 
80
101
  # Stops tracing and persists the results
81
102
  # @return [void]
82
103
  def stop
104
+ return unless @tracer # checks whether trace_enabled? was false when start was called or if it was called
105
+
83
106
  @tracer.stop
84
- persist
107
+ persist(@tracer.metadata)
85
108
  cleanup
86
109
  end
87
110
 
@@ -92,25 +115,35 @@ module Codebeacon
92
115
  @rubylib_node = NodeSource.new('rubylib', Codebeacon::Tracer.config.rubylib_path)
93
116
  end
94
117
 
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
118
+ private def persist(metadata)
119
+ name = metadata.name || ""
120
+ description = metadata.description || ""
121
+
122
+ if config.skip_tracing?(name, description)
123
+ config.logger.debug("Skipping persistence due to metadata exclusion - name: '#{name}', description: '#{description}'") if config.debug?
124
+ return
125
+ end
126
+
127
+ if Codebeacon::Tracer.config.dry_run?
128
+ config.logger.debug("Dry run - skipping persistence") if config.debug?
129
+ return
130
+ end
131
+
132
+ begin
133
+ schema = DatabaseSchema.new
134
+ schema.create_tables
135
+ DatabaseSchema.trim_db_files
136
+ pm = PersistenceManager.new(schema.db)
137
+ ordered_sources = [ @app_node, @gem_node, @rubylib_node ]
138
+ pm.save_metadata(metadata)
139
+ pm.save_node_sources(ordered_sources)
140
+ pm.save_trees(@tracer.tree_manager.trees)
141
+ schema.create_indexes
142
+ schema.db.close
143
+ touch_refresh
144
+ rescue => e
145
+ Codebeacon::Tracer.logger.error("Error during persistence: #{e.message}")
146
+ Codebeacon::Tracer.logger.error(e.backtrace.join("\n")) if Codebeacon::Tracer.config.debug?
114
147
  end
115
148
  end
116
149
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: codebeacon-tracer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Conley
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-25 00:00:00.000000000 Z
11
+ date: 2025-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parser
@@ -38,6 +38,26 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: listen
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.8'
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 3.8.0
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: '3.8'
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 3.8.0
41
61
  - !ruby/object:Gem::Dependency
42
62
  name: pry
43
63
  requirement: !ruby/object:Gem::Requirement
@@ -140,17 +160,22 @@ description: Codebeacon::Tracer provides tools to trace and capture call graphs
140
160
  execution paths.
141
161
  email:
142
162
  - conley.jj@gmail.com
143
- executables: []
163
+ executables:
164
+ - codebeacon
144
165
  extensions: []
145
166
  extra_rdoc_files: []
146
167
  files:
168
+ - bin/codebeacon
147
169
  - lib/codebeacon-tracer.rb
148
170
  - lib/codebeacon/tracer/src/configuration.rb
149
171
  - lib/codebeacon/tracer/src/data/database.rb
150
172
  - lib/codebeacon/tracer/src/data/metadata_mapper.rb
151
173
  - lib/codebeacon/tracer/src/data/node_source_mapper.rb
152
174
  - lib/codebeacon/tracer/src/data/persistence_manager.rb
175
+ - lib/codebeacon/tracer/src/data/safe_serializer.rb
176
+ - lib/codebeacon/tracer/src/data/trace_metadata.rb
153
177
  - lib/codebeacon/tracer/src/data/tree_node_mapper.rb
178
+ - lib/codebeacon/tracer/src/data/type_detector.rb
154
179
  - lib/codebeacon/tracer/src/logger.rb
155
180
  - lib/codebeacon/tracer/src/models/call_tree.rb
156
181
  - lib/codebeacon/tracer/src/models/node_builder.rb