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 +4 -4
- data/bin/codebeacon +18 -0
- data/lib/codebeacon/tracer/src/configuration.rb +133 -6
- data/lib/codebeacon/tracer/src/data/metadata_mapper.rb +36 -3
- data/lib/codebeacon/tracer/src/data/persistence_manager.rb +15 -25
- data/lib/codebeacon/tracer/src/data/safe_serializer.rb +40 -0
- data/lib/codebeacon/tracer/src/data/trace_metadata.rb +78 -0
- data/lib/codebeacon/tracer/src/data/tree_node_mapper.rb +9 -6
- data/lib/codebeacon/tracer/src/data/type_detector.rb +20 -0
- data/lib/codebeacon/tracer/src/models/node_builder.rb +4 -0
- data/lib/codebeacon/tracer/src/models/tree_node.rb +3 -2
- data/lib/codebeacon/tracer/src/rails/middleware.rb +5 -1
- data/lib/codebeacon/tracer/src/tracer.rb +20 -16
- data/lib/codebeacon/tracer/version.rb +1 -1
- data/lib/codebeacon-tracer.rb +58 -25
- metadata +29 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 47ec3d248b6f0358c3c36310a7b39c506982695565e453d11580ae20b1f28582
|
|
4
|
+
data.tar.gz: 8f6da9ed15ca33105c73fead83856a014c718853cbf30db5641719e5d6fdfc36
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
133
|
-
@
|
|
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(
|
|
22
|
-
|
|
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(
|
|
34
|
-
@metadata_mapper.insert(
|
|
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
|
-
|
|
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
|
|
99
|
-
if node.method == :initialize
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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?(
|
|
106
|
-
|
|
107
|
-
|
|
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
|
data/lib/codebeacon-tracer.rb
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jonathan Conley
|
|
8
8
|
autorequire:
|
|
9
|
-
bindir:
|
|
9
|
+
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
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
|