Skip to content

Commit

Permalink
Add ActiveSupport for cache module (#2380)
Browse files Browse the repository at this point in the history
  • Loading branch information
frederikspang authored Oct 18, 2024
1 parent 2ff68a7 commit ee37a4a
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
### Features

- Add `include_sentry_event` matcher for RSpec [#2424](https://github.com/getsentry/sentry-ruby/pull/2424)
- 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+)


### Bug Fixes

Expand Down
2 changes: 2 additions & 0 deletions sentry-rails/lib/sentry/rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "sentry/rails/tracing/action_view_subscriber"
require "sentry/rails/tracing/active_record_subscriber"
require "sentry/rails/tracing/active_storage_subscriber"
require "sentry/rails/tracing/active_support_subscriber"

module Sentry
class Configuration
Expand Down Expand Up @@ -164,6 +165,7 @@ def initialize
end
@tracing_subscribers = Set.new([
Sentry::Rails::Tracing::ActionViewSubscriber,
Sentry::Rails::Tracing::ActiveSupportSubscriber,
Sentry::Rails::Tracing::ActiveRecordSubscriber,
Sentry::Rails::Tracing::ActiveStorageSubscriber
])
Expand Down
63 changes: 63 additions & 0 deletions sentry-rails/lib/sentry/rails/tracing/active_support_subscriber.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

require "sentry/rails/tracing/abstract_subscriber"

module Sentry
module Rails
module Tracing
class ActiveSupportSubscriber < AbstractSubscriber
READ_EVENT_NAMES = %w[
cache_read.active_support
].freeze

WRITE_EVENT_NAMES = %w[
cache_write.active_support
cache_increment.active_support
cache_decrement.active_support
].freeze

REMOVE_EVENT_NAMES = %w[
cache_delete.active_support
].freeze

FLUSH_EVENT_NAMES = %w[
cache_prune.active_support
].freeze

EVENT_NAMES = READ_EVENT_NAMES + WRITE_EVENT_NAMES + REMOVE_EVENT_NAMES + FLUSH_EVENT_NAMES

SPAN_ORIGIN = "auto.cache.rails"

def self.subscribe!
subscribe_to_event(EVENT_NAMES) do |event_name, duration, payload|
record_on_current_span(
op: operation_name(event_name),
origin: SPAN_ORIGIN,
start_timestamp: payload[START_TIMESTAMP_NAME],
description: payload[:store],
duration: duration
) do |span|
span.set_data("cache.key", [*payload[:key]].select { |key| Utils::EncodingHelper.valid_utf_8?(key) })
span.set_data("cache.hit", payload[:hit] == true) # Handle nil case
end
end
end

def self.operation_name(event_name)
case
when READ_EVENT_NAMES.include?(event_name)
"cache.get"
when WRITE_EVENT_NAMES.include?(event_name)
"cache.put"
when REMOVE_EVENT_NAMES.include?(event_name)
"cache.remove"
when FLUSH_EVENT_NAMES.include?(event_name)
"cache.flush"
else
"other"
end
end
end
end
end
end
2 changes: 2 additions & 0 deletions sentry-rails/spec/dummy/test_rails_app/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ def self.name
app.config.logger = ActiveSupport::Logger.new(nil)
app.config.eager_load = false
app.config.active_job.queue_adapter = :test
app.config.cache_store = :memory_store
app.config.action_controller.perform_caching = true

# Eager load namespaces can be accumulated after repeated initializations and make initialization
# slower after each run
Expand Down
4 changes: 2 additions & 2 deletions sentry-rails/spec/sentry/rails/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
class MySubscriber; end

it "returns the default subscribers" do
expect(subject.tracing_subscribers.size).to eq(3)
expect(subject.tracing_subscribers.size).to eq(4)
end

it "is customizable" do
subject.tracing_subscribers << MySubscriber
expect(subject.tracing_subscribers.size).to eq(4)
expect(subject.tracing_subscribers.size).to eq(5)
end

it "is replaceable" do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Sentry::Rails::Tracing::ActiveSupportSubscriber, :subscriber, type: :request do
let(:transport) do
Sentry.get_current_client.transport
end

context "when transaction is sampled" do
before do
make_basic_app do |config, app|
config.traces_sample_rate = 1.0
config.rails.tracing_subscribers = [described_class]
end
end

it "tracks cache write" do
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
Sentry.get_current_scope.set_span(transaction)

Rails.cache.write("my_cache_key", "my_cache_value")
transaction.finish

expect(transport.events.count).to eq(1)
cache_transaction = transport.events.first.to_hash
expect(cache_transaction[:type]).to eq("transaction")

expect(cache_transaction[:spans].count).to eq(1)
expect(cache_transaction[:spans][0][:op]).to eq("cache.put")
expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails")
end

#
it "tracks cache increment" do
skip("Tracks on Rails 8.0 for all Cache Stores; Until then only MemCached and Redis Stores.") if Rails.version.to_f < 8.0

Rails.cache.write("my_cache_key", 0)

transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
Sentry.get_current_scope.set_span(transaction)
Rails.cache.increment("my_cache_key")

transaction.finish

expect(Rails.cache.read("my_cache_key")).to eq(1)
expect(transport.events.count).to eq(1)
cache_transaction = transport.events.first.to_hash
expect(cache_transaction[:type]).to eq("transaction")
expect(cache_transaction[:spans].count).to eq(2)
expect(cache_transaction[:spans][1][:op]).to eq("cache.put")
expect(cache_transaction[:spans][1][:origin]).to eq("auto.cache.rails")
end

#
it "tracks cache decrement" do
skip("Tracks on Rails 8.0 for all Cache Stores; Until then only MemCached and Redis Stores.") if Rails.version.to_f < 8.0

Rails.cache.write("my_cache_key", 0)

transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
Sentry.get_current_scope.set_span(transaction)
Rails.cache.decrement("my_cache_key")

transaction.finish

expect(transport.events.count).to eq(1)
cache_transaction = transport.events.first.to_hash
expect(cache_transaction[:type]).to eq("transaction")
expect(cache_transaction[:spans].count).to eq(2)
expect(cache_transaction[:spans][1][:op]).to eq("cache.put")
expect(cache_transaction[:spans][1][:origin]).to eq("auto.cache.rails")
end

it "tracks cache read" do
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
Sentry.get_current_scope.set_span(transaction)
Rails.cache.read("my_cache_key")

transaction.finish

expect(transport.events.count).to eq(1)
cache_transaction = transport.events.first.to_hash
expect(cache_transaction[:type]).to eq("transaction")
expect(cache_transaction[:spans].count).to eq(1)
expect(cache_transaction[:spans][0][:op]).to eq("cache.get")
expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails")
end

it "tracks cache delete" do
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
Sentry.get_current_scope.set_span(transaction)

Rails.cache.read("my_cache_key")

transaction.finish

expect(transport.events.count).to eq(1)
cache_transaction = transport.events.first.to_hash
expect(cache_transaction[:type]).to eq("transaction")
expect(cache_transaction[:spans].count).to eq(1)
expect(cache_transaction[:spans][0][:op]).to eq("cache.get")
expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails")
end
it "tracks cache prune" do
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
Sentry.get_current_scope.set_span(transaction)

Rails.cache.write("my_cache_key", 123, expires_in: 0.seconds)

Rails.cache.prune(0)

transaction.finish

expect(transport.events.count).to eq(1)
cache_transaction = transport.events.first.to_hash
expect(cache_transaction[:type]).to eq("transaction")
expect(cache_transaction[:spans].count).to eq(2)
expect(cache_transaction[:spans][1][:op]).to eq("cache.flush")
expect(cache_transaction[:spans][1][:origin]).to eq("auto.cache.rails")
end

it "tracks sets cache hit" do
skip("cache.hit is unset on Rails 6.0.x.") if Rails.version.to_i == 6

Rails.cache.write("my_cache_key", "my_cache_value")
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
Sentry.get_current_scope.set_span(transaction)
Rails.cache.read("my_cache_key")
Rails.cache.read("my_cache_key_non_existing")

transaction.finish
expect(transport.events.count).to eq(1)
cache_transaction = transport.events.first.to_hash
expect(cache_transaction[:type]).to eq("transaction")
expect(cache_transaction[:spans].count).to eq(2)
expect(cache_transaction[:spans][0][:op]).to eq("cache.get")
expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails")
expect(cache_transaction[:spans][0][:data]['cache.key']).to eq(["my_cache_key"])
expect(cache_transaction[:spans][0][:data]['cache.hit']).to eq(true)

expect(cache_transaction[:spans][1][:op]).to eq("cache.get")
expect(cache_transaction[:spans][1][:origin]).to eq("auto.cache.rails")
expect(cache_transaction[:spans][1][:data]['cache.key']).to eq(["my_cache_key_non_existing"])
expect(cache_transaction[:spans][1][:data]['cache.hit']).to eq(false)
end

it "tracks cache delete" do
Rails.cache.write("my_cache_key", "my_cache_value")
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
Sentry.get_current_scope.set_span(transaction)
Rails.cache.delete("my_cache_key")

transaction.finish
expect(transport.events.count).to eq(1)
cache_transaction = transport.events.first.to_hash
expect(cache_transaction[:type]).to eq("transaction")
expect(cache_transaction[:spans].count).to eq(1)
expect(cache_transaction[:spans][0][:op]).to eq("cache.remove")
expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails")
expect(cache_transaction[:spans][0][:data]['cache.key']).to eq(["my_cache_key"])
end
end

context "when transaction is not sampled" do
before do
make_basic_app
end

it "doesn't record spans" do
Rails.cache.write("my_cache_key", "my_cache_value")

expect(transport.events.count).to eq(0)
end
end
end

0 comments on commit ee37a4a

Please sign in to comment.