# frozen_string_literal: true # Unless explicitly stated otherwise all files in this repository are licensed # under the Apache 2.0 license (see LICENSE). # This product includes software developed at Datadog (https://www.datadoghq.com/). # Copyright 2020 Datadog, Inc. require 'ddtrace/span' require 'ddtrace/ext/http' require 'ddtrace/ext/sql' require 'ddtrace/ext/app_types' require 'ddtrace/contrib/redis/ext' require 'opentelemetry/trace/status' require 'ddtrace/distributed_tracing/headers/headers' module OpenTelemetry module Exporters module Datadog class Exporter # @api private class SpanEncoder ENV_KEY = 'env' VERSION_KEY = 'version' DD_ORIGIN = '_dd_origin' USER_REJECT = -1 AUTO_REJECT = 0 AUTO_KEEP = 1 USER_KEEP = 2 INTERNAL_TRACE_REGEX = %r{/v\d\.\d/traces}.freeze SAMPLE_RATE_METRIC_KEY = '_sample_rate' SAMPLING_PRIORITY_KEY = '_sampling_priority_v1' ORIGIN_REGEX = /#{DD_ORIGIN}\=(.*?)($|,)/.freeze PROBABILITY_REGEX = /\d[.]\d{1,6}/.freeze TRUNCATION_HELPER = ::Datadog::DistributedTracing::Headers::Headers.new({}) INSTRUMENTATION_SPAN_TYPES = { 'OpenTelemetry::Instrumentation::Ethon' => ::Datadog::Ext::HTTP::TYPE_OUTBOUND, 'OpenTelemetry::Instrumentation::Excon' => ::Datadog::Ext::HTTP::TYPE_OUTBOUND, 'OpenTelemetry::Instrumentation::Faraday' => ::Datadog::Ext::HTTP::TYPE_OUTBOUND, 'OpenTelemetry::Instrumentation::Mysql2' => ::Datadog::Ext::SQL::TYPE, 'OpenTelemetry::Instrumentation::Net::HTTP' => ::Datadog::Ext::HTTP::TYPE_OUTBOUND, 'OpenTelemetry::Instrumentation::Rack' => ::Datadog::Ext::HTTP::TYPE_INBOUND, 'OpenTelemetry::Instrumentation::Redis' => ::Datadog::Contrib::Redis::Ext::TYPE, 'OpenTelemetry::Instrumentation::RestClient' => ::Datadog::Ext::HTTP::TYPE_OUTBOUND, 'OpenTelemetry::Instrumentation::Sidekiq' => ::Datadog::Ext::AppTypes::WORKER, 'OpenTelemetry::Instrumentation::Sinatra' => ::Datadog::Ext::HTTP::TYPE_INBOUND }.freeze def translate_to_datadog(otel_spans, service, env = nil, version = nil, tags = nil) # rubocop:disable Metrics/AbcSize datadog_spans = [] default_tags = get_default_tags(tags) || {} otel_spans.each do |span| trace_id, span_id, parent_id = get_trace_ids(span) span_type = get_span_type(span) span_name = get_span_name(span) datadog_span = ::Datadog::Span.new(nil, span_name, service: service, trace_id: trace_id, parent_id: parent_id, resource: get_resource(span), span_type: span_type) # span_id is autogenerated so have to override datadog_span.span_id = span_id datadog_span.start_time = span.start_timestamp datadog_span.end_time = span.end_timestamp # set span.error, span tag error.msg/error.type if span.status && span.status.canonical_code != OpenTelemetry::Trace::Status::OK datadog_span.status = 1 exception_type, exception_msg, exception_stack = get_exception_info(span) if exception_type && exception_msg && exception_stack datadog_span.set_tag('error.type', exception_type) datadog_span.set_tag('error.msg', exception_msg) datadog_span.set_tag('error.stack', exception_stack) end end # set default tags default_tags&.keys&.each do |attribute| datadog_span.set_tag(attribute, span.attributes[attribute]) end origin = get_origin_string(span) datadog_span.set_tag(DD_ORIGIN, origin) if origin && parent_id.zero? datadog_span.set_tag(VERSION_KEY, version) if version && parent_id.zero? datadog_span.set_tag(ENV_KEY, env) if env # set tags - takes precedence over env vars span.attributes&.keys&.each do |attribute| datadog_span.set_tag(attribute, span.attributes[attribute]) end sampling_rate = get_sampling_rate(span) if filter_internal_request?(span) datadog_span.set_metric(SAMPLE_RATE_METRIC_KEY, USER_REJECT) elsif sampling_rate datadog_span.set_metric(SAMPLE_RATE_METRIC_KEY, sampling_rate) end datadog_spans << datadog_span end datadog_spans end def int64(hex_string, base) TRUNCATION_HELPER.value_to_id(hex_string, base) end private def get_trace_ids(span) trace_id = int64(span.trace_id.unpack1('H*'), 16) span_id = int64(span.span_id.unpack1('H*'), 16) parent_id = span.parent_span_id ? int64(span.parent_span_id.unpack1('H*'), 16) || 0 : 0 [trace_id, span_id, parent_id] rescue StandardError => e OpenTelemetry.logger.debug("error encoding trace_ids #{e.message}") [0, 0, 0] end def get_span_type(span) # Get Datadog span type return unless span.instrumentation_library instrumentation_name = span.instrumentation_library.name INSTRUMENTATION_SPAN_TYPES[instrumentation_name] rescue NoMethodError span.name end def get_exception_info(span) # Parse span exception type, msg, and stack from span events error_event = span&.events&.find { |ev| ev.name == 'error' } return unless error_event err_type = error_event.attributes['error.type'] err_msg = error_event.attributes['error.msg'] err_stack = error_event.attributes['error.stack'] [err_type, err_msg, err_stack] rescue StandardError => e OpenTelemetry.logger.debug("error on exception info from span events: #{span.events} , #{e.message}") end def get_resource(span) # Get resource name for http related spans # TODO: how to handle resource naming for broader span types, ie db/cache/queue etc if span.attributes&.key?('http.method') route = span.attributes['http.route'] || span.attributes['http.target'] return span.attributes['http.method'] + ' ' + route if route return span.attributes['http.method'] end span.name rescue StandardError => e OpenTelemetry.logger.debug("error encoding trace_ids #{e.message}") span.name end def get_sampling_rate(span) get_rate_from_description(span) || 1 if span.trace_flags&.sampled? end def get_span_name(span) # Get span name by using instrumentation and kind while backing off to instrumentation_name = span.instrumentation_library&.name kind = span.kind instrumentation_name && kind ? "#{instrumentation_name.to_s.gsub(':', '_')}.#{kind}" : span.name rescue NoMethodError span.name end def get_origin_string(span) tracestate = begin span.tracestate rescue NoMethodError nil end return if tracestate.nil? || tracestate.index(DD_ORIGIN).nil? # Depending on the edge cases in tracestate values this might be # less efficient than mapping string => array => hash. origin_value = tracestate.match(ORIGIN_REGEX) return if origin_value.nil? origin_value[1] rescue StandardError => e OpenTelemetry.logger.debug("error getting origin from trace state, #{e.message}") end def get_default_tags(tags) # Parse a string of tags typically provided via environment variables. # The expected string is of the form: "key1:value1,key2:value2" return {} if tags.nil? tag_map = tags.split(',').map { |kv| kv.split(':') }.to_h if tag_map.keys&.index('') || tag_map.values&.index('') || tag_map.values&.any? { |v| v.ends_with?(':') } OpenTelemetry.logger.debug("malformed tag in default tags: #{tags}") {} else tag_map end end def get_rate_from_description(span) # format to parse of sampler description is # "ProbabilitySampler{1.000000}" or # "AlwaysOnSampler" / "AlwaysOffSampler" # TODO: remove begin/rescue block if PR #282 is accepted+released sampler = begin span.sampler rescue NoMethodError nil end return nil unless sampler&.is_a?(ProbabilitySampler) rate = sampler.description&.match(PROBABILITY_REGEX) return nil unless rate rate[0].to_f(4) rescue StandardError => e # rescue just in case the format changes dramatically in the future OpenTelemetry.logger.warn("error while extracting sampling rate #{e.message} , #{e.backtrace}") nil end def filter_internal_request?(span) span.attributes['http.route'].match(INTERNAL_TRACE_REGEX) if span.attributes&.key?('http.route') end end end end end end