# frozen_string_literal: true require 'opentelemetry-sdk' require 'opentelemetry/exporter/zipkin' require 'opentelemetry/exporter/otlp' require 'opentelemetry/propagator/b3' require 'opentelemetry/trace/propagation/trace_context' require 'opentelemetry/instrumentation/faraday' require 'opentelemetry/instrumentation/net/http' require 'opentelemetry/instrumentation/http' require 'opentelemetry/instrumentation/mongo' require 'opentelemetry/instrumentation/mysql2' require 'opentelemetry/instrumentation/pg' require 'opentelemetry/instrumentation/rack' require 'opentelemetry/instrumentation/rails' require 'opentelemetry/instrumentation/restclient' require 'opentelemetry/instrumentation/sinatra' require_relative './instrumentation' require_relative './instrumentation/data_capture' require_relative './instrumentation/rack_compatible' require_relative './config' require_relative './instrumentation/sinatra' require_relative './version' require 'singleton' # We can't name the class 'Hypertrace::Agent' because the built proto definitions # create a module Hypertrace::Agent :( # RubyAgent is repetitve, but want to remain somewhat consistent compared to python/node class Hypertrace::RubyAgent include Hypertrace::Logging include Singleton SUPPORTED_INSTRUMENTATIONS = [ 'OpenTelemetry::Instrumentation::ActionPack', 'OpenTelemetry::Instrumentation::ActionView', 'OpenTelemetry::Instrumentation::ActiveRecord', 'OpenTelemetry::Instrumentation::ActiveSupport', 'OpenTelemetry::Instrumentation::Faraday', 'OpenTelemetry::Instrumentation::Mongo', 'OpenTelemetry::Instrumentation::Mysql2', 'OpenTelemetry::Instrumentation::PG', 'OpenTelemetry::Instrumentation::Rack', 'OpenTelemetry::Instrumentation::Sinatra', 'OpenTelemetry::Instrumentation::Net::HTTP', 'OpenTelemetry::Instrumentation::HTTP', 'OpenTelemetry::Instrumentation::RestClient' ].freeze SUPPORTED_INSTRUMENTATIONS_ADDITIONAL_PATCH = { 'OpenTelemetry::Instrumentation::Faraday' => ['./instrumentation/faraday_patch'], 'OpenTelemetry::Instrumentation::Rack' => ['./instrumentation/rack', './instrumentation/rack_env_getter'], 'OpenTelemetry::Instrumentation::Net::HTTP' => ['./instrumentation/net_http_patch'], 'OpenTelemetry::Instrumentation::HTTP' => ['./instrumentation/http_patch'], 'OpenTelemetry::Instrumentation::RestClient' => ['./instrumentation/rest_client_patch'], } def self.instrument! self.instance.instrument! end def self.instrument_as_additional! self.instance.instrument_as_additional! end def self.config self.instance.config end def initialize(version = Hypertrace::VERSION) log.info { "Initializing Hypertrace" } @config = Hypertrace::Config::Config.new @version = version log.info { "Hypertrace version: #{Hypertrace::VERSION}" } log.info { "Ruby version: #{RUBY_VERSION}" } end def config @config.config end def instrument! initalize_tracer end def instrument_as_additional! resource = OpenTelemetry::SDK::Resources::Resource.create(create_resource_attributes) exporter = create_resource_customized_exporter resource span_processor = create_span_processor exporter existing_processors = OpenTelemetry.tracer_provider.instance_variable_get(:"@span_processors") if existing_processors.nil? || existing_processors&.empty? log.error("No existing tracer_provider found, continuing without Hypertrace instrumentation") return end existing_processors << span_processor OpenTelemetry.tracer_provider.instance_variable_set(:'@span_processors', existing_processors) # We don't pass a span_processor or resource because we have to add it to the already configured tracer provider # The resource will be added at the export phase configure_instrumentation nil, nil configure_propagators end def create_span_processor exporter return OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(EXPORTER) if ENV['HT_CI_TEST'] != nil return OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter) end def initalize_tracer resource = OpenTelemetry::SDK::Resources::Resource.create(create_resource_attributes) exporter = create_exporter span_processor = create_span_processor(exporter) # TODO: Extra resource Attributes From Config configure_instrumentation span_processor, resource configure_propagators end def configure_instrumentation span_processor, resource OpenTelemetry::SDK.configure do |c| c.add_span_processor span_processor if span_processor c.resource = resource if resource SUPPORTED_INSTRUMENTATIONS.each do |instrumentation_string| c.use instrumentation_string additional_patches = SUPPORTED_INSTRUMENTATIONS_ADDITIONAL_PATCH[instrumentation_string] if additional_patches additional_patches.each do |patch_file| apply_custom_patch patch_file end end end end end private def create_resource_customized_exporter resource exporter = nil if config.reporting.trace_reporter_type == :OTLP verify_mode = config.reporting.secure.value ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE exporter = Hypertrace::OtlpHttpExporter.new(endpoint: config.reporting.endpoint.value, ssl_verify_mode: verify_mode, is_hypertrace: true, supported_instrumentation_libraries: SUPPORTED_INSTRUMENTATIONS, custom_resource: resource) return exporter end log.error "Resource customized exporter not supported for zipkin, only OTLP HTTP" end def create_exporter exporter = nil if config.reporting.trace_reporter_type == :OTLP verify_mode = config.reporting.secure.value ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: config.reporting.endpoint.value, ssl_verify_mode: verify_mode) return exporter end if config.reporting.trace_reporter_type == :ZIPKIN exporter = OpenTelemetry::Exporter::Zipkin::Exporter.new(endpoint: config.reporting.endpoint.value) return exporter end end def configure_propagators propagator_list = [] [config.propagation_formats.to_a].each do |format| if format[0] == :TRACECONTEXT propagator_list << OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator elsif format[0] == :B3 propagator_list << OpenTelemetry::Propagator::B3::Single.text_map_propagator end end if propagator_list.empty? log.warn { "No propagators were added!" } end OpenTelemetry.propagation = OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators(propagator_list.compact) end private def apply_custom_patch file begin require_relative file log.debug { "Applied patch for #{file}" } rescue => _e log.debug { "Unable to apply patch for #{file} this is most likely because the library is unavailable or an unsupported version" } end end def create_resource_attributes { 'service.name': config.service_name.value, 'service.instance.id': Process.pid, 'telemetry.sdk.version': @version, 'telemetry.sdk.name': 'hypertrace', 'telemetry.sdk.language': 'ruby' }.transform_keys(&:to_s) end end