Skip to content

Commit ee37a4a

Browse files
Add ActiveSupport for cache module (#2380)
1 parent 2ff68a7 commit ee37a4a

File tree

6 files changed

+247
-2
lines changed

6 files changed

+247
-2
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
### Features
44

55
- Add `include_sentry_event` matcher for RSpec [#2424](https://github.com/getsentry/sentry-ruby/pull/2424)
6+
- Add support for Sentry Cache instrumentation, when using Rails.cache ([#2380](https://github.com/getsentry/sentry-ruby/pull/2380)) (MemoryStore and FileStore require Rails 8.0+)
7+
68

79
### Bug Fixes
810

sentry-rails/lib/sentry/rails/configuration.rb

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require "sentry/rails/tracing/action_view_subscriber"
55
require "sentry/rails/tracing/active_record_subscriber"
66
require "sentry/rails/tracing/active_storage_subscriber"
7+
require "sentry/rails/tracing/active_support_subscriber"
78

89
module Sentry
910
class Configuration
@@ -164,6 +165,7 @@ def initialize
164165
end
165166
@tracing_subscribers = Set.new([
166167
Sentry::Rails::Tracing::ActionViewSubscriber,
168+
Sentry::Rails::Tracing::ActiveSupportSubscriber,
167169
Sentry::Rails::Tracing::ActiveRecordSubscriber,
168170
Sentry::Rails::Tracing::ActiveStorageSubscriber
169171
])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literal: true
2+
3+
require "sentry/rails/tracing/abstract_subscriber"
4+
5+
module Sentry
6+
module Rails
7+
module Tracing
8+
class ActiveSupportSubscriber < AbstractSubscriber
9+
READ_EVENT_NAMES = %w[
10+
cache_read.active_support
11+
].freeze
12+
13+
WRITE_EVENT_NAMES = %w[
14+
cache_write.active_support
15+
cache_increment.active_support
16+
cache_decrement.active_support
17+
].freeze
18+
19+
REMOVE_EVENT_NAMES = %w[
20+
cache_delete.active_support
21+
].freeze
22+
23+
FLUSH_EVENT_NAMES = %w[
24+
cache_prune.active_support
25+
].freeze
26+
27+
EVENT_NAMES = READ_EVENT_NAMES + WRITE_EVENT_NAMES + REMOVE_EVENT_NAMES + FLUSH_EVENT_NAMES
28+
29+
SPAN_ORIGIN = "auto.cache.rails"
30+
31+
def self.subscribe!
32+
subscribe_to_event(EVENT_NAMES) do |event_name, duration, payload|
33+
record_on_current_span(
34+
op: operation_name(event_name),
35+
origin: SPAN_ORIGIN,
36+
start_timestamp: payload[START_TIMESTAMP_NAME],
37+
description: payload[:store],
38+
duration: duration
39+
) do |span|
40+
span.set_data("cache.key", [*payload[:key]].select { |key| Utils::EncodingHelper.valid_utf_8?(key) })
41+
span.set_data("cache.hit", payload[:hit] == true) # Handle nil case
42+
end
43+
end
44+
end
45+
46+
def self.operation_name(event_name)
47+
case
48+
when READ_EVENT_NAMES.include?(event_name)
49+
"cache.get"
50+
when WRITE_EVENT_NAMES.include?(event_name)
51+
"cache.put"
52+
when REMOVE_EVENT_NAMES.include?(event_name)
53+
"cache.remove"
54+
when FLUSH_EVENT_NAMES.include?(event_name)
55+
"cache.flush"
56+
else
57+
"other"
58+
end
59+
end
60+
end
61+
end
62+
end
63+
end

sentry-rails/spec/dummy/test_rails_app/app.rb

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ def self.name
5858
app.config.logger = ActiveSupport::Logger.new(nil)
5959
app.config.eager_load = false
6060
app.config.active_job.queue_adapter = :test
61+
app.config.cache_store = :memory_store
62+
app.config.action_controller.perform_caching = true
6163

6264
# Eager load namespaces can be accumulated after repeated initializations and make initialization
6365
# slower after each run

sentry-rails/spec/sentry/rails/configuration_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@
2525
class MySubscriber; end
2626

2727
it "returns the default subscribers" do
28-
expect(subject.tracing_subscribers.size).to eq(3)
28+
expect(subject.tracing_subscribers.size).to eq(4)
2929
end
3030

3131
it "is customizable" do
3232
subject.tracing_subscribers << MySubscriber
33-
expect(subject.tracing_subscribers.size).to eq(4)
33+
expect(subject.tracing_subscribers.size).to eq(5)
3434
end
3535

3636
it "is replaceable" do
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
RSpec.describe Sentry::Rails::Tracing::ActiveSupportSubscriber, :subscriber, type: :request do
6+
let(:transport) do
7+
Sentry.get_current_client.transport
8+
end
9+
10+
context "when transaction is sampled" do
11+
before do
12+
make_basic_app do |config, app|
13+
config.traces_sample_rate = 1.0
14+
config.rails.tracing_subscribers = [described_class]
15+
end
16+
end
17+
18+
it "tracks cache write" do
19+
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
20+
Sentry.get_current_scope.set_span(transaction)
21+
22+
Rails.cache.write("my_cache_key", "my_cache_value")
23+
transaction.finish
24+
25+
expect(transport.events.count).to eq(1)
26+
cache_transaction = transport.events.first.to_hash
27+
expect(cache_transaction[:type]).to eq("transaction")
28+
29+
expect(cache_transaction[:spans].count).to eq(1)
30+
expect(cache_transaction[:spans][0][:op]).to eq("cache.put")
31+
expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails")
32+
end
33+
34+
#
35+
it "tracks cache increment" do
36+
skip("Tracks on Rails 8.0 for all Cache Stores; Until then only MemCached and Redis Stores.") if Rails.version.to_f < 8.0
37+
38+
Rails.cache.write("my_cache_key", 0)
39+
40+
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
41+
Sentry.get_current_scope.set_span(transaction)
42+
Rails.cache.increment("my_cache_key")
43+
44+
transaction.finish
45+
46+
expect(Rails.cache.read("my_cache_key")).to eq(1)
47+
expect(transport.events.count).to eq(1)
48+
cache_transaction = transport.events.first.to_hash
49+
expect(cache_transaction[:type]).to eq("transaction")
50+
expect(cache_transaction[:spans].count).to eq(2)
51+
expect(cache_transaction[:spans][1][:op]).to eq("cache.put")
52+
expect(cache_transaction[:spans][1][:origin]).to eq("auto.cache.rails")
53+
end
54+
55+
#
56+
it "tracks cache decrement" do
57+
skip("Tracks on Rails 8.0 for all Cache Stores; Until then only MemCached and Redis Stores.") if Rails.version.to_f < 8.0
58+
59+
Rails.cache.write("my_cache_key", 0)
60+
61+
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
62+
Sentry.get_current_scope.set_span(transaction)
63+
Rails.cache.decrement("my_cache_key")
64+
65+
transaction.finish
66+
67+
expect(transport.events.count).to eq(1)
68+
cache_transaction = transport.events.first.to_hash
69+
expect(cache_transaction[:type]).to eq("transaction")
70+
expect(cache_transaction[:spans].count).to eq(2)
71+
expect(cache_transaction[:spans][1][:op]).to eq("cache.put")
72+
expect(cache_transaction[:spans][1][:origin]).to eq("auto.cache.rails")
73+
end
74+
75+
it "tracks cache read" do
76+
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
77+
Sentry.get_current_scope.set_span(transaction)
78+
Rails.cache.read("my_cache_key")
79+
80+
transaction.finish
81+
82+
expect(transport.events.count).to eq(1)
83+
cache_transaction = transport.events.first.to_hash
84+
expect(cache_transaction[:type]).to eq("transaction")
85+
expect(cache_transaction[:spans].count).to eq(1)
86+
expect(cache_transaction[:spans][0][:op]).to eq("cache.get")
87+
expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails")
88+
end
89+
90+
it "tracks cache delete" do
91+
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
92+
Sentry.get_current_scope.set_span(transaction)
93+
94+
Rails.cache.read("my_cache_key")
95+
96+
transaction.finish
97+
98+
expect(transport.events.count).to eq(1)
99+
cache_transaction = transport.events.first.to_hash
100+
expect(cache_transaction[:type]).to eq("transaction")
101+
expect(cache_transaction[:spans].count).to eq(1)
102+
expect(cache_transaction[:spans][0][:op]).to eq("cache.get")
103+
expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails")
104+
end
105+
it "tracks cache prune" do
106+
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
107+
Sentry.get_current_scope.set_span(transaction)
108+
109+
Rails.cache.write("my_cache_key", 123, expires_in: 0.seconds)
110+
111+
Rails.cache.prune(0)
112+
113+
transaction.finish
114+
115+
expect(transport.events.count).to eq(1)
116+
cache_transaction = transport.events.first.to_hash
117+
expect(cache_transaction[:type]).to eq("transaction")
118+
expect(cache_transaction[:spans].count).to eq(2)
119+
expect(cache_transaction[:spans][1][:op]).to eq("cache.flush")
120+
expect(cache_transaction[:spans][1][:origin]).to eq("auto.cache.rails")
121+
end
122+
123+
it "tracks sets cache hit" do
124+
skip("cache.hit is unset on Rails 6.0.x.") if Rails.version.to_i == 6
125+
126+
Rails.cache.write("my_cache_key", "my_cache_value")
127+
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
128+
Sentry.get_current_scope.set_span(transaction)
129+
Rails.cache.read("my_cache_key")
130+
Rails.cache.read("my_cache_key_non_existing")
131+
132+
transaction.finish
133+
expect(transport.events.count).to eq(1)
134+
cache_transaction = transport.events.first.to_hash
135+
expect(cache_transaction[:type]).to eq("transaction")
136+
expect(cache_transaction[:spans].count).to eq(2)
137+
expect(cache_transaction[:spans][0][:op]).to eq("cache.get")
138+
expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails")
139+
expect(cache_transaction[:spans][0][:data]['cache.key']).to eq(["my_cache_key"])
140+
expect(cache_transaction[:spans][0][:data]['cache.hit']).to eq(true)
141+
142+
expect(cache_transaction[:spans][1][:op]).to eq("cache.get")
143+
expect(cache_transaction[:spans][1][:origin]).to eq("auto.cache.rails")
144+
expect(cache_transaction[:spans][1][:data]['cache.key']).to eq(["my_cache_key_non_existing"])
145+
expect(cache_transaction[:spans][1][:data]['cache.hit']).to eq(false)
146+
end
147+
148+
it "tracks cache delete" do
149+
Rails.cache.write("my_cache_key", "my_cache_value")
150+
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
151+
Sentry.get_current_scope.set_span(transaction)
152+
Rails.cache.delete("my_cache_key")
153+
154+
transaction.finish
155+
expect(transport.events.count).to eq(1)
156+
cache_transaction = transport.events.first.to_hash
157+
expect(cache_transaction[:type]).to eq("transaction")
158+
expect(cache_transaction[:spans].count).to eq(1)
159+
expect(cache_transaction[:spans][0][:op]).to eq("cache.remove")
160+
expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails")
161+
expect(cache_transaction[:spans][0][:data]['cache.key']).to eq(["my_cache_key"])
162+
end
163+
end
164+
165+
context "when transaction is not sampled" do
166+
before do
167+
make_basic_app
168+
end
169+
170+
it "doesn't record spans" do
171+
Rails.cache.write("my_cache_key", "my_cache_value")
172+
173+
expect(transport.events.count).to eq(0)
174+
end
175+
end
176+
end

0 commit comments

Comments
 (0)