Skip to content

Commit 4a747e8

Browse files
committed
WIP
1 parent 9a967d2 commit 4a747e8

File tree

8 files changed

+187
-5
lines changed

8 files changed

+187
-5
lines changed

sentry-ruby/lib/sentry/envelope.rb

+1-4
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ def type
1919
end
2020

2121
def to_s
22-
<<~ITEM
23-
#{JSON.generate(@headers)}
24-
#{JSON.generate(@payload)}
25-
ITEM
22+
[JSON.generate(@headers), JSON.generate(@payload)].join("\n")
2623
end
2724

2825
def serialize

sentry-ruby/lib/sentry/hub.rb

+4
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ def start_transaction(transaction: nil, custom_sampling_context: {}, instrumente
9090
sampling_context.merge!(custom_sampling_context)
9191

9292
transaction.set_initial_sample_decision(sampling_context: sampling_context)
93+
94+
#TODO-neel-profiler sample
95+
transaction.profiler.start
96+
9397
transaction
9498
end
9599

sentry-ruby/lib/sentry/profiler.rb

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# frozen_string_literal: true
2+
3+
require 'etc'
4+
require 'stackprof'
5+
require 'securerandom'
6+
7+
module Sentry
8+
class Profiler
9+
10+
VERSION = '1'
11+
PLATFORM = 'ruby'
12+
# 101 Hz in microseconds
13+
DEFAULT_INTERVAL = 1e6 / 101
14+
15+
def initialize
16+
@event_id = SecureRandom.uuid.delete('-')
17+
@started = false
18+
end
19+
20+
def start
21+
@started = StackProf.start(interval: DEFAULT_INTERVAL,
22+
mode: :wall,
23+
raw: true,
24+
aggregate: false)
25+
26+
log('Not started since running elsewhere') unless @started
27+
end
28+
29+
def stop
30+
StackProf.stop
31+
end
32+
33+
def to_hash
34+
return nil unless Sentry.initialized?
35+
36+
results = StackProf.results
37+
return nil unless results
38+
return nil if results.empty?
39+
40+
frame_map = {}
41+
42+
frames = results[:frames].to_enum.with_index.map do |frame, idx|
43+
frame_id, frame_data = frame
44+
45+
# need to map over stackprof frame ids to ours
46+
frame_map[frame_id] = idx
47+
48+
# TODO-neel module, filename, in_app
49+
{
50+
abs_path: frame_data[:file],
51+
function: frame_data[:name],
52+
lineno: frame_data[:line]
53+
}.compact
54+
end
55+
56+
idx = 0
57+
stacks = []
58+
num_seen = []
59+
60+
# extract stacks from raw
61+
# raw is a single array of [.., len_stack, *stack_frames(len_stack), num_stack_seen , ..]
62+
while (len = results[:raw][idx])
63+
idx += 1
64+
65+
# our call graph is reversed
66+
stack = results[:raw].slice(idx, len).map { |id| frame_map[id] }.compact.reverse
67+
stacks << stack
68+
69+
num_seen << results[:raw][idx + len]
70+
idx += len + 1
71+
72+
log('Unknown frame in stack') if stack.size != len
73+
end
74+
75+
idx = 0
76+
elapsed_since_start_ns = 0
77+
samples = []
78+
79+
num_seen.each_with_index do |n, i|
80+
n.times do
81+
# stackprof deltas are in microseconds
82+
delta = results[:raw_timestamp_deltas][idx]
83+
elapsed_since_start_ns += (delta * 1e3).to_i
84+
idx += 1
85+
86+
# Not sure why but some deltas are very small like 0/1 values,
87+
# they pollute our flamegraph so just ignore them for now.
88+
# Open issue at https://github.com/tmm1/stackprof/issues/201
89+
next if delta < 10
90+
91+
samples << {
92+
stack_id: i,
93+
# TODO-neel we need to patch rb_profile_frames and write our own C extension to enable threading info
94+
# till then, on multi-threaded servers like puma, we will get frames from other active threads when the one
95+
# we're profiling is idle/sleeping/waiting for IO etc
96+
# https://bugs.ruby-lang.org/issues/10602
97+
thread_id: '0',
98+
elapsed_since_start_ns: elapsed_since_start_ns.to_s
99+
}
100+
end
101+
end
102+
103+
log('Some samples thrown away') if samples.size != results[:samples]
104+
105+
if samples.size <= 2
106+
log('Not enough samples, discarding profiler')
107+
return nil
108+
end
109+
110+
profile = {
111+
frames: frames,
112+
stacks: stacks,
113+
samples: samples
114+
}
115+
116+
{
117+
event_id: @event_id,
118+
platform: PLATFORM,
119+
version: VERSION,
120+
profile: profile
121+
}
122+
end
123+
124+
private
125+
126+
def log(message)
127+
Sentry.logger.debug(LOGGER_PROGNAME) { "[Profiler] #{message}" }
128+
end
129+
130+
end
131+
end

sentry-ruby/lib/sentry/scope.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,8 @@ def os_context
309309
name: uname[:sysname] || RbConfig::CONFIG["host_os"],
310310
version: uname[:version],
311311
build: uname[:release],
312-
kernel_version: uname[:version]
312+
kernel_version: uname[:version],
313+
machine: uname[:machine]
313314
}
314315
end
315316
end

sentry-ruby/lib/sentry/transaction.rb

+11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "sentry/baggage"
4+
require "sentry/profiler"
45

56
module Sentry
67
class Transaction < Span
@@ -83,6 +84,7 @@ def initialize(
8384
@effective_sample_rate = nil
8485
@contexts = {}
8586
@measurements = {}
87+
@profiler = nil
8688
init_span_recorder
8789
end
8890

@@ -254,6 +256,9 @@ def finish(hub: nil, end_timestamp: nil)
254256
@name = UNLABELD_NAME
255257
end
256258

259+
# TODO-neel-profiler sample
260+
@profiler&.stop
261+
257262
if @sampled
258263
event = hub.current_client.event_from_transaction(self)
259264
hub.capture_event(event)
@@ -288,6 +293,12 @@ def set_context(key, value)
288293
@contexts[key] = value
289294
end
290295

296+
# The stackprof profiler instance
297+
# @return [Profiler]
298+
def profiler
299+
@profiler ||= Profiler.new
300+
end
301+
291302
protected
292303

293304
def init_span_recorder(limit = 1000)

sentry-ruby/lib/sentry/transaction_event.rb

+28
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ class TransactionEvent < Event
1717
# @return [Float, nil]
1818
attr_reader :start_timestamp
1919

20+
# @return [Hash, nil]
21+
attr_accessor :profile
22+
2023
def initialize(transaction:, **options)
2124
super(**options)
2225

@@ -32,6 +35,9 @@ def initialize(transaction:, **options)
3235

3336
finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
3437
self.spans = finished_spans.map(&:to_hash)
38+
39+
# TODO-neel-profiler cleanup sampling etc
40+
self.profile = populate_profile(transaction)
3541
end
3642

3743
# Sets the event's start_timestamp.
@@ -49,5 +55,27 @@ def to_hash
4955
data[:measurements] = @measurements
5056
data
5157
end
58+
59+
private
60+
61+
def populate_profile(transaction)
62+
return nil unless transaction.profiler
63+
64+
transaction.profiler.to_hash.merge(
65+
environment: environment,
66+
release: release,
67+
timestamp: Time.at(start_timestamp).iso8601,
68+
device: { architecture: Scope.os_context[:machine] },
69+
os: { name: Scope.os_context[:name], version: Scope.os_context[:version] },
70+
runtime: Scope.runtime_context,
71+
transaction: {
72+
id: event_id,
73+
name: transaction.name,
74+
trace_id: transaction.trace_id,
75+
# TODO-neel-profiler stubbed for now, see thread_id note in profiler.rb
76+
active_thead_id: '0'
77+
}
78+
)
79+
end
5280
end
5381
end

sentry-ruby/lib/sentry/transport.rb

+8
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def send_envelope(envelope)
6161

6262
if data
6363
log_info("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry")
64+
File.open('/tmp/dump', 'w') { |file| file.write(data) } if envelope.items.map(&:type).include?('profile')
6465
send_data(data)
6566
end
6667
end
@@ -154,6 +155,13 @@ def envelope_from_event(event)
154155
event_payload
155156
)
156157

158+
if event.is_a?(TransactionEvent) && event.profile
159+
envelope.add_item(
160+
{ type: 'profile', content_type: 'application/json' },
161+
event.profile
162+
)
163+
end
164+
157165
client_report_headers, client_report_payload = fetch_pending_client_report
158166
envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
159167

sentry-ruby/sentry-ruby.gemspec

+2
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ Gem::Specification.new do |spec|
2121
spec.require_paths = ["lib"]
2222

2323
spec.add_dependency "concurrent-ruby", '~> 1.0', '>= 1.0.2'
24+
# TODO-neel-profiler make peer dep
25+
spec.add_dependency "stackprof", '~> 0.2.23'
2426
end

0 commit comments

Comments
 (0)