Commit 943f9318 authored by Andrew Newdigate's avatar Andrew Newdigate
Browse files

Add cache support for distributed tracing

parent 43ee3fb8
Loading
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@ module Labkit
    module Rails
      autoload :ActionViewSubscriber, "labkit/tracing/rails/action_view_subscriber"
      autoload :ActiveRecordSubscriber, "labkit/tracing/rails/active_record_subscriber"
      autoload :ActiveSupportSubscriber, "labkit/tracing/rails/active_support_subscriber"
      autoload :RailsCommon, "labkit/tracing/rails/rails_common"
    end
  end
+86 −0
Original line number Diff line number Diff line
# frozen_string_literal: true

module Labkit
  module Tracing
    module Rails
      # ActiveSupport bridges action active support notifications to
      # the distributed tracing subsystem
      class ActiveSupportSubscriber
        include RailsCommon

        COMPONENT_TAG = "ActiveSupport"

        CACHE_READ_TOPIC = "cache_read.active_support"
        CACHE_GENERATE_TOPIC = "cache_generate.active_support"
        CACHE_FETCH_HIT_TOPIC = "cache_fetch_hit.active_support"
        CACHE_WRITE_TOPIC = "cache_write.active_support"
        CACHE_DELETE_TOPIC = "cache_delete.active_support"

        # Instruments Rails ActiveSupport events for opentracing.
        # Returns a lambda, which, when called will unsubscribe from the notifications
        def self.instrument
          subscriber = new

          subscriptions = [
            ActiveSupport::Notifications.subscribe(CACHE_READ_TOPIC) do |_, start, finish, _, payload|
              subscriber.notify_cache_read(start, finish, payload)
            end,
            ActiveSupport::Notifications.subscribe(CACHE_GENERATE_TOPIC) do |_, start, finish, _, payload|
              subscriber.notify_cache_generate(start, finish, payload)
            end,
            ActiveSupport::Notifications.subscribe(CACHE_FETCH_HIT_TOPIC) do |_, start, finish, _, payload|
              subscriber.notify_cache_fetch_hit(start, finish, payload)
            end,
            ActiveSupport::Notifications.subscribe(CACHE_WRITE_TOPIC) do |_, start, finish, _, payload|
              subscriber.notify_cache_write(start, finish, payload)
            end,
            ActiveSupport::Notifications.subscribe(CACHE_DELETE_TOPIC) do |_, start, finish, _, payload|
              subscriber.notify_cache_delete(start, finish, payload)
            end,
          ]

          create_unsubscriber subscriptions
        end

        # For more information on the payloads: https://guides.rubyonrails.org/active_support_instrumentation.html#active-support
        def notify_cache_read(start, finish, payload)
          generate_span_for_notification("cache_read", start, finish, payload, tags_for_cache_read(payload))
        end

        def notify_cache_generate(start, finish, payload)
          generate_span_for_notification("cache_generate", start, finish, payload, tags_for_key(payload))
        end

        def notify_cache_fetch_hit(start, finish, payload)
          generate_span_for_notification("cache_fetch_hit", start, finish, payload, tags_for_key(payload))
        end

        def notify_cache_write(start, finish, payload)
          generate_span_for_notification("cache_write", start, finish, payload, tags_for_key(payload))
        end

        def notify_cache_delete(start, finish, payload)
          generate_span_for_notification("cache_delete", start, finish, payload, tags_for_key(payload))
        end

        private

        def tags_for_cache_read(payload)
          {
            "component" => COMPONENT_TAG,
            "cache.key" => payload[:key],
            "cache.hit" => payload[:hit],
            "cache.super_operation" => payload[:super_operation],
          }
        end

        def tags_for_key(payload)
          {
            "component" => COMPONENT_TAG,
            "cache.key" => payload[:key],
          }
        end
      end
    end
  end
end
+107 −0
Original line number Diff line number Diff line
# frozen_string_literal: true

describe Labkit::Tracing::Rails::ActiveSupportSubscriber do
  using RSpec::Parameterized::TableSyntax

  describe ".instrument" do
    it "is unsubscribeable" do
      unsubscribe = described_class.instrument

      expect(unsubscribe).not_to be_nil
      expect { unsubscribe.call }.not_to raise_error
    end
  end

  describe "#notify_cache_read" do
    subject { described_class.new }

    let(:start) { Time.now }
    let(:finish) { Time.now }

    where(:key, :hit, :super_operation) do
      nil | nil | nil
      123 | nil | nil
      123 | true | nil
      123 | false | nil
      123 | true | "fetch"
    end

    with_them do
      def payload
        { key: key, hit: hit, super_operation: super_operation }
      end

      def expected_tags
        {
          "component" => "ActiveSupport",
          "cache.key" => key,
          "cache.hit" => hit,
          "cache.super_operation" => super_operation,
        }
      end

      it "should notify the tracer when the hash contains null values" do
        expect(Labkit::Tracing::TracingUtils).to receive(:postnotify_span).with("cache_read", start, finish, tags: expected_tags, exception: nil)

        subject.notify_cache_read(start, finish, payload)
      end

      it "should notify the tracer when the payload is missing values" do
        expect(Labkit::Tracing::TracingUtils).to receive(:postnotify_span).with("cache_read", start, finish, tags: expected_tags, exception: nil)

        subject.notify_cache_read(start, finish, payload.compact)
      end

      it "should not throw exceptions when with the default tracer" do
        expect { subject.notify_cache_read(start, finish, payload) }.not_to raise_error
      end
    end
  end

  describe "#notify_*" do
    subject { described_class.new }

    let(:start) { Time.now }
    let(:finish) { Time.now }

    where(:method, :operation_name, :key) do
      :notify_cache_generate | "cache_generate" | nil
      :notify_cache_generate | "cache_generate" | 123
      :notify_cache_fetch_hit | "cache_fetch_hit" | nil
      :notify_cache_fetch_hit | "cache_fetch_hit" | 123
      :notify_cache_write | "cache_write" | nil
      :notify_cache_write | "cache_write" | 123
      :notify_cache_delete | "cache_delete" | nil
      :notify_cache_delete | "cache_delete" | 123
    end

    with_them do
      def payload
        { key: key }
      end

      def expected_tags
        {
          "component" => "ActiveSupport",
          "cache.key" => key,
        }
      end

      it "should notify the tracer when the hash contains null values" do
        expect(Labkit::Tracing::TracingUtils).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: nil)

        subject.method(method).call(start, finish, payload)
      end

      it "should notify the tracer when the payload is missing values" do
        expect(Labkit::Tracing::TracingUtils).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: nil)

        subject.method(method).call(start, finish, payload.compact)
      end

      it "should not throw exceptions when with the default tracer" do
        expect { subject.method(method).call(start, finish, payload) }.not_to raise_error
      end
    end
  end
end