diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 1f227415d..13c6c181d 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -39,6 +39,11 @@ endif::[] [[unreleased]] ==== Unreleased +[float] +===== Added + +- Add support for AWS Storage Table/CosmosDB {pull}999[#999] + [float] ===== Fixed diff --git a/Gemfile b/Gemfile index bb9c5498d..b93c2a263 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,7 @@ gem 'aws-sdk-dynamodb', require: nil gem 'aws-sdk-s3', require: nil gem 'aws-sdk-sqs', require: nil gem 'aws-sdk-sns', require: nil +gem 'azure-storage-table', require: nil if RUBY_VERSION < '3.0' gem 'elasticsearch', require: nil gem 'fakeredis', require: nil gem 'faraday', require: nil diff --git a/docker-compose.yml b/docker-compose.yml index 6b9d1ab4d..19ce47808 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ services: build: context: . args: - BUNDLER_VERSION: '2.0.2' + BUNDLER_VERSION: '2.2.21' image: '$IMAGE_NAME' environment: HOME: '/tmp' diff --git a/features/azure_app_service_metadata.feature b/features/azure_app_service_metadata.feature index 3149e8719..3b7aab4f2 100644 --- a/features/azure_app_service_metadata.feature +++ b/features/azure_app_service_metadata.feature @@ -68,4 +68,4 @@ Feature: Extracting Metadata for Azure App Service | WEBSITE_RESOURCE_GROUP | resource_group | | WEBSITE_SITE_NAME | site_name | When cloud metadata is collected - Then cloud metadata is null \ No newline at end of file + Then cloud metadata is null diff --git a/lib/elastic_apm.rb b/lib/elastic_apm.rb index aea5ea491..36c7a0c1c 100644 --- a/lib/elastic_apm.rb +++ b/lib/elastic_apm.rb @@ -32,6 +32,7 @@ require 'elastic_apm/logging' # Core +require 'elastic_apm/fields' require 'elastic_apm/agent' require 'elastic_apm/config' require 'elastic_apm/context' diff --git a/lib/elastic_apm/config.rb b/lib/elastic_apm/config.rb index c2b02bfb8..bd91ad709 100644 --- a/lib/elastic_apm/config.rb +++ b/lib/elastic_apm/config.rb @@ -133,6 +133,7 @@ def assign(update) def available_instrumentations %w[ action_dispatch + azure_storage_table delayed_job dynamo_db elasticsearch diff --git a/lib/elastic_apm/fields.rb b/lib/elastic_apm/fields.rb new file mode 100644 index 000000000..987be3b12 --- /dev/null +++ b/lib/elastic_apm/fields.rb @@ -0,0 +1,88 @@ +# 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 + # An interface for creating simple, value holding objects that correspond to + # object fields in the API. + # + # Example: + # class MyThing + # include Fields + # field :name + # field :address, optional: true + # end + # + # MyThing.new(name: 'AJ').to_h + # # => { name: 'AJ' } + # MyThing.new().empty? + # # => true + module Fields + module InstanceMethods + def initialize(**attrs) + attrs.each do |key, value| + self.send(:"#{key}=", value) + end + + super() + end + + def empty? + self.class.fields.each do |key| + next if send(key) + next if optionals.include?(key) + + return true + end + + false + end + + def to_h + self.class.fields.each_with_object({}) do |key, fields| + fields[key] = send(key) + end + end + + private + + def optionals + self.class.optionals + end + end + + module ClassMethods + def field(key, optional: false) + attr_accessor(key) + + fields.push(key) + optionals.push(key) if optional + end + + attr_reader :fields, :optionals + end + + def self.included(cls) + cls.extend(ClassMethods) + cls.include(InstanceMethods) + + cls.instance_variable_set(:@fields, []) + cls.instance_variable_set(:@optionals, []) + end + end +end diff --git a/lib/elastic_apm/span.rb b/lib/elastic_apm/span.rb index 70a50eb64..884a896e4 100644 --- a/lib/elastic_apm/span.rb +++ b/lib/elastic_apm/span.rb @@ -106,6 +106,18 @@ def stop(clock_end = Util.monotonic_micros) @duration ||= (clock_end - @clock_start) @parent.child_stopped @self_time = @duration - child_durations.duration + + if exit_span? + context.destination ||= Context::Destination.new + context.destination.service ||= Context::Destination::Service.new + context.destination.service.resource ||= (subtype || type) + + # Deprecated fields but required by some versions of APM Server, so + # we auto-infer them from existing fields + context.destination.service.name ||= (subtype || type) + context.destination.service.type ||= type + end + self end @@ -166,5 +178,9 @@ def long_enough_for_stacktrace? duration >= min_duration end + + def exit_span? + context.destination || context.db || context.message || context.http + end end end diff --git a/lib/elastic_apm/span/context/destination.rb b/lib/elastic_apm/span/context/destination.rb index a3b0345be..88b4789b4 100644 --- a/lib/elastic_apm/span/context/destination.rb +++ b/lib/elastic_apm/span/context/destination.rb @@ -14,7 +14,7 @@ # 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 @@ -22,74 +22,53 @@ class Span class Context # @api private class Destination + include Fields + + field :address + field :port + field :service + field :cloud # @api private class Service - class MissingValues < StandardError; end - - def initialize(name: nil, type: nil, resource: nil) - @name = name - @type = type - @resource = resource + include Fields - # APM Server expects all values for service - raise MissingValues unless @name && @type && resource - end - - attr_accessor :name, :type, :resource + field :name + field :type + field :resource end # @api private class Cloud - def initialize(region: nil) - @region = region - end + include Fields - attr_accessor :region + field :region end - def initialize( - address: nil, - port: nil, - service: nil, - cloud: nil - ) - @address = address - @port = port - @service = build_service(service) - @cloud = build_cloud(cloud) - end + def initialize(service: nil, cloud: nil, **attrs) + super(**attrs) - attr_reader( - :address, - :port, - :service, - :cloud - ) + self.service = build_service(service) + self.cloud = build_cloud(cloud) + end - def self.from_uri(uri_or_str, type: 'external', port: nil) + def self.from_uri(uri_or_str, type: nil, **attrs) uri = normalize(uri_or_str) - service = Service.new( - name: only_scheme_and_host(uri), - resource: "#{uri.host}:#{uri.port}", - type: type - ) + service = + case type + when 'http' then http_service(uri) + else nil + end new( address: uri.hostname, - port: port || uri.port, - service: service + port: uri.port, + service: service, + **attrs ) end - def self.only_scheme_and_host(uri_or_str) - uri = normalize(uri_or_str) - uri.path = '' - uri.password = uri.query = uri.fragment = nil - uri.to_s - end - class << self private @@ -97,25 +76,27 @@ def normalize(uri_or_str) return uri_or_str.dup if uri_or_str.is_a?(URI) URI(uri_or_str) end + + def http_service(uri) + Service.new(resource: "#{uri.host}:#{uri.port}") + end end private - def build_service(service = nil) - return unless service - return service if service.is_a?(Service) - - Service.new(**service) - rescue Service::MissingValues - nil # If we are missing any service value, return nothing - end - def build_cloud(cloud = nil) - return unless cloud + return Cloud.new unless cloud return cloud if cloud.is_a?(Cloud) Cloud.new(**cloud) end + + def build_service(service = nil) + return Service.new unless service + return service if service.is_a?(Service) + + Service.new(**service) + end end end end diff --git a/lib/elastic_apm/spies.rb b/lib/elastic_apm/spies.rb index aa8930007..f150c4570 100644 --- a/lib/elastic_apm/spies.rb +++ b/lib/elastic_apm/spies.rb @@ -56,6 +56,26 @@ def self.register(*args) end end + def self.without_faraday + return yield unless defined?(FaradaySpy) + + # rubocop:disable Style/ExplicitBlockArgument + ElasticAPM::Spies::FaradaySpy.disable_in do + yield + end + # rubocop:enable Style/ExplicitBlockArgument + end + + def self.without_net_http + return yield unless defined?(NetHTTPSpy) + + # rubocop:disable Style/ExplicitBlockArgument + ElasticAPM::Spies::NetHTTPSpy.disable_in do + yield + end + # rubocop:enable Style/ExplicitBlockArgument + end + def self.register_require_hook(registration) registration.require_paths.each do |path| require_hooks[path] = registration diff --git a/lib/elastic_apm/spies/azure_storage_table.rb b/lib/elastic_apm/spies/azure_storage_table.rb new file mode 100644 index 000000000..c0a9f45fb --- /dev/null +++ b/lib/elastic_apm/spies/azure_storage_table.rb @@ -0,0 +1,148 @@ +# 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 diff --git a/lib/elastic_apm/spies/dynamo_db.rb b/lib/elastic_apm/spies/dynamo_db.rb index 16a864470..8b03e5025 100644 --- a/lib/elastic_apm/spies/dynamo_db.rb +++ b/lib/elastic_apm/spies/dynamo_db.rb @@ -56,8 +56,6 @@ def self.prepended(mod) # Alias all available operations mod.api.operation_names.each do |operation_name| define_method(operation_name) do |params = {}, options = {}| - cloud = ElasticAPM::Span::Context::Destination::Cloud.new(region: config.region) - context = ElasticAPM::Span::Context.new( db: { instance: config.region, @@ -65,12 +63,8 @@ def self.prepended(mod) statement: params[:key_condition_expression] }, destination: { - cloud: cloud, - service: { - name: NAME, - resource: SUBTYPE, - type: TYPE - } + service: { resource: "#{SUBTYPE}/#{config.region}" }, + cloud: { region: config.region } } ) diff --git a/lib/elastic_apm/spies/faraday.rb b/lib/elastic_apm/spies/faraday.rb index 988dc2e1a..e6518a73d 100644 --- a/lib/elastic_apm/spies/faraday.rb +++ b/lib/elastic_apm/spies/faraday.rb @@ -22,17 +22,28 @@ module ElasticAPM module Spies # @api private class FaradaySpy + DISABLE_KEY = :__elastic_apm_faraday_disabled TYPE = 'external' SUBTYPE = 'http' - def self.without_net_http - return yield unless defined?(NetHTTPSpy) + class << self + def disabled=(disabled) + Thread.current[DISABLE_KEY] = disabled + end + + def disabled? + Thread.current[DISABLE_KEY] ||= false + end + + def disable_in + self.disabled = true - # rubocop:disable Style/ExplicitBlockArgument - ElasticAPM::Spies::NetHTTPSpy.disable_in do - yield + begin + yield + ensure + self.disabled = false + end end - # rubocop:enable Style/ExplicitBlockArgument end # @api private @@ -43,6 +54,10 @@ def run_request(method, url, body, headers, &block) return super(method, url, body, headers, &block) end + if ElasticAPM::Spies::FaradaySpy.disabled? + return super(method, url, body, headers, &block) + end + uri = URI(build_url(url)) # If url is set inside block it isn't available until yield, @@ -61,7 +76,7 @@ def run_request(method, url, body, headers, &block) upcased_method = method.to_s.upcase if uri - destination = ElasticAPM::Span::Context::Destination.from_uri(uri) + destination = ElasticAPM::Span::Context::Destination.from_uri(uri, type: SUBTYPE) context = ElasticAPM::Span::Context.new( @@ -85,7 +100,7 @@ def run_request(method, url, body, headers, &block) subtype: SUBTYPE, context: context ) do |span| - ElasticAPM::Spies::FaradaySpy.without_net_http do + ElasticAPM::Spies.without_net_http do trace_context = span&.trace_context || transaction.trace_context result = super(method, url, body, headers) do |req| diff --git a/lib/elastic_apm/spies/http.rb b/lib/elastic_apm/spies/http.rb index a2178cfd6..8f9c6384b 100644 --- a/lib/elastic_apm/spies/http.rb +++ b/lib/elastic_apm/spies/http.rb @@ -37,7 +37,7 @@ def perform(req, options) context = ElasticAPM::Span::Context.new( http: { url: req.uri, method: method }, - destination: ElasticAPM::Span::Context::Destination.from_uri(req.uri) + destination: ElasticAPM::Span::Context::Destination.from_uri(req.uri, type: SUBTYPE) ) name = "#{method} #{host}" diff --git a/lib/elastic_apm/spies/net_http.rb b/lib/elastic_apm/spies/net_http.rb index e93223c63..cd3116813 100644 --- a/lib/elastic_apm/spies/net_http.rb +++ b/lib/elastic_apm/spies/net_http.rb @@ -22,17 +22,17 @@ module ElasticAPM module Spies # @api private class NetHTTPSpy - KEY = :__elastic_apm_net_http_disabled + DISABLE_KEY = :__elastic_apm_net_http_disabled TYPE = 'external' SUBTYPE = 'http' class << self def disabled=(disabled) - Thread.current[KEY] = disabled + Thread.current[DISABLE_KEY] = disabled end def disabled? - Thread.current[KEY] ||= false + Thread.current[DISABLE_KEY] ||= false end def disable_in @@ -80,7 +80,7 @@ def request(req, body = nil, &block) context = ElasticAPM::Span::Context.new( http: { url: uri, method: method }, - destination: ElasticAPM::Span::Context::Destination.from_uri(uri) + destination: ElasticAPM::Span::Context::Destination.from_uri(uri, type: SUBTYPE) ) ElasticAPM.with_span( diff --git a/lib/elastic_apm/spies/s3.rb b/lib/elastic_apm/spies/s3.rb index 171e81903..e13361bbb 100644 --- a/lib/elastic_apm/spies/s3.rb +++ b/lib/elastic_apm/spies/s3.rb @@ -14,7 +14,7 @@ # 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 @@ -85,14 +85,11 @@ def self.prepended(mod) bucket_name = ElasticAPM::Spies::S3Spy.bucket_name(params) region = ElasticAPM::Spies::S3Spy.accesspoint_region(params) || config.region + resource = "#{SUBTYPE}/#{bucket_name || 'unknown-bucket'}" context = ElasticAPM::Span::Context.new( destination: { - cloud: { region: region }, - service: { - resource: bucket_name, - type: TYPE, - name: SUBTYPE - } + service: { resource: resource }, + cloud: { region: region } } ) diff --git a/lib/elastic_apm/spies/sequel.rb b/lib/elastic_apm/spies/sequel.rb index c392fc0e8..0c8c0f441 100644 --- a/lib/elastic_apm/spies/sequel.rb +++ b/lib/elastic_apm/spies/sequel.rb @@ -46,7 +46,7 @@ def log_connection_yield(sql, connection, args = nil, &block) context = ElasticAPM::Span::Context.new( db: { statement: sql, type: 'sql', user: opts[:user] }, - destination: { service: { name: subtype, resource: subtype, type: TYPE } } + destination: { service: { resource: subtype } } ) span = ElasticAPM.start_span( diff --git a/lib/elastic_apm/spies/sns.rb b/lib/elastic_apm/spies/sns.rb index 294c394f1..603c0f9a0 100644 --- a/lib/elastic_apm/spies/sns.rb +++ b/lib/elastic_apm/spies/sns.rb @@ -65,16 +65,10 @@ def self.arn_region(arn) def self.span_context(topic, region) ElasticAPM::Span::Context.new( - message: { - queue_name: topic - }, + message: { queue_name: topic }, destination: { - service: { - resource: [SUBTYPE, topic].compact.join('/'), - type: TYPE, - name: SUBTYPE, - }, - cloud: ElasticAPM::Span::Context::Destination::Cloud.new(region: region) + service: { resource: "#{SUBTYPE}/#{topic}" }, + cloud: { region: region } } ) end diff --git a/lib/elastic_apm/spies/sqs.rb b/lib/elastic_apm/spies/sqs.rb index 36c787d9a..8055d797c 100644 --- a/lib/elastic_apm/spies/sqs.rb +++ b/lib/elastic_apm/spies/sqs.rb @@ -50,19 +50,11 @@ def self.region_from_url(url) end def self.span_context(queue_name, region) - cloud = ElasticAPM::Span::Context::Destination::Cloud.new(region: region) - ElasticAPM::Span::Context.new( - message: { - queue_name: queue_name - }, + message: { queue_name: queue_name }, destination: { - service: { - resource: [SUBTYPE, queue_name].compact.join('/'), - type: TYPE, - name: SUBTYPE - }, - cloud: cloud + service: { resource: "#{SUBTYPE}/#{queue_name}" }, + cloud: { region: region } } ) end diff --git a/lib/elastic_apm/transport/serializers/span_serializer.rb b/lib/elastic_apm/transport/serializers/span_serializer.rb index 7014d7d6f..c25d3baf8 100644 --- a/lib/elastic_apm/transport/serializers/span_serializer.rb +++ b/lib/elastic_apm/transport/serializers/span_serializer.rb @@ -100,16 +100,12 @@ def build_destination(destination) port: destination.port } - if service = destination.service - base[:service] = { - name: keyword_field(destination.service.name), - resource: keyword_field(destination.service.resource), - type: keyword_field(destination.service.type) - } + unless destination.service&.empty? + base[:service] = destination.service.to_h end - if cloud = destination.cloud - base[:cloud] = { region: cloud.region } + unless destination.cloud&.empty? + base[:cloud] = destination.cloud.to_h end base diff --git a/spec/elastic_apm/fields_spec.rb b/spec/elastic_apm/fields_spec.rb new file mode 100644 index 000000000..334fd07ff --- /dev/null +++ b/spec/elastic_apm/fields_spec.rb @@ -0,0 +1,52 @@ +# 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 + +require "spec_helper" + +module ElasticAPM + RSpec.describe Fields do + class MyObject + include Fields + + field :name + field :address, optional: true + end + + it "adds an initializer and accessors" do + subject = MyObject.new(name: "thing") + expect(subject.name).to(eq("thing")) + end + + it "knows its fields" do + expect(MyObject.fields).to eq(%i[name address]) + end + + describe "#empty?" do + it "is when missing all values" do + subject = MyObject.new + expect(subject).to be_empty + end + + it "isn't when all fields set" do + subject = MyObject.new(name: 'a', address: 'b') + expect(subject).to_not be_empty + end + end + end +end diff --git a/spec/elastic_apm/span/context/destination_spec.rb b/spec/elastic_apm/span/context/destination_spec.rb index 47a0c8243..bbd1a4190 100644 --- a/spec/elastic_apm/span/context/destination_spec.rb +++ b/spec/elastic_apm/span/context/destination_spec.rb @@ -14,79 +14,87 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. - +# # frozen_string_literal: true -require 'spec_helper' +require "spec_helper" module ElasticAPM class Span class Context RSpec.describe Destination do - describe '.from_uri' do - let(:uri) { URI('http://example.com/path/?a=1') } + describe ".new" do + it "initializes with hashes" do + subject = described_class.new( + address: 'a', + port: 80, + service: { resource: 'b' }, + cloud: { region: 'c' } + ) + + expect(subject.address).to eq 'a' + expect(subject.port).to eq 80 + expect(subject.service.resource).to eq 'b' + expect(subject.cloud.region).to eq 'c' + end + end + + describe ".from_uri" do + let(:uri) { URI("http://example.com/path/?a=1") } subject { described_class.from_uri(uri) } - it 'parses and initializes correctly' do - expect(subject.address).to eq 'example.com' - expect(subject.port).to eq 80 - expect(subject.service.name).to eq 'http://example.com' - expect(subject.service.resource).to eq 'example.com:80' - expect(subject.service.type).to eq 'external' + it "parses and initializes correctly" do + expect(subject.address).to eq("example.com") + expect(subject.port).to eq(80) + + # deprecated + expect(subject.service.name).to be(nil) + expect(subject.service.type).to be(nil) end - context 'https' do - let(:uri) { URI('https://example.com/path?a=1') } + context("https") do + let(:uri) { URI("https://example.com/path?a=1") } - it 'parses and initializes correctly' do - expect(subject.service.name).to eq 'https://example.com' - expect(subject.service.resource).to eq 'example.com:443' - expect(subject.address).to eq 'example.com' - expect(subject.port).to eq 443 + it "parses and initializes correctly" do + expect(subject.address).to eq("example.com") + expect(subject.port).to eq(443) end end - context 'non-default port' do - let(:uri) { URI('http://example.com:8080/path?a=1') } + context("non-default port") do + let(:uri) { URI("http://example.com:8080/path?a=1") } - it 'parses and initializes correctly' do - expect(subject.service.name).to eq 'http://example.com:8080' - expect(subject.service.resource).to eq 'example.com:8080' - expect(subject.address).to eq 'example.com' - expect(subject.port).to eq 8080 + it "parses and initializes correctly" do + expect(subject.address).to eq("example.com") + expect(subject.port).to eq(8080) end end - context 'when given a string' do - let(:uri) { 'http://example.com/path?a=1' } + context("when given a string") do + let(:uri) { "http://example.com/path?a=1" } - it 'parses and initializes correctly' do - expect(subject.service.name).to eq 'http://example.com' - expect(subject.service.resource).to eq 'example.com:80' - expect(subject.service.type).to eq 'external' - expect(subject.address).to eq 'example.com' - expect(subject.port).to eq 80 + it "parses and initializes correctly" do + expect(subject.address).to eq("example.com") + expect(subject.port).to eq(80) end end - context 'IPv6' do - let(:uri) { 'http://[::1]:8080/' } + context("IPv6") do + let(:uri) { "http://[::1]:8080/" } - it 'parses and initializes correctly' do - expect(subject.service.name).to eq 'http://[::1]:8080' - expect(subject.service.resource).to eq '[::1]:8080' - expect(subject.service.type).to eq 'external' - expect(subject.address).to eq '::1' - expect(subject.port).to eq 8080 + it "parses and initializes correctly" do + expect(subject.address).to eq("::1") + expect(subject.port).to eq(8080) end end - end - context 'when missing a value for service' do - it 'skips the whole thing' do - subject = described_class.new(service: { name: 'Bob' }) - expect(subject.service).to be nil + context("type: http") do + subject { described_class.from_uri(uri, type: "http") } + + it "adds destination" do + expect(subject.service.resource).to eq("example.com:80") + end end end end diff --git a/spec/elastic_apm/span_spec.rb b/spec/elastic_apm/span_spec.rb index b4fcc972c..df2210b46 100644 --- a/spec/elastic_apm/span_spec.rb +++ b/spec/elastic_apm/span_spec.rb @@ -201,9 +201,15 @@ module ElasticAPM describe "#set_destination" do it 'adds destination to context' do - subject.set_destination(address: 'asdf', cloud: { region: 'us-1' }) + subject.set_destination( + address: 'asdf', + service: { resource: 'xyz' }, + cloud: { region: 'abc' } + ) expect(subject.context.destination).to be_a(Span::Context::Destination) expect(subject.context.destination.address).to eq 'asdf' + expect(subject.context.destination.service.resource).to eq 'xyz' + expect(subject.context.destination.cloud.region).to eq 'abc' end end end diff --git a/spec/elastic_apm/spies/azure_storage_table_spec.rb b/spec/elastic_apm/spies/azure_storage_table_spec.rb new file mode 100644 index 000000000..ef778be4c --- /dev/null +++ b/spec/elastic_apm/spies/azure_storage_table_spec.rb @@ -0,0 +1,282 @@ +# 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 + +require "spec_helper" + +enabled = true + +# Azure::Storage doesn't support Ruby 3 yet so skip if we can't load + +begin + require "azure/storage/table" +rescue LoadError + enabled = false +end + +if enabled + module ElasticAPM + RSpec.describe "Spy: Azure Storage Table" do + let(:client) do + common_client = ::Azure::Storage::Common::Client.create( + storage_account_name: "abc", + # lib validates valid Base64 + storage_access_key: Base64.encode64("xyz"), + storage_table_host: "https://my-account.table.core.windows.net" + ) + + ::Azure::Storage::Table::TableService.new(client: common_client) + end + + def stub_server(method, path, body: {}) + stub = stub_request( + method, + "https://my-account.table.core.windows.net#{path}" + ).to_return(body: body&.to_json) + + yield + + expect(stub).to have_been_requested + end + + describe '#create_table', :intercept do + it 'adds a span' do + stub_server(:post, "/Tables") do + with_agent do + ElasticAPM.with_transaction do + client.create_table("testtable") + end + end + end + + expect(@intercepted.spans.length).to be(1) + span, = @intercepted.spans + + expect(span.name).to eq("AzureTable Create testtable") + expect(span.type).to eq("storage") + expect(span.subtype).to eq("azuretable") + expect(span.action).to eq("Create") + expect(span.outcome).to eq("success") + + # span context destination + expect(span.context.destination.address).to eq("my-account.table.core.windows.net") + expect(span.context.destination.port).to eq(443) + expect(span.context.destination.service.resource).to eq("azuretable/my-account") + end + end + + describe "#delete_table", :intercept do + it 'adds a span' do + stub_server(:delete, "/Tables('testtable')") do + with_agent do + ElasticAPM.with_transaction do + client.delete_table("testtable") + end + end + end + + expect(@intercepted.spans.length).to be(1) + span, = @intercepted.spans + + expect(span.name).to eq("AzureTable Delete testtable") + expect(span.action).to eq("Delete") + end + end + + describe "#get_table", :intercept do + it 'adds a span' do + stub_server(:get, "/Tables('testtable')") do + with_agent do + ElasticAPM.with_transaction do + client.get_table("testtable") + end + end + end + + expect(@intercepted.spans.length).to be(1) + span, = @intercepted.spans + + expect(span.name).to eq("AzureTable GetTable testtable") + expect(span.action).to eq("GetTable") + end + end + + describe "#get_table_acl", :intercept do + it 'adds a span' do + stub_server(:get, "/testtable?comp=acl", body: nil) do + with_agent do + ElasticAPM.with_transaction do + client.get_table_acl("testtable") + end + end + end + + expect(@intercepted.spans.length).to be(1) + span, = @intercepted.spans + + expect(span.name).to eq("AzureTable GetAcl testtable") + expect(span.action).to eq("GetAcl") + end + end + + describe "#set_table_acl", :intercept do + it 'adds a span' do + stub_server(:put, "/testtable?comp=acl") do + with_agent do + ElasticAPM.with_transaction do + client.set_table_acl("testtable") + end + end + end + + expect(@intercepted.spans.length).to be(1) + span, = @intercepted.spans + + expect(span.name).to eq("AzureTable SetAcl testtable") + expect(span.action).to eq("SetAcl") + end + end + + describe "#insert_entity", :intercept do + it 'adds a span' do + stub_server(:post, "/testtable()", body: { "best" => "true" }) do + with_agent do + ElasticAPM.with_transaction do + client.insert_entity("testtable", {best: "true"}) + end + end + end + + expect(@intercepted.spans.length).to be(1) + span, = @intercepted.spans + + expect(span.name).to eq("AzureTable Insert testtable") + expect(span.action).to eq("Insert") + end + end + + describe "#query_entities", :intercept do + it 'adds a span' do + stub_server(:get, "/testtable()") do + with_agent do + ElasticAPM.with_transaction do + client.query_entities("testtable") + end + end + end + + expect(@intercepted.spans.length).to be(1) + span, = @intercepted.spans + + expect(span.name).to eq("AzureTable Query testtable") + expect(span.action).to eq("Query") + end + end + + describe "#update_entity", :intercept do + it 'adds a span' do + stub_server(:put, "/testtable()") do + with_agent do + ElasticAPM.with_transaction do + client.update_entity("testtable", {best: "true"}) + end + end + end + + expect(@intercepted.spans.length).to be(1) + span, = @intercepted.spans + + expect(span.name).to eq("AzureTable Update testtable") + expect(span.action).to eq("Update") + end + end + + describe "#merge_entity", :intercept do + it 'adds a span' do + stub_server(:post, "/testtable()") do + with_agent do + ElasticAPM.with_transaction do + client.merge_entity("testtable", {best: "true"}) + end + end + end + + expect(@intercepted.spans.length).to be(1) + span, = @intercepted.spans + + expect(span.name).to eq("AzureTable Merge testtable") + expect(span.action).to eq("Merge") + end + end + + describe "#delete_entity", :intercept do + it 'adds a span' do + stub_server(:delete, "/testtable(PartitionKey='test-partition-key',RowKey='1')") do + with_agent do + ElasticAPM.with_transaction do + client.delete_entity("testtable", "test-partition-key", "1") + end + end + end + + expect(@intercepted.spans.length).to be(1) + span, = @intercepted.spans + + expect(span.name).to eq("AzureTable Delete testtable") + expect(span.action).to eq("Delete") + end + end + + describe "#get_entity", :intercept do + it 'adds a span' do + stub_server(:get, "/testtable(PartitionKey='test-partition-key',RowKey='1')") do + with_agent do + ElasticAPM.with_transaction do + client.get_entity("testtable", "test-partition-key", "1") + end + end + end + + expect(@intercepted.spans.length).to be(1) + span, = @intercepted.spans + + expect(span.name).to eq("AzureTable Query testtable") + expect(span.action).to eq("Query") + end + end + + describe "#query_tables", :intercept do + it 'adds a span' do + stub_server(:get, "/Tables") do + with_agent do + ElasticAPM.with_transaction do + client.query_tables() + end + end + end + + expect(@intercepted.spans.length).to be(1) + span, = @intercepted.spans + + expect(span.name).to eq("AzureTable QueryTables") + expect(span.action).to eq("QueryTables") + end + end + end + end +end diff --git a/spec/elastic_apm/spies/dynamo_db_spec.rb b/spec/elastic_apm/spies/dynamo_db_spec.rb index 6bde9adbc..2c7610c60 100644 --- a/spec/elastic_apm/spies/dynamo_db_spec.rb +++ b/spec/elastic_apm/spies/dynamo_db_spec.rb @@ -16,69 +16,69 @@ # under the License. # frozen_string_literal: true - -require 'spec_helper' -require 'aws-sdk-dynamodb' +require "spec_helper" +require "aws-sdk-dynamodb" module ElasticAPM - RSpec.describe 'Spy: DynamoDB' do + RSpec.describe "Spy: DynamoDB" do let(:dynamo_db_client) do - ::Aws::DynamoDB::Client.new(stub_responses: true, region: 'us-west-1') + ::Aws::DynamoDB::Client.new(stub_responses: true, region: "us-west-1") end it "spans operations", :intercept do with_agent do ElasticAPM.with_transaction do - dynamo_db_client.delete_item(table_name: 'test', key: {}) + dynamo_db_client.delete_item(table_name: "test", key: {}) end end span = @intercepted.spans.first - expect(span.name).to eq('DynamoDB DeleteItem test') - expect(span.type).to eq('db') - expect(span.subtype).to eq('dynamodb') + expect(span.name).to eq("DynamoDB DeleteItem test") + expect(span.type).to eq("db") + expect(span.subtype).to eq("dynamodb") expect(span.action).to eq(:delete_item) - expect(span.outcome).to eq('success') + expect(span.outcome).to eq("success") # span context db - expect(span.context.db.instance).to eq('us-west-1') - expect(span.context.db.type).to eq('dynamodb') + expect(span.context.db.instance).to eq("us-west-1") + expect(span.context.db.type).to eq("dynamodb") # span context destination - expect(span.context.destination.cloud.region).to eq('us-west-1') - expect(span.context.destination.service.resource).to eq('dynamodb') - expect(span.context.destination.service.type).to eq('db') + expect(span.context.destination.service.resource).to eq("dynamodb/us-west-1") + expect(span.context.destination.cloud.region).to eq("us-west-1") end it "omits the table name when there is none", :intercept do with_agent do ElasticAPM.with_transaction do - dynamo_db_client.describe_backup(backup_arn: 'test-arn') + dynamo_db_client.describe_backup(backup_arn: "test-arn") end end span = @intercepted.spans.first - expect(span.name).to eq('DynamoDB DescribeBackup') + expect(span.name).to eq("DynamoDB DescribeBackup") end it "captures the key_condition_expression for queries", :intercept do with_agent do ElasticAPM.with_transaction do - dynamo_db_client.query(table_name: 'myTable', - key_condition_expression: 'Artist = :v1') + dynamo_db_client.query( + table_name: "myTable", + key_condition_expression: "Artist = :v1" + ) end end span = @intercepted.spans.first - expect(span.name).to eq('DynamoDB Query myTable') - expect(span.context.db.statement).to eq('Artist = :v1') + expect(span.name).to eq("DynamoDB Query myTable") + expect(span.context.db.statement).to eq("Artist = :v1") end - context 'when the operation fails' do - it 'sets span outcome to `failure`', :intercept do + context("when the operation fails") do + it "sets span outcome to `failure`", :intercept do with_agent do ElasticAPM.with_transaction do begin @@ -90,8 +90,8 @@ module ElasticAPM span = @intercepted.spans.first - expect(span.name).to eq('DynamoDB BatchGetItem') - expect(span.outcome).to eq('failure') + expect(span.name).to eq("DynamoDB BatchGetItem") + expect(span.outcome).to eq("failure") end end end diff --git a/spec/elastic_apm/spies/faraday_spec.rb b/spec/elastic_apm/spies/faraday_spec.rb index e28c4471a..dcf0bbe14 100644 --- a/spec/elastic_apm/spies/faraday_spec.rb +++ b/spec/elastic_apm/spies/faraday_spec.rb @@ -75,9 +75,7 @@ module ElasticAPM span, = @intercepted.spans destination = span.context.destination - expect(destination.service.name).to match('http://example.com') expect(destination.service.resource).to match('example.com:80') - expect(destination.service.type).to match('external') expect(destination.address).to match('example.com') expect(destination.port).to match(80) end diff --git a/spec/elastic_apm/spies/http_spec.rb b/spec/elastic_apm/spies/http_spec.rb index de70d27eb..60ff12b7d 100644 --- a/spec/elastic_apm/spies/http_spec.rb +++ b/spec/elastic_apm/spies/http_spec.rb @@ -68,9 +68,7 @@ module ElasticAPM span, = @intercepted.spans destination = span.context.destination - expect(destination.service.name).to match('http://example.com') expect(destination.service.resource).to match('example.com:80') - expect(destination.service.type).to match('external') expect(destination.address).to match('example.com') expect(destination.port).to match(80) end @@ -87,9 +85,7 @@ module ElasticAPM span, = @intercepted.spans destination = span.context.destination - expect(destination.service.name).to match('http://[::1]:8080') expect(destination.service.resource).to match('[::1]:8080') - expect(destination.service.type).to match('external') expect(destination.address).to match('::1') expect(destination.port).to match(8080) end diff --git a/spec/elastic_apm/spies/net_http_spec.rb b/spec/elastic_apm/spies/net_http_spec.rb index 51e107e5b..3e31eaffc 100644 --- a/spec/elastic_apm/spies/net_http_spec.rb +++ b/spec/elastic_apm/spies/net_http_spec.rb @@ -171,9 +171,7 @@ module ElasticAPM span, = @intercepted.spans - expect(span.context.destination.service.name).to eq 'http://example.com:1234' expect(span.context.destination.service.resource).to eq 'example.com:1234' - expect(span.context.destination.service.type).to eq 'external' expect(span.context.destination.address).to eq 'example.com' expect(span.context.destination.port).to eq 1234 end @@ -191,33 +189,12 @@ module ElasticAPM span, = @intercepted.spans - expect(span.context.destination.service.name).to eq 'http://[::1]:8080' expect(span.context.destination.service.resource).to eq '[::1]:8080' - expect(span.context.destination.service.type).to eq 'external' expect(span.context.destination.address).to eq '::1' expect(span.context.destination.port).to eq 8080 end end - it 'handles IPv6 addresses' do - WebMock.stub_request(:get, %r{http://\[::1\]/.*}) - - with_agent do - ElasticAPM.with_transaction 'Net::HTTP test' do - Net::HTTP.start('[::1]') do |http| - http.get '/path' - end - end - end - - span, = @intercepted.spans - - expect(span.name).to eq 'GET [::1]' - expect(span.context.destination.service.name).to eq 'http://[::1]' - expect(span.context.destination.service.resource).to eq '[::1]:80' - expect(span.context.destination.service.type).to eq 'external' - end - it 'allows underscores in hostnames' do WebMock.stub_request(:get, %r{http://exa_ple.com/.*}) diff --git a/spec/elastic_apm/spies/s3_spec.rb b/spec/elastic_apm/spies/s3_spec.rb index 23f6c8610..2642e55dd 100644 --- a/spec/elastic_apm/spies/s3_spec.rb +++ b/spec/elastic_apm/spies/s3_spec.rb @@ -42,9 +42,7 @@ module ElasticAPM expect(span.outcome).to eq('success') # span context destination - expect(span.context.destination.service.resource).to eq('my-bucket') - expect(span.context.destination.service.type).to eq('storage') - expect(span.context.destination.service.name).to eq('s3') + expect(span.context.destination.service.resource).to eq('s3/my-bucket') expect(span.context.destination.cloud.region).to eq('us-west-1') end @@ -59,7 +57,7 @@ module ElasticAPM span = @intercepted.spans.first expect(span.name).to eq('S3 CreateBucket mybucket') - expect(span.context.destination.service.resource).to eq('mybucket') + expect(span.context.destination.service.resource).to eq('s3/mybucket') end end @@ -75,7 +73,7 @@ module ElasticAPM expect(span.name).to eq('S3 ListBuckets') expect(span.action).to eq('ListBuckets') - expect(span.context.destination.service).to eq(nil) + expect(span.context.destination.service.resource).to eq('s3/unknown-bucket') end end @@ -93,62 +91,41 @@ module ElasticAPM span = @intercepted.spans.first expect(span.name).to eq('S3 GetObject accesspoint/myendpoint') - expect(span.context.destination.service.resource).to eq('accesspoint/myendpoint') + expect(span.context.destination.service.resource).to eq('s3/accesspoint/myendpoint') end + end - it 'extracts the region from the access point with a slash', :intercept do - with_agent do - ElasticAPM.with_transaction do - s3_client.get_object( - bucket: 'arn:aws:s3:us-east-2:123456789012:accesspoint/myendpoint', - key: 'test' - ) - end + it 'extracts the region from the access point with a slash', :intercept do + with_agent do + ElasticAPM.with_transaction do + s3_client.get_object( + bucket: 'arn:aws:s3:us-east-2:123456789012:accesspoint/myendpoint', + key: 'test' + ) end - - span = @intercepted.spans.first - - expect(span.name).to eq('S3 GetObject accesspoint/myendpoint') - expect(span.action).to eq('GetObject') - - # span context destination - expect(span.context.destination.cloud.region).to eq('us-east-2') - expect(span.context.destination.service.resource).to eq('accesspoint/myendpoint') end - it 'extracts the region from the access point with a colon', :intercept do - with_agent do - ElasticAPM.with_transaction do - s3_client.get_object( - bucket: 'arn:aws:s3:us-east-2:123456789012:accesspoint:myendpoint', - key: 'test' - ) - end - end - - span = @intercepted.spans.first + span = @intercepted.spans.first - expect(span.name).to eq('S3 GetObject accesspoint:myendpoint') - expect(span.context.destination.service.resource).to eq('accesspoint:myendpoint') - expect(span.context.destination.cloud.region).to eq('us-east-2') - end + expect(span.context.destination.cloud.region).to eq('us-east-2') + expect(span.context.destination.service.resource).to eq('s3/accesspoint/myendpoint') end - it "caches the formatted operation name", :intercept do + it 'extracts the region from the access point with a colon', :intercept do with_agent do - expect( - ElasticAPM::Spies::S3Spy::MUTEX - ).to receive(:synchronize).once.and_call_original - ElasticAPM.with_transaction do - s3_client.list_buckets - s3_client.list_buckets + s3_client.get_object( + bucket: 'arn:aws:s3:us-east-2:123456789012:accesspoint:myendpoint', + key: 'test' + ) end end - span1, span2 = @intercepted.spans - expect(span1.name).to eq('S3 ListBuckets') - expect(span2.name).to eq('S3 ListBuckets') + span = @intercepted.spans.first + + expect(span.name).to eq('S3 GetObject accesspoint:myendpoint') + expect(span.context.destination.service.resource).to eq('s3/accesspoint:myendpoint') + expect(span.context.destination.cloud.region).to eq('us-east-2') end context 'when the operation fails' do diff --git a/spec/elastic_apm/spies/sequel_spec.rb b/spec/elastic_apm/spies/sequel_spec.rb index 45e154f86..c92484d55 100644 --- a/spec/elastic_apm/spies/sequel_spec.rb +++ b/spec/elastic_apm/spies/sequel_spec.rb @@ -51,9 +51,7 @@ module ElasticAPM .to eq "SELECT count(*) AS 'count' FROM `users` LIMIT 1" destination = span.context.destination - expect(destination.service.name).to eq 'sqlite' expect(destination.service.resource).to eq 'sqlite' - expect(destination.service.type).to eq 'db' end it 'captures rows_affected for update and delete operations', :intercept do diff --git a/spec/elastic_apm/spies/sns_spec.rb b/spec/elastic_apm/spies/sns_spec.rb index a6bca340a..e5bbb1243 100644 --- a/spec/elastic_apm/spies/sns_spec.rb +++ b/spec/elastic_apm/spies/sns_spec.rb @@ -47,8 +47,6 @@ module ElasticAPM # Span context expect(span.context.destination.service.resource).to eq('sns/MyTopic') - expect(span.context.destination.service.type).to eq('messaging') - expect(span.context.destination.service.name).to eq('sns') expect(span.context.destination.cloud.region).to eq('us-east-1') expect(span.context.message.queue_name).to eq('MyTopic') end @@ -158,7 +156,6 @@ module ElasticAPM span = @intercepted.spans.first expect(span.name).to eq('SNS PUBLISH to my-sub-topic') - expect(span.context.destination.cloud.region).to eq('us-east-1') expect(span.context.destination.service.resource).to eq('sns/my-sub-topic') expect(span.context.message.queue_name).to eq('my-sub-topic') end @@ -176,7 +173,6 @@ module ElasticAPM span = @intercepted.spans.first expect(span.name).to eq('SNS PUBLISH to MyTopic') - expect(span.context.destination.cloud.region).to eq('us-east-1') expect(span.context.destination.service.resource).to eq('sns/MyTopic') expect(span.context.message.queue_name).to eq('MyTopic') end diff --git a/spec/elastic_apm/spies/sqs_spec.rb b/spec/elastic_apm/spies/sqs_spec.rb index 4685d6b5f..93f4118b1 100644 --- a/spec/elastic_apm/spies/sqs_spec.rb +++ b/spec/elastic_apm/spies/sqs_spec.rb @@ -47,8 +47,6 @@ module ElasticAPM # Span context expect(span.context.destination.service.resource).to eq('sqs/my-queue') - expect(span.context.destination.service.type).to eq('messaging') - expect(span.context.destination.service.name).to eq('sqs') expect(span.context.destination.cloud.region).to eq('us-east-1') expect(span.context.message.queue_name).to eq('my-queue') end @@ -125,10 +123,7 @@ module ElasticAPM # Span context expect(span.context.destination.service.resource).to eq('sqs/my-queue') - expect(span.context.destination.service.type).to eq('messaging') - expect(span.context.destination.service.name).to eq('sqs') expect(span.context.message.queue_name).to eq('my-queue') - expect(span.context.destination.cloud.region).to eq('us-east-1') end it 'adds trace context to the message attributes', :intercept do @@ -211,9 +206,6 @@ module ElasticAPM # Span context expect(span.context.destination.service.resource).to eq('sqs/my-queue') - expect(span.context.destination.service.type).to eq('messaging') - expect(span.context.destination.service.name).to eq('sqs') - expect(span.context.destination.cloud.region).to eq('us-east-1') expect(span.context.message.queue_name).to eq('my-queue') end diff --git a/spec/elastic_apm/stacktrace_builder_spec.rb b/spec/elastic_apm/stacktrace_builder_spec.rb index 3a67f2f1f..ca9755d6e 100644 --- a/spec/elastic_apm/stacktrace_builder_spec.rb +++ b/spec/elastic_apm/stacktrace_builder_spec.rb @@ -111,7 +111,7 @@ def java_exception [true, "/Users/someone/.gem/ruby/2.5.0/gems/railties-5.1.5/lib/rails.rb:24:in `whatever'"], [true, "/app/vendor/bundle/ruby/2.5.0/bundler/gems/apm-agent-ruby-8135f18735fb/lib/elastic_apm/subscriber.rb:10:in `things'"], [true, "/app/vendor/ruby-2.5.0/lib/ruby/2.5.0/benchmark.rb:10:in `things'"], - [true, "org/jruby/RubyBasicObject.java:1728:in `instance_exec'"], + [true, "org/jruby/BasicObject.java:1728:in `instance_exec'"], [true, "/tmp/vendor/j9.1/jruby/2.3.0/bin/rspec:1:in `
'"], [true, "/usr/local/lib/ruby/gems/2.5.0/gems/bundler-1.16.1/lib/bundler/friendly_errors.rb:122:in `yield'"] # rubocop:enable Layout/LineLength diff --git a/spec/elastic_apm/transport/serializers/span_serializer_spec.rb b/spec/elastic_apm/transport/serializers/span_serializer_spec.rb index 92470fd53..7256410c6 100644 --- a/spec/elastic_apm/transport/serializers/span_serializer_spec.rb +++ b/spec/elastic_apm/transport/serializers/span_serializer_spec.rb @@ -174,25 +174,6 @@ module Serializers end end - context 'with a destination and cloud' do - it 'adds destination with cloud' do - cloud = ElasticAPM::Span::Context::Destination::Cloud.new(region: 'mars-1') - - span = Span.new( - name: 'Span', - transaction: transaction, - parent: transaction, - trace_context: trace_context, - context: Span::Context.new(destination: { cloud: cloud }) - ) - - result = subject.build(span) - - expect(result.dig(:span, :context, :destination, :cloud)) - .to match({ region: 'mars-1' }) - end - end - context 'with a message' do it 'adds message object' do span = Span.new( @@ -274,6 +255,34 @@ module Serializers expect(result[:span][:outcome]).to eq 'success' end end + + context 'with a destination and cloud' do + it 'adds destination with cloud' do + span = Span.new( + name: 'Span', + transaction: transaction, + parent: transaction, + trace_context: trace_context, + context: Span::Context.new( + destination: { + service: { resource: 'a' }, + cloud: { region: 'b' } + } + ) + ) + + # set auto-infered destination.service fields + span.start.done + + result = subject.build(span) + + expect(result.dig(:span, :context, :destination, :service)) + .to match({ resource: 'a', name: span.type, type: span.type }) + + expect(result.dig(:span, :context, :destination, :cloud)) + .to match({ region: 'b' }) + end + end end end end