Skip to content

Commit 53afc20

Browse files
committed
Category specific runtime callback
1 parent 34bb303 commit 53afc20

File tree

2 files changed

+154
-17
lines changed

2 files changed

+154
-17
lines changed

debug_runtime_callback.rb

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
require 'json'
2+
require 'webrick'
3+
require 'fiddle'
4+
require 'datadog'
5+
6+
# Simple HTTP server to capture crash reports
7+
def start_crash_server(port)
8+
server = WEBrick::HTTPServer.new(
9+
Port: port,
10+
Logger: WEBrick::Log.new(File.open(File::NULL, "w")),
11+
AccessLog: []
12+
)
13+
14+
crash_report = nil
15+
16+
server.mount_proc '/' do |req, res|
17+
if req.request_method == 'POST'
18+
body = req.body
19+
crash_report = JSON.parse(body) rescue body
20+
puts "=== CRASH REPORT RECEIVED ==="
21+
puts JSON.pretty_generate(crash_report) if crash_report.is_a?(Hash)
22+
puts "=== END CRASH REPORT ==="
23+
end
24+
res.body = '{}'
25+
end
26+
27+
Thread.new { server.start }
28+
[server, proc { crash_report }]
29+
end
30+
31+
# Nested Ruby functions to create a meaningful stack
32+
def level_5
33+
puts "In level_5 - about to crash"
34+
# Trigger segfault
35+
Fiddle.free(42)
36+
end
37+
38+
def level_4
39+
puts "In level_4"
40+
level_5
41+
end
42+
43+
def level_3
44+
puts "In level_3"
45+
level_4
46+
end
47+
48+
def level_2
49+
puts "In level_2"
50+
level_3
51+
end
52+
53+
def level_1
54+
puts "In level_1"
55+
level_2
56+
end
57+
58+
def main_crash_test
59+
puts "Starting crash test with nested functions"
60+
level_1
61+
end
62+
63+
# Main test
64+
puts "Starting standalone crashtracker test..."
65+
66+
# Start server
67+
server, get_crash_report = start_crash_server(9999)
68+
sleep 0.1 # Let server start
69+
70+
puts "Forking process to test crashtracker..."
71+
72+
pid = fork do
73+
begin
74+
puts "Child process started"
75+
76+
# Configure crashtracker
77+
Datadog.configure do |c|
78+
c.agent.host = '127.0.0.1'
79+
c.agent.port = 9999
80+
end
81+
82+
puts "Crashtracker configured, starting crash test..."
83+
84+
# Call our nested function that will crash
85+
main_crash_test
86+
87+
rescue => e
88+
puts "Unexpected error in child: #{e}"
89+
exit 1
90+
end
91+
end
92+
93+
# Wait for child process
94+
Process.wait(pid)
95+
puts "Child process finished with status: #{$?.exitstatus}"
96+
97+
# Give server time to receive the crash report
98+
sleep 1
99+
100+
# Get and save the crash report
101+
crash_report = get_crash_report.call
102+
if crash_report
103+
# Write full crash report to tmp file
104+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
105+
crash_file = "/tmp/crashtracker_report_#{timestamp}.json"
106+
File.write(crash_file, JSON.pretty_generate(crash_report))
107+
puts "\n=== CRASH REPORT SAVED ==="
108+
puts "Full crash report written to: #{crash_file}"
109+
110+
puts "\n=== RUNTIME STACK ANALYSIS ==="
111+
if crash_report.is_a?(Hash) && crash_report.dig('payload', 0, 'message')
112+
message = JSON.parse(crash_report.dig('payload', 0, 'message'))
113+
runtime_stack = message['experimental']['runtime_stack']
114+
if runtime_stack
115+
puts "Runtime stack format: #{runtime_stack['format']}"
116+
puts "Number of frames captured: #{runtime_stack['frames']&.length || 0}"
117+
puts "\nStack frames:"
118+
runtime_stack['frames']&.each_with_index do |frame, i|
119+
puts " [#{i}] #{frame['function']} @ #{frame['file']}:#{frame['line']}"
120+
end
121+
122+
runtime_stack_file = "/tmp/runtime_stack_#{timestamp}.json"
123+
File.write(runtime_stack_file, JSON.pretty_generate(runtime_stack))
124+
puts "\nRuntime stack saved to: #{runtime_stack_file}"
125+
else
126+
puts "No runtime_stack found in crash report"
127+
end
128+
else
129+
puts "Could not parse crash report structure"
130+
end
131+
else
132+
puts "No crash report received"
133+
end
134+
135+
# Cleanup
136+
server.shutdown
137+
puts "\nTest complete."

ext/libdatadog_api/crashtracker.c

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@
3737
#include <sys/mman.h>
3838
#include <unistd.h>
3939
#include <errno.h>
40+
#include <string.h>
41+
42+
// Helper macro for creating CharSlice from C strings at runtime
43+
// This safely converts a const char* to ddog_CharSlice with proper length calculation
44+
#define DDOG_CHARSLICE_FROM_CSTR(cstr) \
45+
((cstr != NULL) ? (ddog_CharSlice){ .ptr = (cstr), .len = strlen(cstr) } : (ddog_CharSlice){ .ptr = NULL, .len = 0 })
4046

4147
// Include profiling stack walking functionality
4248
// Note: rb_iseq_path and rb_iseq_base_label are already declared in MJIT header
@@ -52,8 +58,7 @@ static VALUE _native_register_runtime_stack_callback(VALUE _self, VALUE callback
5258
static VALUE _native_is_runtime_callback_registered(DDTRACE_UNUSED VALUE _self);
5359

5460
static void ruby_runtime_stack_callback(
55-
void (*emit_frame)(const ddog_crasht_RuntimeStackFrame*),
56-
void (*emit_stacktrace_string)(const char*)
61+
void (*emit_frame)(const ddog_crasht_RuntimeStackFrame*)
5762
);
5863

5964
static bool first_init = true;
@@ -67,7 +72,7 @@ static bool is_pointer_readable(const void *ptr, size_t size) {
6772
size_t pages = ((char*)ptr + size - (char*)aligned_ptr + page_size - 1) / page_size;
6873

6974
// Stack-allocate a small buffer for mincore results.. should be safe?
70-
char vec[16];
75+
unsigned char vec[16];
7176
if (pages > 16) return false; // Too big to check safely
7277

7378
return mincore(aligned_ptr, pages * page_size, vec) == 0;
@@ -248,12 +253,9 @@ static VALUE _native_stop(DDTRACE_UNUSED VALUE _self) {
248253
return Qtrue;
249254
}
250255

251-
252256
static void ruby_runtime_stack_callback(
253-
void (*emit_frame)(const ddog_crasht_RuntimeStackFrame*),
254-
void (*emit_stacktrace_string)(const char*)
257+
void (*emit_frame)(const ddog_crasht_RuntimeStackFrame*)
255258
) {
256-
(void)emit_stacktrace_string;
257259

258260
VALUE current_thread = rb_thread_current();
259261
if (current_thread == Qnil) return;
@@ -337,8 +339,8 @@ static void ruby_runtime_stack_callback(
337339
}
338340

339341
ddog_crasht_RuntimeStackFrame frame = {
340-
.function_name = function_name,
341-
.file_name = file_name,
342+
.function_name = DDOG_CHARSLICE_FROM_CSTR(function_name),
343+
.file_name = DDOG_CHARSLICE_FROM_CSTR(file_name),
342344
.line_number = line_no,
343345
.column_number = 0
344346
};
@@ -365,8 +367,8 @@ static void ruby_runtime_stack_callback(
365367
}
366368

367369
ddog_crasht_RuntimeStackFrame frame = {
368-
.function_name = function_name,
369-
.file_name = file_name,
370+
.function_name = DDOG_CHARSLICE_FROM_CSTR(function_name),
371+
.file_name = DDOG_CHARSLICE_FROM_CSTR(file_name),
370372
.line_number = 0,
371373
.column_number = 0
372374
};
@@ -385,17 +387,15 @@ static VALUE _native_register_runtime_stack_callback(DDTRACE_UNUSED VALUE _self,
385387
rb_raise(rb_eArgError, "Invalid callback_type. Only :frame is supported");
386388
}
387389

388-
enum ddog_crasht_CallbackResult result = ddog_crasht_register_runtime_stack_callback(
389-
ruby_runtime_stack_callback,
390-
DDOG_CRASHT_CALLBACK_TYPE_FRAME
390+
enum ddog_crasht_CallbackResult result = ddog_crasht_register_runtime_frame_callback(
391+
ruby_runtime_stack_callback
391392
);
392393

393394
switch (result) {
394395
case DDOG_CRASHT_CALLBACK_RESULT_OK:
395396
return Qtrue;
396-
case DDOG_CRASHT_CALLBACK_RESULT_ERROR:
397-
rb_raise(rb_eRuntimeError, "Failed to register runtime callback");
398-
break;
397+
default:
398+
return Qfalse;
399399
}
400400

401401
return Qfalse;

0 commit comments

Comments
 (0)