Skip to content

Commit 8909e86

Browse files
committed
Merge remote-tracking branch 'origin/master' into neel/metrics/code-locations
2 parents 088ccad + b0b73fd commit 8909e86

File tree

6 files changed

+120
-20
lines changed

6 files changed

+120
-20
lines changed

CHANGELOG.md

+16-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- Add `Sentry::Metrics.timing` API for measuring block duration [#2254](https://github.com/getsentry/sentry-ruby/pull/2254)
1111
- Add metric summaries on spans [#2255](https://github.com/getsentry/sentry-ruby/pull/2255)
1212
- Add `config.metrics.before_emit` callback [#2258](https://github.com/getsentry/sentry-ruby/pull/2258)
13+
- Add code locations for metrics [#2263](https://github.com/getsentry/sentry-ruby/pull/2263)
1314

1415
The SDK now supports recording and aggregating metrics. A new thread will be started
1516
for aggregation and will flush the pending data to Sentry every 5 seconds.
@@ -45,11 +46,14 @@
4546
Sentry::Metrics.timing('how_long') { sleep(1) }
4647
# timing - measure duration of code block in other duraton units
4748
Sentry::Metrics.timing('how_long_ms', unit: 'millisecond') { sleep(0.5) }
49+
```
50+
51+
You can filter some keys or update tags on the fly with the `before_emit` callback, which will be triggered before a metric is aggregated.
4852

49-
# add a before_emit callback to filter keys or update tags
53+
```ruby
5054
Sentry.init do |config|
5155
# ...
52-
config.metrics.enabled = true
56+
# the 'foo' metric will be filtered and the tags will be updated to add :bar and remove :baz
5357
config.metrics.before_emit = lambda do |key, tags|
5458
return nil if key == 'foo'
5559
tags[:bar] = 42
@@ -59,6 +63,16 @@
5963
end
6064
```
6165

66+
By default, the SDK will send code locations for unique metrics (defined by type, key and unit) once a day and with every startup/shutdown of your application.
67+
You can turn this off with the following:
68+
69+
```ruby
70+
Sentry.init do |config|
71+
# ...
72+
config.metrics.enable_code_locations = false
73+
end
74+
```
75+
6276
### Bug Fixes
6377

6478
- Fix undefined method 'constantize' issue in `sentry-resque` ([#2248](https://github.com/getsentry/sentry-ruby/pull/2248))

sentry-ruby/lib/sentry/metrics.rb

+7-4
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,20 @@ def gauge(key, value, unit: 'none', tags: {}, timestamp: nil)
3535

3636
def timing(key, unit: 'second', tags: {}, timestamp: nil, &block)
3737
return unless block_given?
38-
return unless DURATION_UNITS.include?(unit)
38+
return yield unless DURATION_UNITS.include?(unit)
3939

40-
Sentry.with_child_span(op: OP_NAME, description: key) do |span|
40+
result, value = Sentry.with_child_span(op: OP_NAME, description: key) do |span|
4141
tags.each { |k, v| span.set_tag(k, v.is_a?(Array) ? v.join(', ') : v.to_s) } if span
4242

4343
start = Timing.send(unit.to_sym)
44-
yield
44+
result = yield
4545
value = Timing.send(unit.to_sym) - start
4646

47-
Sentry.metrics_aggregator&.add(:d, key, value, unit: unit, tags: tags, timestamp: timestamp)
47+
[result, value]
4848
end
49+
50+
Sentry.metrics_aggregator&.add(:d, key, value, unit: unit, tags: tags, timestamp: timestamp)
51+
result
4952
end
5053
end
5154
end

sentry-ruby/lib/sentry/metrics/aggregator.rb

+6-6
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class Aggregator
2323
}
2424

2525
# exposed only for testing
26-
attr_reader :thread, :buckets, :flush_shift
26+
attr_reader :thread, :buckets, :flush_shift, :code_locations
2727

2828
def initialize(configuration, client)
2929
@client = client
@@ -55,7 +55,8 @@ def add(type,
5555
value,
5656
unit: 'none',
5757
tags: {},
58-
timestamp: nil)
58+
timestamp: nil,
59+
stacklevel: nil)
5960
return unless ensure_thread
6061
return unless METRIC_TYPES.keys.include?(type)
6162

@@ -72,7 +73,7 @@ def add(type,
7273
bucket_key = [type, key, unit, serialized_tags]
7374

7475
added = @mutex.synchronize do
75-
record_code_location(type, key, unit, timestamp) if @enable_code_locations
76+
record_code_location(type, key, unit, timestamp, stacklevel: stacklevel) if @enable_code_locations
7677
process_bucket(bucket_timestamp, bucket_key, type, value)
7778
end
7879

@@ -247,13 +248,12 @@ def process_bucket(timestamp, key, type, value)
247248
end
248249
end
249250

250-
# TODO-neel-metrics stacklevel
251-
def record_code_location(type, key, unit, timestamp)
251+
def record_code_location(type, key, unit, timestamp, stacklevel: nil)
252252
meta_key = [type, key, unit]
253253
start_of_day = Time.utc(timestamp.year, timestamp.month, timestamp.day).to_i
254254

255255
@code_locations[start_of_day] ||= {}
256-
@code_locations[start_of_day][meta_key] ||= @stacktrace_builder.metrics_code_location(caller[DEFAULT_STACKLEVEL])
256+
@code_locations[start_of_day][meta_key] ||= @stacktrace_builder.metrics_code_location(caller[stacklevel || DEFAULT_STACKLEVEL])
257257
end
258258
end
259259
end

sentry-ruby/lib/sentry/metrics/configuration.rb

+6
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ def before_emit=(value)
4242

4343
@before_emit = value
4444
end
45+
46+
def before_emit=(value)
47+
check_callable!("metrics.before_emit", value)
48+
49+
@before_emit = value
50+
end
4551
end
4652
end
4753
end

sentry-ruby/spec/sentry/metrics/aggregator_spec.rb

+81-6
Original file line numberDiff line numberDiff line change
@@ -270,10 +270,58 @@
270270
end
271271
end
272272
end
273+
274+
describe 'code location reporting' do
275+
it 'does not record location if off' do
276+
perform_basic_setup do |config|
277+
config.metrics.enabled = true
278+
config.metrics.enable_code_locations = false
279+
end
280+
281+
subject.add(:c, 'incr', 1)
282+
expect(subject.code_locations).to eq({})
283+
end
284+
285+
it 'records the code location with a timestamp for the day' do
286+
subject.add(:c, 'incr', 1, unit: 'second', stacklevel: 3)
287+
288+
timestamp = Time.now.utc
289+
start_of_day = Time.utc(timestamp.year, timestamp.month, timestamp.day).to_i
290+
expect(subject.code_locations.keys.first).to eq(start_of_day)
291+
end
292+
293+
it 'has the code location keyed with mri (metric resource identifier) from type/key/unit' do
294+
subject.add(:c, 'incr', 1, unit: 'second', stacklevel: 3)
295+
mri = subject.code_locations.values.first.keys.first
296+
expect(mri).to eq([:c, 'incr', 'second'])
297+
end
298+
299+
it 'has the code location information in the hash' do
300+
subject.add(:c, 'incr', 1, unit: 'second', stacklevel: 3)
301+
302+
location = subject.code_locations.values.first.values.first
303+
expect(location).to include(:abs_path, :filename, :pre_context, :context_line, :post_context, :lineno)
304+
expect(location[:abs_path]).to match(/aggregator_spec.rb/)
305+
expect(location[:filename]).to match(/aggregator_spec.rb/)
306+
expect(location[:context_line]).to include("subject.add(:c, 'incr', 1, unit: 'second', stacklevel: 3)")
307+
end
308+
309+
it 'does not add code location for the same mri twice' do
310+
subject.add(:c, 'incr', 1, unit: 'second', stacklevel: 3)
311+
subject.add(:c, 'incr', 1, unit: 'second', stacklevel: 3)
312+
expect(subject.code_locations.values.first.size).to eq(1)
313+
end
314+
315+
it 'adds code location for different mris twice' do
316+
subject.add(:c, 'incr', 1, unit: 'second', stacklevel: 3)
317+
subject.add(:c, 'incr', 1, unit: 'none', stacklevel: 3)
318+
expect(subject.code_locations.values.first.size).to eq(2)
319+
end
320+
end
273321
end
274322

275323
describe '#flush' do
276-
context 'with empty buckets' do
324+
context 'with empty buckets and empty locations' do
277325
it 'returns early and does nothing' do
278326
expect(sentry_envelopes.count).to eq(0)
279327
subject.flush
@@ -289,11 +337,11 @@
289337
before do
290338
allow(Time).to receive(:now).and_return(fake_time)
291339
10.times { subject.add(:c, 'incr', 1) }
292-
5.times { |i| subject.add(:d, 'dist', i, unit: 'second', tags: { "foö$-bar" => "snöwmän% 23{}" }) }
340+
5.times { |i| subject.add(:d, 'disöt', i, unit: 'second', tags: { "foö$-bar" => "snöwmän% 23{}" }) }
293341

294342
allow(Time).to receive(:now).and_return(fake_time + 9)
295343
5.times { subject.add(:c, 'incr', 1) }
296-
5.times { |i| subject.add(:d, 'dist', i + 5, unit: 'second', tags: { "foö$-bar" => "snöwmän% 23{}" }) }
344+
5.times { |i| subject.add(:d, 'disöt', i + 5, unit: 'second', tags: { "foö$-bar" => "snöwmän% 23{}" }) }
297345

298346
expect(subject.buckets.keys).to eq([fake_time.to_i - 3, fake_time.to_i + 7])
299347
expect(subject.buckets.values[0].length).to eq(2)
@@ -311,6 +359,11 @@
311359
expect(subject.buckets.values[0].length).to eq(2)
312360
end
313361

362+
it 'empties the pending code locations in place' do
363+
subject.flush
364+
expect(subject.code_locations).to eq({})
365+
end
366+
314367
it 'calls the background worker' do
315368
expect(Sentry.background_worker).to receive(:perform)
316369
subject.flush
@@ -327,10 +380,32 @@
327380

328381
incr, dist = item.payload.split("\n")
329382
expect(incr).to eq("incr@none:10.0|c|#environment:test,release:test-release|T#{fake_time.to_i - 3}")
330-
expect(dist).to eq("dist@second:0.0:1.0:2.0:3.0:4.0|d|" +
383+
expect(dist).to eq("dis_t@second:0.0:1.0:2.0:3.0:4.0|d|" +
331384
"#environment:test,fo_-bar:snöwmän 23{},release:test-release|" +
332385
"T#{fake_time.to_i - 3}")
333386
end
387+
388+
it 'sends the pending code locations in metric_meta envelope item with correct payload' do
389+
subject.flush
390+
391+
envelope = sentry_envelopes.first
392+
expect(envelope.headers).to eq({})
393+
394+
item = envelope.items.last
395+
expect(item.headers).to eq({ type: 'metric_meta', content_type: 'application/json' })
396+
expect(item.payload[:timestamp]).to be_a(Integer)
397+
398+
mapping = item.payload[:mapping]
399+
expect(mapping.keys).to eq(['c:incr@none', 'd:dis_t@second'])
400+
401+
location_1 = mapping['c:incr@none'].first
402+
expect(location_1[:type]).to eq('location')
403+
expect(location_1).to include(:abs_path, :filename, :pre_context, :context_line, :post_context, :lineno)
404+
405+
location_2 = mapping['d:dis_t@second'].first
406+
expect(location_2[:type]).to eq('location')
407+
expect(location_2).to include(:abs_path, :filename, :pre_context, :context_line, :post_context, :lineno)
408+
end
334409
end
335410

336411
context 'with force' do
@@ -355,11 +430,11 @@
355430

356431
incr1, dist1, incr2, dist2 = item.payload.split("\n")
357432
expect(incr1).to eq("incr@none:10.0|c|#environment:test,release:test-release|T#{fake_time.to_i - 3}")
358-
expect(dist1).to eq("dist@second:0.0:1.0:2.0:3.0:4.0|d|" +
433+
expect(dist1).to eq("dis_t@second:0.0:1.0:2.0:3.0:4.0|d|" +
359434
"#environment:test,fo_-bar:snöwmän 23{},release:test-release|" +
360435
"T#{fake_time.to_i - 3}")
361436
expect(incr2).to eq("incr@none:5.0|c|#environment:test,release:test-release|T#{fake_time.to_i + 7}")
362-
expect(dist2).to eq("dist@second:5.0:6.0:7.0:8.0:9.0|d|" +
437+
expect(dist2).to eq("dis_t@second:5.0:6.0:7.0:8.0:9.0|d|" +
363438
"#environment:test,fo_-bar:snöwmän 23{},release:test-release|" +
364439
"T#{fake_time.to_i + 7}")
365440
end

sentry-ruby/spec/sentry/metrics_spec.rb

+4-2
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@
9191

9292
it 'does nothing with a non-duration unit' do
9393
expect(aggregator).not_to receive(:add)
94-
described_class.timing('foo', unit: 'ratio') { }
94+
result = described_class.timing('foo', unit: 'ratio') { 42 }
95+
expect(result).to eq(42)
9596
end
9697

9798
it 'measures time taken as distribution and passes through args to aggregator' do
@@ -104,7 +105,8 @@
104105
timestamp: fake_time
105106
)
106107

107-
described_class.timing('foo', unit: 'millisecond', tags: { fortytwo: 42 }, timestamp: fake_time) { sleep(0.1) }
108+
result = described_class.timing('foo', unit: 'millisecond', tags: { fortytwo: 42 }, timestamp: fake_time) { sleep(0.1); 42 }
109+
expect(result).to eq(42)
108110
end
109111

110112
context 'with running transaction' do

0 commit comments

Comments
 (0)