codebeacon-tracer 0.1.0 → 0.3.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: 834e2095be6b5d5a919b96bbb1395dc33bfc569cfb189d67b7e95434574c8641
4
+ data.tar.gz: 1215e606cf263cc3cf77954e4f60e175bd8df43ad9adbe1ad17859bc03003c4b
5
5
  SHA512:
6
- metadata.gz: c59c1bd83110e7e4214988ca69fa6f08d9533c096b98e4e555c28d2700ef35d05e3c1fd154804d156bc9b022656dbf3b3a6c17e2592df1cc0f67abe73f3aef15
7
- data.tar.gz: 7d2ae9552ea537767cb5ef93409565243674a75b65f3b9810ca5f3c894845743aa775def5dd519c4ac99687b057ab157e7b4e2e5b57ea234afb7aaaa84f9c96c
6
+ metadata.gz: 7f7871f11660157413fecdb33a856d9f06465690570226b201e9c5ff2a31dd3a6dbea826b2c4a0d4729a15ebadfab4d07357e9ff8978f76314ed43ef63844471
7
+ data.tar.gz: c2d72f58559f32136268687dfaea900344a4141d6beb4f252821f9cb838e7e2b296cd304c0a9e51387589611068dff1e34f9a88139c56c5a85ebe2022389cb2a
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
@@ -19,6 +25,7 @@ module Codebeacon
19
25
  exclude_paths << lib_root
20
26
  reload_paths_to_record
21
27
  load_main_config
28
+ start_config_file_watcher
22
29
  end
23
30
 
24
31
  def load_main_config
@@ -85,6 +92,10 @@ module Codebeacon
85
92
  File.expand_path(File.join(lib_root, 'config.yml'))
86
93
  end
87
94
 
95
+ def tracer_config_path
96
+ File.join(data_dir, "tracer_config.yml")
97
+ end
98
+
88
99
  def read_paths
89
100
  if File.exist?(paths_path)
90
101
  YAML.load_file(paths_path)
@@ -125,12 +136,46 @@ module Codebeacon
125
136
  end
126
137
 
127
138
  def trace_enabled?
128
- # @trace_enabled
129
- true
139
+ if @donotcache
140
+ load_tracer_config_enabled
141
+ else
142
+ @trace_enabled ||= load_tracer_config_enabled
143
+ end
144
+ end
145
+
146
+ def reload_tracer_config
147
+ @trace_enabled = load_tracer_config_enabled
148
+ @recording_meta_exclude_patterns = nil # Force reload of exclusion patterns
130
149
  end
131
150
 
132
- def trace_enabled=(value)
133
- @trace_enabled = value
151
+ def recording_meta_exclude_patterns
152
+ if @donotcache
153
+ load_recording_meta_exclude_patterns
154
+ else
155
+ @recording_meta_exclude_patterns ||= load_recording_meta_exclude_patterns
156
+ end
157
+ end
158
+
159
+ def skip_tracing?(name, description)
160
+ return false unless recording_meta_exclude_patterns&.any?
161
+
162
+ name_str = name.to_s
163
+ description_str = description.to_s
164
+
165
+ excluded = recording_meta_exclude_patterns.any? do |pattern|
166
+ name_matches = File.fnmatch(pattern['name'], name_str, File::FNM_CASEFOLD)
167
+ description_matches = File.fnmatch(pattern['description'], description_str, File::FNM_CASEFOLD)
168
+ name_matches && description_matches
169
+ end
170
+
171
+ if excluded && debug?
172
+ logger.debug("Skipping trace due to metadata exclusion - name: '#{name_str}', description: '#{description_str}'")
173
+ end
174
+
175
+ excluded
176
+ rescue => e
177
+ logger.warn("Error checking skip_tracing patterns: #{e.message}")
178
+ false # Default to not skipping on error
134
179
  end
135
180
 
136
181
  def debug?
@@ -194,6 +239,68 @@ module Codebeacon
194
239
  # Codebeacon::Tracer.logger.info("Call count: #{@return_count}")
195
240
  # Codebeacon::Tracer.logger.info("Return @depth: #{@depth}, method: #{tp.method_id}, line: #{tp.lineno}, path: #{tp.path}")
196
241
  end
242
+
243
+ def start_config_file_watcher
244
+ return unless defined?(Listen) && !@config_listener
245
+ return unless File.directory?(data_dir)
246
+
247
+ begin
248
+ @config_listener = Listen.to(data_dir, only: /tracer_config\.yml$/) do |modified, added, removed|
249
+ if (modified + added + removed).any? { |path| File.basename(path) == 'tracer_config.yml' }
250
+ reload_tracer_config
251
+ end
252
+ end
253
+ @config_listener.start
254
+ rescue => e
255
+ @donotcache = true
256
+ logger.warn("Failed to start config file watcher: #{e.message}")
257
+ end
258
+ end
259
+
260
+ def stop_config_file_watcher
261
+ if @config_listener
262
+ @config_listener.stop
263
+ @config_listener = nil
264
+ end
265
+ end
266
+
267
+ private
268
+
269
+ def load_recording_meta_exclude_patterns
270
+ if File.exist?(tracer_config_path)
271
+ config_data = YAML.load_file(tracer_config_path)
272
+ patterns = config_data.dig('filters', 'recording_meta_exclude') || []
273
+
274
+ # Validate patterns
275
+ valid_patterns = patterns.select do |pattern|
276
+ if pattern.is_a?(Hash) && pattern.key?('name') && pattern.key?('description')
277
+ true
278
+ else
279
+ logger.warn("Invalid recording_meta_exclude pattern: #{pattern.inspect}")
280
+ false
281
+ end
282
+ end
283
+
284
+ valid_patterns
285
+ else
286
+ []
287
+ end
288
+ rescue => e
289
+ logger.warn("Error loading recording meta exclude patterns: #{e.message}")
290
+ []
291
+ end
292
+
293
+ def load_tracer_config_enabled
294
+ if File.exist?(tracer_config_path)
295
+ config_data = YAML.load_file(tracer_config_path)
296
+ config_data['tracing_enabled'] != false # Default to true if not specified
297
+ else
298
+ true
299
+ end
300
+ rescue => e
301
+ logger.warn("Error loading tracer config: #{e.message}")
302
+ true
303
+ end
197
304
  end
198
305
  end
199
306
  end
@@ -10,7 +10,16 @@ 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
14
23
  );
15
24
  SQL
16
25
  end
@@ -18,8 +27,30 @@ module Codebeacon
18
27
  def self.create_indexes(database)
19
28
  end
20
29
 
21
- def insert(name, description)
22
- @db.execute("INSERT INTO metadata (name, description) VALUES (?, ?)", [name, description])
30
+ def insert(metadata)
31
+ metadata_hash = metadata.to_hash
32
+
33
+ @db.execute(<<-SQL,
34
+ INSERT INTO metadata (
35
+ name, description, caller_file, caller_method, caller_line,
36
+ caller_class, caller_defined_class, start_time, end_time,
37
+ duration_ms, trigger_type
38
+ ) VALUES (
39
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
40
+ )
41
+ SQL
42
+ metadata_hash[:name],
43
+ metadata_hash[:description],
44
+ metadata_hash[:caller_file],
45
+ metadata_hash[:caller_method],
46
+ metadata_hash[:caller_line],
47
+ metadata_hash[:caller_class],
48
+ metadata_hash[:caller_defined_class],
49
+ metadata_hash[:start_time]&.iso8601,
50
+ metadata_hash[:end_time]&.iso8601,
51
+ metadata_hash[:duration_ms],
52
+ metadata_hash[:trigger_type]
53
+ )
23
54
  end
24
55
  end
25
56
  end
@@ -30,8 +30,8 @@ module Codebeacon
30
30
  @progress_logger = Codebeacon::Tracer.logger.newProgressLogger("nodes persisted")
31
31
  end
32
32
 
33
- def save_metadata(name, description)
34
- @metadata_mapper.insert(name, description)
33
+ def save_metadata(metadata)
34
+ @metadata_mapper.insert(metadata)
35
35
  end
36
36
 
37
37
  def save_node_sources(node_sources)
@@ -0,0 +1,76 @@
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
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
+
16
+ if caller_location
17
+ capture_caller_info_from_location(caller_location)
18
+ end
19
+ end
20
+
21
+ def finish_trace
22
+ @end_time = Time.now
23
+ @duration_ms = ((@end_time - @start_time) * 1000).round(2)
24
+ end
25
+
26
+ def to_hash
27
+ {
28
+ name: @name,
29
+ description: @description,
30
+ caller_file: @caller_file,
31
+ caller_method: @caller_method,
32
+ caller_line: @caller_line,
33
+ caller_class: @caller_class,
34
+ caller_defined_class: @caller_defined_class,
35
+ start_time: @start_time,
36
+ end_time: @end_time,
37
+ duration_ms: @duration_ms,
38
+ trigger_type: @trigger_type
39
+ }
40
+ end
41
+
42
+ private
43
+
44
+ def capture_caller_info_from_location(location)
45
+ begin
46
+ @caller_file = location.absolute_path || location.path
47
+ @caller_line = location.lineno
48
+ @caller_method = location.label
49
+
50
+ # Try to determine the calling class/module from the location
51
+ @caller_class, @caller_defined_class = determine_caller_class_from_location(location)
52
+ rescue => e
53
+ Codebeacon::Tracer.logger.warn("Failed to capture caller info from location: #{e.message}") if Codebeacon::Tracer.config.debug?
54
+ end
55
+ end
56
+
57
+ def determine_caller_class_from_location(location)
58
+ caller_class = ""
59
+ caller_defined_class = ""
60
+
61
+ # Extract class information from the location label if possible
62
+ if location.label && location.label.include?('#')
63
+ # Instance method call - extract class name
64
+ class_method = location.label.split('#')
65
+ caller_class = class_method[0] if class_method.length > 1
66
+ elsif location.label && location.label.include?('.')
67
+ # Class method call - extract class name
68
+ class_method = location.label.split('.')
69
+ caller_class = class_method[0] if class_method.length > 1
70
+ end
71
+
72
+ [caller_class, caller_defined_class]
73
+ end
74
+ end
75
+ end
76
+ end
@@ -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()
@@ -95,6 +106,7 @@ module Codebeacon
95
106
  if [:call, :b_call, :return, :b_return].include?(type)
96
107
  paths << Kernel.caller(1..1)[0]
97
108
  end
109
+ paths.uniq!
98
110
  next if skip_methods?(paths)
99
111
  yield tp
100
112
  rescue => e
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Codebeacon
4
4
  module Tracer
5
- VERSION = "0.1.0"
5
+ VERSION = "0.3.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.3.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-16 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,16 +160,19 @@ 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/trace_metadata.rb
153
176
  - lib/codebeacon/tracer/src/data/tree_node_mapper.rb
154
177
  - lib/codebeacon/tracer/src/logger.rb
155
178
  - lib/codebeacon/tracer/src/models/call_tree.rb