From 3dc37e489c4124d4f05b944924318e6048b75b4d Mon Sep 17 00:00:00 2001 From: Robert Laurin Date: Wed, 13 May 2020 14:42:48 -0400 Subject: [PATCH] Add support for automated resource detection This adds support for populating a resource with with Telemetry data as well as Google Cloud Platform environment metata data. For #230 --- sdk/lib/opentelemetry/sdk/configurator.rb | 12 +- sdk/lib/opentelemetry/sdk/resources.rb | 2 + .../sdk/resources/auto_detector.rb | 33 +++++ .../opentelemetry/sdk/resources/constants.rb | 122 ++++++++++++++++++ .../detectors/google_cloud_platform.rb | 52 ++++++++ .../sdk/resources/detectors/telemetry.rb | 25 ++++ .../opentelemetry/sdk/resources/resource.rb | 1 + sdk/lib/opentelemetry/sdk/trace/tracer.rb | 12 +- .../sdk/trace/tracer_provider.rb | 6 +- sdk/opentelemetry-sdk.gemspec | 1 + .../sdk/resources/auto_detector_test.rb | 27 ++++ .../detectors/google_cloud_platform_test.rb | 64 +++++++++ .../sdk/resources/detectors/telemetry_test.rb | 28 ++++ .../sdk/trace/tracer_provider_test.rb | 3 +- .../opentelemetry/sdk/trace/tracer_test.rb | 12 +- sdk/test/test_helper.rb | 9 ++ 16 files changed, 385 insertions(+), 24 deletions(-) create mode 100644 sdk/lib/opentelemetry/sdk/resources/auto_detector.rb create mode 100644 sdk/lib/opentelemetry/sdk/resources/constants.rb create mode 100644 sdk/lib/opentelemetry/sdk/resources/detectors/google_cloud_platform.rb create mode 100644 sdk/lib/opentelemetry/sdk/resources/detectors/telemetry.rb create mode 100644 sdk/test/opentelemetry/sdk/resources/auto_detector_test.rb create mode 100644 sdk/test/opentelemetry/sdk/resources/detectors/google_cloud_platform_test.rb create mode 100644 sdk/test/opentelemetry/sdk/resources/detectors/telemetry_test.rb diff --git a/sdk/lib/opentelemetry/sdk/configurator.rb b/sdk/lib/opentelemetry/sdk/configurator.rb index 1713e8ae44..daaa9eeab5 100644 --- a/sdk/lib/opentelemetry/sdk/configurator.rb +++ b/sdk/lib/opentelemetry/sdk/configurator.rb @@ -16,7 +16,7 @@ class Configurator private_constant :USE_MODE_UNSPECIFIED, :USE_MODE_ONE, :USE_MODE_ALL attr_writer :logger, :http_extractors, :http_injectors, :text_extractors, - :text_injectors + :text_injectors, :resource def initialize @adapter_names = [] @@ -27,7 +27,7 @@ def initialize @text_injectors = nil @span_processors = [] @use_mode = USE_MODE_UNSPECIFIED - @tracer_provider = Trace::TracerProvider.new + @resource = OpenTelemetry::SDK::Resources::Resource.create end def logger @@ -83,12 +83,16 @@ def configure OpenTelemetry.correlations = CorrelationContext::Manager.new configure_propagation configure_span_processors - OpenTelemetry.tracer_provider = @tracer_provider + OpenTelemetry.tracer_provider = tracer_provider install_instrumentation end private + def tracer_provider + @tracer_provider ||= Trace::TracerProvider.new(@resource) + end + def check_use_mode!(mode) @use_mode = mode if @use_mode == USE_MODE_UNSPECIFIED raise 'Use either `use_all` or `use`, but not both' unless @use_mode == mode @@ -105,7 +109,7 @@ def install_instrumentation def configure_span_processors processors = @span_processors.empty? ? [default_span_processor] : @span_processors - processors.each { |p| @tracer_provider.add_span_processor(p) } + processors.each { |p| tracer_provider.add_span_processor(p) } end def default_span_processor diff --git a/sdk/lib/opentelemetry/sdk/resources.rb b/sdk/lib/opentelemetry/sdk/resources.rb index 7155df90c6..bea8fe9afc 100644 --- a/sdk/lib/opentelemetry/sdk/resources.rb +++ b/sdk/lib/opentelemetry/sdk/resources.rb @@ -13,3 +13,5 @@ module Resources end require 'opentelemetry/sdk/resources/resource' +require 'opentelemetry/sdk/resources/constants' +require 'opentelemetry/sdk/resources/auto_detector' diff --git a/sdk/lib/opentelemetry/sdk/resources/auto_detector.rb b/sdk/lib/opentelemetry/sdk/resources/auto_detector.rb new file mode 100644 index 0000000000..44838e8fe7 --- /dev/null +++ b/sdk/lib/opentelemetry/sdk/resources/auto_detector.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/sdk/resources/detectors/google_cloud_platform' +require 'opentelemetry/sdk/resources/detectors/telemetry' + +module OpenTelemetry + module SDK + module Resources + module AutoDetector + extend self + + DETECTORS = [ + OpenTelemetry::SDK::Resources::Detectors::GoogleCloudPlatform, + OpenTelemetry::SDK::Resources::Detectors::Telemetry, + ] + + def detect + resources = DETECTORS.map do |detector| + detector.detect(); + end + + resources.reduce(OpenTelemetry::SDK::Resources::Resource.create) do |empty_resource, detected_resource| + empty_resource.merge(detected_resource) + end + end + end + end + end +end diff --git a/sdk/lib/opentelemetry/sdk/resources/constants.rb b/sdk/lib/opentelemetry/sdk/resources/constants.rb new file mode 100644 index 0000000000..74531e900a --- /dev/null +++ b/sdk/lib/opentelemetry/sdk/resources/constants.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Resources + # Attributes describing a service instance. + SERVICE_RESOURCE = { + # Logical name of the service. + name: 'service.name', + + # A namespace for `service.name`. + namespace: 'service.namespace', + + # The string ID of the service instance. + instance_id: 'service.instance.id', + + # The version string of the service API or implementation. + version: 'service.version', + } + + # Attributes describing the telemetry library. + TELEMETRY_SDK_RESOURCE = { + # The name of the telemetry library. + name: 'telemetry.sdk.name', + + # The language of telemetry library and of the code instrumented with it. + language: 'telemetry.sdk.language', + + # The version string of the telemetry library + version: 'telemetry.sdk.version', + } + + # Attributes defining a compute unit (e.g. Container, Process, Lambda + # Function). + CONTAINER_RESOURCE = { + # The container name. + name: 'container.name', + + # The name of the image the container was built on. + image_name: 'container.image.name', + + # The container image tag. + image_tag: 'container.image.tag', + } + + FAAS_RESOURCE = { + # The name of the function being executed. + name: 'faas.name', + + # The unique name of the function being executed. + id: 'faas.id', + + # The version string of the function being executed. + version: 'faas.version', + + # The execution environment ID as a string. + instance: 'faas.instance', + } + + # Attributes defining a deployment service (e.g. Kubernetes). + K8S_RESOURCE = { + # The name of the cluster that the pod is running in. + cluster_name: 'k8s.cluster.name', + + # The name of the namespace that the pod is running in. + namespace_name: 'k8s.namespace.name', + + # The name of the pod. + pod_name: 'k8s.pod.name', + + # The name of the deployment. + deployment_name: 'k8s.deployment.name', + } + + # Attributes defining a computing instance (e.g. host). + HOST_RESOURCE = { + # Hostname of the host. It contains what the hostname command returns on the + # host machine. + hostname: 'host.hostname', + + # Unique host id. For Cloud this must be the instance_id assigned by the + # cloud provider + id: 'host.id', + + # Name of the host. It may contain what hostname returns on Unix systems, + # the fully qualified, or a name specified by the user. + name: 'host.name', + + # Type of host. For Cloud this must be the machine type. + type: 'host.type', + + # Name of the VM image or OS install the host was instantiated from. + image_name: 'host.image.name', + + # VM image id. For Cloud, this value is from the provider. + image_id: 'host.image.id', + + # The version string of the VM image. + image_version: 'host.image.version', + } + + # Attributes defining a running environment (e.g. Cloud, Data Center). + CLOUD_RESOURCE = { + # Name of the cloud provider. Example values are aws, azure, gcp. + provider: 'cloud.provider', + + # The cloud account id used to identify different entities. + account_id: 'cloud.account.id', + + # A specific geographical location where different entities can run. + region: 'cloud.region', + + # Zones are a sub set of the region connected through low-latency links. + zone: 'cloud.zone', + } + end + end +end diff --git a/sdk/lib/opentelemetry/sdk/resources/detectors/google_cloud_platform.rb b/sdk/lib/opentelemetry/sdk/resources/detectors/google_cloud_platform.rb new file mode 100644 index 0000000000..51c206768c --- /dev/null +++ b/sdk/lib/opentelemetry/sdk/resources/detectors/google_cloud_platform.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'google-cloud-env' + +module OpenTelemetry + module SDK + module Resources + module Detectors + module GoogleCloudPlatform + extend self + + def detect + gcp_env = Google::Cloud::Env.new + return Resource.create unless gcp_env.compute_engine? + + resource_labels = {} + resource_labels[CLOUD_RESOURCE[:provider]] = 'gcp' + resource_labels[CLOUD_RESOURCE[:account_id]] = gcp_env.project_id || '' + resource_labels[CLOUD_RESOURCE[:region]] = gcp_env.instance_attribute('cluster-location') || '' + resource_labels[CLOUD_RESOURCE[:zone]] = gcp_env.instance_zone || '' + + resource_labels[HOST_RESOURCE[:hostname]] = hostname + resource_labels[HOST_RESOURCE[:id]] = gcp_env.lookup_metadata('instance', 'id') || '' + resource_labels[HOST_RESOURCE[:name]] = gcp_env.lookup_metadata('instance', 'hostname') || '' + + if gcp_env.kubernetes_engine? + resource_labels[K8S_RESOURCE[:cluster_name]] = gcp_env.instance_attribute('cluster-name') || '' + resource_labels[K8S_RESOURCE[:namespace_name]] = gcp_env.kubernetes_engine_namespace_id || '' + resource_labels[K8S_RESOURCE[:pod_name]] = hostname + + resource_labels[CONTAINER_RESOURCE[:name]] = ENV['CONTAINER_NAME'] || '' + end + + Resource.create(resource_labels) + end + + private + + def hostname + ENV['HOSTNAME'] || `hostname`&.strip + rescue + '' + end + end + end + end + end +end diff --git a/sdk/lib/opentelemetry/sdk/resources/detectors/telemetry.rb b/sdk/lib/opentelemetry/sdk/resources/detectors/telemetry.rb new file mode 100644 index 0000000000..8ba8f05c3a --- /dev/null +++ b/sdk/lib/opentelemetry/sdk/resources/detectors/telemetry.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Resources + module Detectors + module Telemetry + extend self + + def detect + resource_labels = {} + resource_labels[TELEMETRY_SDK_RESOURCE[:name]] = 'OpenTelemetry' + resource_labels[TELEMETRY_SDK_RESOURCE[:language]] = 'ruby' + resource_labels[TELEMETRY_SDK_RESOURCE[:version]] = "semver:#{OpenTelemetry::SDK::VERSION}" + Resource.create(resource_labels) + end + end + end + end + end +end diff --git a/sdk/lib/opentelemetry/sdk/resources/resource.rb b/sdk/lib/opentelemetry/sdk/resources/resource.rb index ebaf32e376..0da7e8fb08 100644 --- a/sdk/lib/opentelemetry/sdk/resources/resource.rb +++ b/sdk/lib/opentelemetry/sdk/resources/resource.rb @@ -74,3 +74,4 @@ def merge(other) end end end + diff --git a/sdk/lib/opentelemetry/sdk/trace/tracer.rb b/sdk/lib/opentelemetry/sdk/trace/tracer.rb index 2ec2e34688..99f058f6c0 100644 --- a/sdk/lib/opentelemetry/sdk/trace/tracer.rb +++ b/sdk/lib/opentelemetry/sdk/trace/tracer.rb @@ -9,21 +9,17 @@ module SDK module Trace # {Tracer} is the SDK implementation of {OpenTelemetry::Trace::Tracer}. class Tracer < OpenTelemetry::Trace::Tracer - attr_reader :name - attr_reader :version + attr_reader :resource # @api private # # Returns a new {Tracer} instance. # - # @param [String] name Instrumentation package name - # @param [String] version Instrumentation package version + # @param [Resource] resource Containing name and version arguments supplied to the TracerProvider # # @return [Tracer] - def initialize(name, version) - @name = name - @version = version - @resource = Resources::Resource.create('name' => name, 'version' => version) + def initialize(resource) + @resource = resource end def start_root_span(name, attributes: nil, links: nil, start_timestamp: nil, kind: nil) diff --git a/sdk/lib/opentelemetry/sdk/trace/tracer_provider.rb b/sdk/lib/opentelemetry/sdk/trace/tracer_provider.rb index d7da2f1b9e..05b435d054 100644 --- a/sdk/lib/opentelemetry/sdk/trace/tracer_provider.rb +++ b/sdk/lib/opentelemetry/sdk/trace/tracer_provider.rb @@ -20,13 +20,14 @@ class TracerProvider < OpenTelemetry::Trace::TracerProvider # Returns a new {TracerProvider} instance. # # @return [TracerProvider] - def initialize + def initialize(resource = OpenTelemetry::SDK::Resources::Resource.create) @mutex = Mutex.new @registry = {} @active_span_processor = NoopSpanProcessor.instance @active_trace_config = Config::TraceConfig::DEFAULT @registered_span_processors = [] @stopped = false + @resource = resource end # Returns a {Tracer} instance. @@ -38,7 +39,8 @@ def initialize def tracer(name = nil, version = nil) name ||= '' version ||= '' - @mutex.synchronize { @registry[Key.new(name, version)] ||= Tracer.new(name, version) } + resource = @resource.merge(OpenTelemetry::SDK::Resources::Resource.create({ 'name' => name, 'version' => version })) + @mutex.synchronize { @registry[Key.new(name, version)] ||= Tracer.new(resource) } end # Attempts to stop all the activity for this {Tracer}. Calls diff --git a/sdk/opentelemetry-sdk.gemspec b/sdk/opentelemetry-sdk.gemspec index 4e2e4a40d3..a84b9981a9 100644 --- a/sdk/opentelemetry-sdk.gemspec +++ b/sdk/opentelemetry-sdk.gemspec @@ -26,6 +26,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 2.5.0' spec.add_dependency 'opentelemetry-api', '~> 0.4.0' + spec.add_dependency 'google-cloud-env' spec.add_development_dependency 'bundler', '>= 1.17' spec.add_development_dependency 'faraday', '~> 0.13' diff --git a/sdk/test/opentelemetry/sdk/resources/auto_detector_test.rb b/sdk/test/opentelemetry/sdk/resources/auto_detector_test.rb new file mode 100644 index 0000000000..931df89ff2 --- /dev/null +++ b/sdk/test/opentelemetry/sdk/resources/auto_detector_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::Resources::AutoDetector do + let(:auto_detector) { OpenTelemetry::SDK::Resources::AutoDetector } + let(:detected_resource) { auto_detector.detect } + let(:detected_resource_labels) { detected_resource.label_enumerator.to_h } + let(:expected_resource_labels) do + { + "telemetry.sdk.name" => "OpenTelemetry", + "telemetry.sdk.language" => "ruby", + "telemetry.sdk.version" => "semver:0.4.0" + } + end + + describe '.detect' do + it 'returns detected resources' do + _(detected_resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) + _(detected_resource_labels).must_equal(expected_resource_labels) + end + end +end diff --git a/sdk/test/opentelemetry/sdk/resources/detectors/google_cloud_platform_test.rb b/sdk/test/opentelemetry/sdk/resources/detectors/google_cloud_platform_test.rb new file mode 100644 index 0000000000..de221046f1 --- /dev/null +++ b/sdk/test/opentelemetry/sdk/resources/detectors/google_cloud_platform_test.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::Resources::Detectors::GoogleCloudPlatform do + let(:detector) { OpenTelemetry::SDK::Resources::Detectors::GoogleCloudPlatform } + + describe '.detect' do + let(:detected_resource) { detector.detect } + let(:detected_resource_labels) { detected_resource.label_enumerator.to_h } + let(:expected_resource_labels) { {} } + + it 'returns an empty resource' do + _(detected_resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) + _(detected_resource_labels).must_equal(expected_resource_labels) + end + + describe 'when in a gcp environment' do + before do + mock = MiniTest::Mock.new + mock.expect(:compute_engine?, true) + mock.expect(:project_id, 'opentelemetry') + mock.expect(:instance_attribute, 'us-central1', ['cluster-location']) + mock.expect(:instance_zone, 'us-central1-a') + mock.expect(:lookup_metadata, 'opentelemetry-test', ['instance', 'id']) + mock.expect(:lookup_metadata, 'opentelemetry-test', ['instance', 'hostname']) + mock.expect(:instance_attribute, 'opentelemetry-cluster', ['cluster-name']) + mock.expect(:kubernetes_engine?, true) + mock.expect(:kubernetes_engine_namespace_id, 'default') + + with_env({ 'HOSTNAME' => 'opentelemetry-test' }) do + Google::Cloud::Env.stub(:new, mock) { detected_resource } + end + end + + let(:expected_resource_labels) do + { + 'cloud.provider' => 'gcp', + 'cloud.account.id' => 'opentelemetry', + 'cloud.region' => 'us-central1', + 'cloud.zone' => 'us-central1-a', + 'host.hostname' => 'opentelemetry-test', + 'host.id' => 'opentelemetry-test', + 'host.name' => 'opentelemetry-test', + 'k8s.cluster.name' => 'opentelemetry-cluster', + 'k8s.namespace.name' => 'default', + 'k8s.pod.name' => 'opentelemetry-test', + 'container.name' => '' + } + end + + it 'returns a resource with gcp attributes' do + _(detected_resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) + _(detected_resource_labels).must_equal(expected_resource_labels) + end + end + end +end + + diff --git a/sdk/test/opentelemetry/sdk/resources/detectors/telemetry_test.rb b/sdk/test/opentelemetry/sdk/resources/detectors/telemetry_test.rb new file mode 100644 index 0000000000..3a54f8c3ad --- /dev/null +++ b/sdk/test/opentelemetry/sdk/resources/detectors/telemetry_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::Resources::Detectors::Telemetry do + let(:detector) { OpenTelemetry::SDK::Resources::Detectors::Telemetry } + + describe '.detect' do + let(:detected_resource) { detector.detect } + let(:detected_resource_labels) { detected_resource.label_enumerator.to_h } + let(:expected_resource_labels) do + { + "telemetry.sdk.name" => "OpenTelemetry", + "telemetry.sdk.language" => "ruby", + "telemetry.sdk.version" => "semver:0.4.0" + } + end + + it 'returns a resource with telemetry attributes' do + _(detected_resource).must_be_instance_of(OpenTelemetry::SDK::Resources::Resource) + _(detected_resource_labels).must_equal(expected_resource_labels) + end + end +end diff --git a/sdk/test/opentelemetry/sdk/trace/tracer_provider_test.rb b/sdk/test/opentelemetry/sdk/trace/tracer_provider_test.rb index bc5ad920fe..7435c446d3 100644 --- a/sdk/test/opentelemetry/sdk/trace/tracer_provider_test.rb +++ b/sdk/test/opentelemetry/sdk/trace/tracer_provider_test.rb @@ -86,8 +86,7 @@ it 'returns a default name-less version-less tracer' do tracer = tracer_provider.tracer - _(tracer.name).must_equal('') - _(tracer.version).must_equal('') + _(tracer.resource.label_enumerator.to_h).must_equal({ 'name' => '', 'version' => '' }) end it 'returns different tracers for different names' do diff --git a/sdk/test/opentelemetry/sdk/trace/tracer_test.rb b/sdk/test/opentelemetry/sdk/trace/tracer_test.rb index a9fc55d502..7e97a6feff 100644 --- a/sdk/test/opentelemetry/sdk/trace/tracer_test.rb +++ b/sdk/test/opentelemetry/sdk/trace/tracer_test.rb @@ -18,15 +18,11 @@ ->(trace_id:, span_id:, parent_context:, links:, name:, kind:, attributes:) { Result.new(decision: Decision::RECORD) } # rubocop:disable Lint/UnusedBlockArgument end - describe '#name' do - it 'reflects the name passed in' do - _(Tracer.new('component', 'semver:1.0').name).must_equal('component') - end - end + describe '#resource' do + let(:resource) { OpenTelemetry::SDK::Resources::Resource.create('a' => 'b') } - describe '#version' do - it 'reflects the version passed in' do - _(Tracer.new('component', 'semver:1.0').version).must_equal('semver:1.0') + it 'reflects the resource passed in' do + _(Tracer.new(resource).resource).must_equal(resource) end end diff --git a/sdk/test/test_helper.rb b/sdk/test/test_helper.rb index 7f8aa09ad5..0d1fe14a89 100644 --- a/sdk/test/test_helper.rb +++ b/sdk/test/test_helper.rb @@ -12,3 +12,12 @@ require 'minitest/autorun' OpenTelemetry.logger = Logger.new('/dev/null') + +def with_env(new_env) + env_to_reset = ENV.select { |k, _| new_env.key?(k) } + keys_to_delete = new_env.keys - ENV.keys + new_env.each_pair { |k, v| ENV[k] = v } + yield + env_to_reset.each_pair { |k, v| ENV[k] = v } + keys_to_delete.each { |k| ENV.delete(k) } +end