Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add metrics code locations #2263

Merged
merged 24 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
be9422d
metrics wip
sl0thentr0py Feb 16, 2024
ee59473
aggregator add impl
sl0thentr0py Feb 21, 2024
675292b
config and apis
sl0thentr0py Feb 21, 2024
73e5e04
encode statsd format and sanitization
sl0thentr0py Feb 22, 2024
9569bf9
capture envelope
sl0thentr0py Feb 22, 2024
8efde86
fix set
sl0thentr0py Feb 26, 2024
512408c
add transaction name to tags
sl0thentr0py Feb 26, 2024
bdac066
Specs
sl0thentr0py Feb 27, 2024
3f51c15
changelog
sl0thentr0py Feb 27, 2024
c1217cd
metric specs
sl0thentr0py Feb 27, 2024
8f9dabb
incr -> increment
sl0thentr0py Feb 29, 2024
ff3814f
Move config to separate metrics obj
sl0thentr0py Mar 5, 2024
f7163ef
Use scope name/source, not transaction
sl0thentr0py Mar 5, 2024
ff9792f
Remove is_json for envelope and use string check
sl0thentr0py Mar 11, 2024
9ba1a4e
trigger ci
sl0thentr0py Mar 11, 2024
478a78b
trigger ci
sl0thentr0py Mar 11, 2024
0e87bab
remove io-console pin
sl0thentr0py Mar 11, 2024
e6bbb90
Add Sentry::Metrics.timing API to measure blocks
sl0thentr0py Mar 4, 2024
7567daa
Merge remote-tracking branch 'origin/master' into neel/metrics/timing
sl0thentr0py Mar 12, 2024
116318b
Metric summaries on span
sl0thentr0py Mar 5, 2024
0df891d
Merge remote-tracking branch 'origin/master' into neel/metrics/span-a…
sl0thentr0py Mar 12, 2024
da14eaf
Add config.metrics.before_emit callback
sl0thentr0py Mar 8, 2024
088ccad
code locations
sl0thentr0py Mar 8, 2024
4107fd4
Merge remote-tracking branch 'origin/master' into neel/metrics/code-l…
sl0thentr0py Mar 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Metric summaries on span
* add a `LocalAggregator` instance on spans to duplicate metrics on the
  span as a gauge metric
* proxy the main aggregator add calls to the local aggregator if a span
  is running
* start a `metrics.timing` span in the `Sentry::Metrics.timing` API
  • Loading branch information
sl0thentr0py committed Mar 12, 2024
commit 116318bfe2b2145f96f4c6a66e541cc0fd8d1edf
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Add [Metrics](https://docs.sentry.io/product/metrics/) support
- Add main APIs and `Aggregator` thread [#2247](https://github.com/getsentry/sentry-ruby/pull/2247)
- Add `Sentry::Metrics.timing` API for measuring block duration [#2254](https://github.com/getsentry/sentry-ruby/pull/2254)
- Add metric summaries on spans [#2255](https://github.com/getsentry/sentry-ruby/pull/2255)

The SDK now supports recording and aggregating metrics. A new thread will be started
for aggregation and will flush the pending data to Sentry every 5 seconds.
Expand Down Expand Up @@ -39,6 +40,7 @@
Sentry::Metrics.set('user_view', 'jane')

# timing - measure duration of code block, defaults to seconds
# will also automatically create a `metric.timing` span
Sentry::Metrics.timing('how_long') { sleep(1) }
# timing - measure duration of code block in other duraton units
Sentry::Metrics.timing('how_long_ms', unit: 'millisecond') { sleep(0.5) }
Expand Down
15 changes: 10 additions & 5 deletions sentry-ruby/lib/sentry/metrics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
INFORMATION_UNITS = %w[bit byte kilobyte kibibyte megabyte mebibyte gigabyte gibibyte terabyte tebibyte petabyte pebibyte exabyte exbibyte]
FRACTIONAL_UNITS = %w[ratio percent]

OP_NAME = 'metric.timing'

class << self
def increment(key, value = 1.0, unit: 'none', tags: {}, timestamp: nil)
Sentry.metrics_aggregator&.add(:c, key, value, unit: unit, tags: tags, timestamp: timestamp)
Expand All @@ -32,15 +34,18 @@
end

def timing(key, unit: 'second', tags: {}, timestamp: nil, &block)
return unless Sentry.metrics_aggregator
return unless block_given?
return unless DURATION_UNITS.include?(unit)

start = Timing.send(unit.to_sym)
yield
value = Timing.send(unit.to_sym) - start
Sentry.with_child_span(op: OP_NAME, description: key) do |span|
tags.each { |k, v| span.set_tag(k, v.is_a?(Array) ? v.join(', ') : v.to_s) } if span

Check warning on line 41 in sentry-ruby/lib/sentry/metrics.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics.rb#L40-L41

Added lines #L40 - L41 were not covered by tests

start = Timing.send(unit.to_sym)
yield
value = Timing.send(unit.to_sym) - start

Check warning on line 45 in sentry-ruby/lib/sentry/metrics.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics.rb#L43-L45

Added lines #L43 - L45 were not covered by tests

Sentry.metrics_aggregator.add(:d, key, value, unit: unit, tags: tags, timestamp: timestamp)
Sentry.metrics_aggregator&.add(:d, key, value, unit: unit, tags: tags, timestamp: timestamp)

Check warning on line 47 in sentry-ruby/lib/sentry/metrics.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics.rb#L47

Added line #L47 was not covered by tests
end
end
end
end
Expand Down
24 changes: 20 additions & 4 deletions sentry-ruby/lib/sentry/metrics/aggregator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,23 @@
serialized_tags = serialize_tags(get_updated_tags(tags))
bucket_key = [type, key, unit, serialized_tags]

@mutex.synchronize do
added = @mutex.synchronize do

Check warning on line 62 in sentry-ruby/lib/sentry/metrics/aggregator.rb

View check run for this annotation

Codecov / codecov/patch

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

Added line #L62 was not covered by tests
@buckets[bucket_timestamp] ||= {}

if @buckets[bucket_timestamp][bucket_key]
@buckets[bucket_timestamp][bucket_key].add(value)
if (metric = @buckets[bucket_timestamp][bucket_key])
old_weight = metric.weight
metric.add(value)
metric.weight - old_weight

Check warning on line 68 in sentry-ruby/lib/sentry/metrics/aggregator.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics/aggregator.rb#L65-L68

Added lines #L65 - L68 were not covered by tests
else
@buckets[bucket_timestamp][bucket_key] = METRIC_TYPES[type].new(value)
metric = METRIC_TYPES[type].new(value)
@buckets[bucket_timestamp][bucket_key] = metric
metric.weight

Check warning on line 72 in sentry-ruby/lib/sentry/metrics/aggregator.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics/aggregator.rb#L70-L72

Added lines #L70 - L72 were not covered by tests
end
end

# for sets, we pass on if there was a new entry to the local gauge
local_value = type == :s ? added : value
process_span_aggregator(bucket_key, local_value)

Check warning on line 78 in sentry-ruby/lib/sentry/metrics/aggregator.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics/aggregator.rb#L77-L78

Added lines #L77 - L78 were not covered by tests
end

def flush(force: false)
Expand Down Expand Up @@ -179,6 +187,14 @@

updated_tags
end

def process_span_aggregator(key, value)
scope = Sentry.get_current_scope
return nil unless scope && scope.span
return nil if scope.transaction_source_low_quality?

Check warning on line 194 in sentry-ruby/lib/sentry/metrics/aggregator.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics/aggregator.rb#L192-L194

Added lines #L192 - L194 were not covered by tests

scope.span.metrics_local_aggregator.add(key, value)

Check warning on line 196 in sentry-ruby/lib/sentry/metrics/aggregator.rb

View check run for this annotation

Codecov / codecov/patch

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

Added line #L196 was not covered by tests
end
end
end
end
53 changes: 53 additions & 0 deletions sentry-ruby/lib/sentry/metrics/local_aggregator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

module Sentry
module Metrics
class LocalAggregator
# exposed only for testing
attr_reader :buckets

def initialize
@buckets = {}

Check warning on line 10 in sentry-ruby/lib/sentry/metrics/local_aggregator.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics/local_aggregator.rb#L10

Added line #L10 was not covered by tests
end

def add(key, value)
if @buckets[key]
@buckets[key].add(value)

Check warning on line 15 in sentry-ruby/lib/sentry/metrics/local_aggregator.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics/local_aggregator.rb#L14-L15

Added lines #L14 - L15 were not covered by tests
else
@buckets[key] = GaugeMetric.new(value)

Check warning on line 17 in sentry-ruby/lib/sentry/metrics/local_aggregator.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics/local_aggregator.rb#L17

Added line #L17 was not covered by tests
end
end

def to_hash
return nil if @buckets.empty?

Check warning on line 22 in sentry-ruby/lib/sentry/metrics/local_aggregator.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics/local_aggregator.rb#L22

Added line #L22 was not covered by tests

@buckets.map do |bucket_key, metric|
type, key, unit, tags = bucket_key

Check warning on line 25 in sentry-ruby/lib/sentry/metrics/local_aggregator.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics/local_aggregator.rb#L24-L25

Added lines #L24 - L25 were not covered by tests

payload_key = "#{type}:#{key}@#{unit}"

Check warning on line 27 in sentry-ruby/lib/sentry/metrics/local_aggregator.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics/local_aggregator.rb#L27

Added line #L27 was not covered by tests
payload_value = {
tags: deserialize_tags(tags),

Check warning on line 29 in sentry-ruby/lib/sentry/metrics/local_aggregator.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics/local_aggregator.rb#L29

Added line #L29 was not covered by tests
min: metric.min,
max: metric.max,
count: metric.count,
sum: metric.sum
}

[payload_key, payload_value]

Check warning on line 36 in sentry-ruby/lib/sentry/metrics/local_aggregator.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics/local_aggregator.rb#L36

Added line #L36 was not covered by tests
end.to_h
end

private

def deserialize_tags(tags)
tags.inject({}) do |h, tag|
k, v = tag
old = h[k]

Check warning on line 45 in sentry-ruby/lib/sentry/metrics/local_aggregator.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics/local_aggregator.rb#L43-L45

Added lines #L43 - L45 were not covered by tests
# make it an array if key repeats
h[k] = old ? (old.is_a?(Array) ? old << v : [old, v]) : v
h

Check warning on line 48 in sentry-ruby/lib/sentry/metrics/local_aggregator.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/metrics/local_aggregator.rb#L47-L48

Added lines #L47 - L48 were not covered by tests
end
end
end
end
end
17 changes: 16 additions & 1 deletion sentry-ruby/lib/sentry/span.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "securerandom"
require "sentry/metrics/local_aggregator"

module Sentry
class Span
Expand Down Expand Up @@ -149,7 +150,7 @@

# @return [Hash]
def to_hash
{
hash = {
trace_id: @trace_id,
span_id: @span_id,
parent_span_id: @parent_span_id,
Expand All @@ -161,6 +162,11 @@
tags: @tags,
data: @data
}

summary = metrics_summary
hash[:_metrics_summary] = summary if summary

hash
end

# Returns the span's context that can be used to embed in an Event.
Expand Down Expand Up @@ -268,5 +274,14 @@
def set_tag(key, value)
@tags[key] = value
end

# Collects gauge metrics on the span for metric summaries.
def metrics_local_aggregator
@metrics_local_aggregator ||= Sentry::Metrics::LocalAggregator.new

Check warning on line 280 in sentry-ruby/lib/sentry/span.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/span.rb#L280

Added line #L280 was not covered by tests
end

def metrics_summary
@metrics_local_aggregator&.to_hash
end
end
end
5 changes: 5 additions & 0 deletions sentry-ruby/lib/sentry/transaction_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
# @return [Hash, nil]
attr_accessor :profile

# @return [Hash, nil]
attr_accessor :metrics_summary

def initialize(transaction:, **options)
super(**options)

Expand All @@ -29,6 +32,7 @@
self.tags = transaction.tags
self.dynamic_sampling_context = transaction.get_baggage.dynamic_sampling_context
self.measurements = transaction.measurements
self.metrics_summary = transaction.metrics_summary

finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
self.spans = finished_spans.map(&:to_hash)
Expand All @@ -49,6 +53,7 @@
data[:spans] = @spans.map(&:to_hash) if @spans
data[:start_timestamp] = @start_timestamp
data[:measurements] = @measurements
data[:_metrics_summary] = @metrics_summary if @metrics_summary

Check warning on line 56 in sentry-ruby/lib/sentry/transaction_event.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/transaction_event.rb#L56

Added line #L56 was not covered by tests
data
end

Expand Down
10 changes: 10 additions & 0 deletions sentry-ruby/spec/sentry/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,16 @@ def sentry_context
event = subject.event_from_transaction(transaction)
expect(event.contexts).to include({ foo: { bar: 42 } })
end

it 'adds metric summary on transaction if any' do
key = [:c, 'incr', 'none', []]
transaction.metrics_local_aggregator.add(key, 10)
hash = subject.event_from_transaction(transaction).to_hash

expect(hash[:_metrics_summary]).to eq({
'c:incr@none' => { count: 1, max: 10.0, min: 10.0, sum: 10.0, tags: {} }
})
end
end

describe "#event_from_exception" do
Expand Down
51 changes: 51 additions & 0 deletions sentry-ruby/spec/sentry/metrics/aggregator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,57 @@
expect(metric).to be_a(Sentry::Metrics::SetMetric)
expect(metric.value).to eq(Set[1])
end

describe 'local aggregation for span metric summaries' do
it 'does nothing without an active scope span' do
expect_any_instance_of(Sentry::Metrics::LocalAggregator).not_to receive(:add)
subject.add(:c, 'incr', 1)
end

context 'with running transaction and active span' do
let(:span) { Sentry.start_transaction }

before do
Sentry.get_current_scope.set_span(span)
Sentry.get_current_scope.set_transaction_name('metric', source: :view)
end

it 'does nothing if transaction name is low quality' do
expect_any_instance_of(Sentry::Metrics::LocalAggregator).not_to receive(:add)

Sentry.get_current_scope.set_transaction_name('/123', source: :url)
subject.add(:c, 'incr', 1)
end

it 'proxies bucket key and value to local aggregator' do
expect(span.metrics_local_aggregator).to receive(:add).with(
array_including(:c, 'incr', 'none'),
1
)
subject.add(:c, 'incr', 1)
end

context 'for set metrics' do
before { subject.add(:s, 'set', 'foo') }

it 'proxies bucket key and value 0 when existing element' do
expect(span.metrics_local_aggregator).to receive(:add).with(
array_including(:s, 'set', 'none'),
0
)
subject.add(:s, 'set', 'foo')
end

it 'proxies bucket key and value 1 when new element' do
expect(span.metrics_local_aggregator).to receive(:add).with(
array_including(:s, 'set', 'none'),
1
)
subject.add(:s, 'set', 'bar')
end
end
end
end
end

describe '#flush' do
Expand Down
83 changes: 83 additions & 0 deletions sentry-ruby/spec/sentry/metrics/local_aggregator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
require 'spec_helper'

RSpec.describe Sentry::Metrics::LocalAggregator do
let(:tags) { [['foo', 1], ['foo', 2], ['bar', 'baz']] }
let(:key) { [:c, 'incr', 'second', tags] }
let(:key2) { [:s, 'set', 'none', []] }

describe '#add' do
it 'creates new GaugeMetric and adds it to bucket if key not existing' do
expect(Sentry::Metrics::GaugeMetric).to receive(:new).with(10).and_call_original

subject.add(key, 10)

metric = subject.buckets[key]
expect(metric).to be_a(Sentry::Metrics::GaugeMetric)
expect(metric.last).to eq(10.0)
expect(metric.min).to eq(10.0)
expect(metric.max).to eq(10.0)
expect(metric.sum).to eq(10.0)
expect(metric.count).to eq(1)
end

it 'adds value to existing GaugeMetric' do
subject.add(key, 10)

metric = subject.buckets[key]
expect(metric).to be_a(Sentry::Metrics::GaugeMetric)
expect(metric).to receive(:add).with(20).and_call_original
expect(Sentry::Metrics::GaugeMetric).not_to receive(:new)

subject.add(key, 20)
expect(metric.last).to eq(20.0)
expect(metric.min).to eq(10.0)
expect(metric.max).to eq(20.0)
expect(metric.sum).to eq(30.0)
expect(metric.count).to eq(2)
end
end

describe '#to_hash' do
it 'returns nil if empty buckets' do
expect(subject.to_hash).to eq(nil)
end

context 'with filled buckets' do
before do
subject.add(key, 10)
subject.add(key, 20)
subject.add(key2, 1)
end

it 'has the correct payload keys in the hash' do
expect(subject.to_hash.keys).to eq([
'c:incr@second',
's:set@none'
])
end

it 'has the tags deserialized correctly with array values' do
expect(subject.to_hash['c:incr@second'][:tags]).to eq({
'foo' => [1, 2],
'bar' => 'baz'
})
end

it 'has the correct gauge metric values' do
expect(subject.to_hash['c:incr@second']).to include({
min: 10.0,
max: 20.0,
count: 2,
sum: 30.0
})

expect(subject.to_hash['s:set@none']).to include({
min: 1.0,
max: 1.0,
count: 1,
sum: 1.0
})
end
end
end
end
Loading
Loading