lib/elastic_apm/spies/azure_storage_table.rb



# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#
# frozen_string_literal: true

module ElasticAPM
  # @api private
  module Spies
    # @api private
    class AzureStorageTableSpy
      TYPE = "storage"
      SUBTYPE = "azuretable"

      module Helpers
        class << self
          def instrument(operation_name, table_name = nil, service:)
            span_name = span_name(operation_name, table_name)
            action = formatted_op_name(operation_name)
            account_name = account_name_from_storage_table_host(service.storage_service_host[:primary])

            destination = ElasticAPM::Span::Context::Destination.from_uri(service.storage_service_host[:primary])
            destination.service.resource = "#{SUBTYPE}/#{account_name}"

            context = ElasticAPM::Span::Context.new(destination: destination)

            ElasticAPM.with_span(span_name, TYPE, subtype: SUBTYPE, action: action, context: context) do
              ElasticAPM::Spies.without_faraday do
                ElasticAPM::Spies.without_net_http do
                  yield
                end
              end
            end
          end

          private

          DEFAULT_OP_NAMES = {
            "create_table" => "Create",
            "delete_table" => "Delete",
            "get_table_acl" => "GetAcl",
            "set_table_acl" => "SetAcl",
            "insert_entity" => "Insert",
            "query_entities" => "Query",
            "update_entity" => "Update",
            "merge_entity" => "Merge",
            "delete_entity" => "Delete"
          }.freeze

          def formatted_op_names
            @formatted_op_names ||= Concurrent::Map.new
          end

          def account_names
            @account_names ||= Concurrent::Map.new
          end

          def span_name(operation_name, table_name = nil)
            base = "AzureTable #{formatted_op_name(operation_name)}"

            return base unless table_name

            "#{base} #{table_name}"
          end

          def formatted_op_name(operation_name)
            formatted_op_names.compute_if_absent(operation_name) do
              DEFAULT_OP_NAMES.fetch(operation_name) do
                operation_name.to_s.split("_").collect(&:capitalize).join
              end
            end
          end

          def account_name_from_storage_table_host(host)
            account_names.compute_if_absent(host) do
              URI(host).host.split(".").first || "unknown"
            end
          rescue Exception
            "unknown"
          end
        end
      end

      # @api private
      module Ext
        # Methods with table_name as first parameter
        %i[
          create_table
          delete_table
          get_table
          get_table_acl
          set_table_acl
          insert_entity
          query_entities
          update_entity
          merge_entity
          delete_entity
        ].each do |method_name|
          define_method(method_name) do |table_name, *args|
            unless (transaction = ElasticAPM.current_transaction)
              return super(table_name, *args)
            end

            ElasticAPM::Spies::AzureStorageTableSpy::Helpers.instrument(
              method_name.to_s, table_name, service: self
            ) do
              super(table_name, *args)
            end
          end
        end

        # Methods WITHOUT table_name as first parameter
        def query_tables(*args)
          unless (transaction = ElasticAPM.current_transaction)
            return super(*args)
          end

          ElasticAPM::Spies::AzureStorageTableSpy::Helpers.instrument("query_tables", service: self) do
            super(*args)
          end
        end
      end

      def install
        ::Azure::Storage::Table::TableService.prepend(Ext)
      end
    end

    register(
      "Azure::Storage::Table::TableService",
      "azure/storage/table",
      AzureStorageTableSpy.new
    )
  end
end