From cecf4cb3defe954875ab952164b9351f682fb603 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Mon, 28 Sep 2026 19:20:12 +0530 Subject: [PATCH 01/12] feat(otlp-exporter): add protocol, semconv_stability, and insecure options to OTLP configuration Signed-off-by: Arjun Rajappa --- lib/instana/config.rb | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/instana/config.rb b/lib/instana/config.rb index 40e7994e..73d1d9c0 100644 --- a/lib/instana/config.rb +++ b/lib/instana/config.rb @@ -4,7 +4,7 @@ require 'yaml' module Instana - class Config + class Config # rubocop:disable Metrics/ClassLength LEGACY_TRACING_KEY = 'com.instana.tracing'.freeze TRACING_KEY = 'tracing'.freeze @@ -63,6 +63,9 @@ def initialize(logger: ::Instana.logger, agent_host: ENV.fetch('INSTANA_AGENT_HO certificate: nil, client_key: nil, client_certificate: nil, + protocol: 'http/protobuf', + semconv_stability: 'stable', + insecure: false, config_source: 'default' } read_otlp_config @@ -257,6 +260,9 @@ def read_otlp_config_from_agent(tracing_config) @config[:otlp][:certificate] = otlp_config['certificate'] if otlp_config['certificate'] @config[:otlp][:client_key] = otlp_config['client_key'] if otlp_config['client_key'] @config[:otlp][:client_certificate] = otlp_config['client_certificate'] if otlp_config['client_certificate'] + @config[:otlp][:protocol] = otlp_config['protocol'] if otlp_config['protocol'] + @config[:otlp][:semconv_stability] = otlp_config['semconv_stability'] if otlp_config['semconv_stability'] + @config[:otlp][:insecure] = truthy?(otlp_config['insecure']) unless otlp_config['insecure'].nil? @config[:otlp][:config_source] = 'agent' end @@ -381,6 +387,9 @@ def build_otlp_yaml_result(otlp_yaml) result[:certificate] = otlp_yaml['certificate'] if otlp_yaml['certificate'] result[:client_key] = otlp_yaml['client_key'] if otlp_yaml['client_key'] result[:client_certificate] = otlp_yaml['client_certificate'] if otlp_yaml['client_certificate'] + result[:protocol] = otlp_yaml['protocol'] if otlp_yaml['protocol'] + result[:semconv_stability] = otlp_yaml['semconv_stability'] if otlp_yaml['semconv_stability'] + result[:insecure] = truthy?(otlp_yaml['insecure']) unless otlp_yaml['insecure'].nil? result.empty? ? nil : result end @@ -399,6 +408,9 @@ def parse_otlp_config_from_env result[:certificate] = raw[:certificate] if raw[:certificate] result[:client_key] = raw[:client_key] if raw[:client_key] result[:client_certificate] = raw[:client_cert] if raw[:client_cert] + result[:protocol] = raw[:protocol] if raw[:protocol] + result[:semconv_stability] = raw[:semconv_stability] if raw[:semconv_stability] + result[:insecure] = truthy?(raw[:insecure_raw]) unless raw[:insecure_raw].nil? result end @@ -413,7 +425,10 @@ def otlp_env_vars headers_raw: ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_HEADERS', nil) || ENV.fetch('OTEL_EXPORTER_OTLP_HEADERS', nil), certificate: ENV.fetch('OTEL_EXPORTER_OTLP_CERTIFICATE', nil), client_key: ENV.fetch('OTEL_EXPORTER_OTLP_CLIENT_KEY', nil), - client_cert: ENV.fetch('OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE', nil) + client_cert: ENV.fetch('OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE', nil), + protocol: ENV.fetch('OTEL_EXPORTER_OTLP_PROTOCOL', nil), + semconv_stability: ENV.fetch('OTEL_SEMCONV_STABILITY_OPT_IN', nil), + insecure_raw: ENV.fetch('OTEL_EXPORTER_OTLP_INSECURE', nil) } end From 7179ece134595626466d63cef1c7f13fa3aa5e4b Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Mon, 28 Sep 2026 19:20:12 +0530 Subject: [PATCH 02/12] feat(otlp-exporter): configure ssl_verify_mode based on insecure setting Signed-off-by: Arjun Rajappa --- lib/instana/backend/host_agent_reporting_observer.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/instana/backend/host_agent_reporting_observer.rb b/lib/instana/backend/host_agent_reporting_observer.rb index fbab3608..d1e4026e 100644 --- a/lib/instana/backend/host_agent_reporting_observer.rb +++ b/lib/instana/backend/host_agent_reporting_observer.rb @@ -186,11 +186,12 @@ def initialize_otlp_exporter endpoint = resolve_otlp_endpoint(config[:endpoint], config[:config_source]) opts = { endpoint: endpoint, timeout: config[:timeout] / 1000.0 } - opts[:compression] = config[:compression] if config[:compression] - opts[:headers] = config[:headers] if config[:headers]&.any? - opts[:certificate_file] = config[:certificate] if config[:certificate] - opts[:client_certificate_file] = config[:client_certificate] if config[:client_certificate] - opts[:client_key_file] = config[:client_key] if config[:client_key] + opts[:compression] = config[:compression] if config[:compression] + opts[:headers] = config[:headers] if config[:headers]&.any? + opts[:certificate_file] = config[:certificate] if config[:certificate] + opts[:client_certificate_file] = config[:client_certificate] if config[:client_certificate] + opts[:client_key_file] = config[:client_key] if config[:client_key] + opts[:ssl_verify_mode] = OpenSSL::SSL::VERIFY_NONE if config[:insecure] @otlp_exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(**opts) rescue StandardError => e From 7b00428c94c2992fee61a7bc12640ac0ffa851b6 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Mon, 28 Sep 2026 19:20:12 +0530 Subject: [PATCH 03/12] feat(otlp-exporter): align AWS S3 and Lambda attributes with semantic conventions Signed-off-by: Arjun Rajappa --- lib/instana/exporter/otlp/aws_converter.rb | 17 +++++-- test/exporter/otlp/aws_converter_test.rb | 56 ++++++++++++++++++---- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/lib/instana/exporter/otlp/aws_converter.rb b/lib/instana/exporter/otlp/aws_converter.rb index d28725bd..5ab70787 100644 --- a/lib/instana/exporter/otlp/aws_converter.rb +++ b/lib/instana/exporter/otlp/aws_converter.rb @@ -6,6 +6,9 @@ require 'opentelemetry/semconv/incubating/messaging' require 'opentelemetry/semconv/db' require 'opentelemetry/semconv/incubating/db' +require 'opentelemetry/semconv/incubating/rpc' +require 'opentelemetry/semconv/incubating/cloud' +require 'opentelemetry/semconv/incubating/faas' module Instana module Exporter @@ -128,10 +131,18 @@ def convert_dynamodb_attributes(attributes, dynamodb_data) def convert_s3_attributes(attributes, s3_data) return unless s3_data - add_attribute(attributes, 'aws.service', 's3') + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_SYSTEM_NAME, 'aws-api') + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_METHOD, s3_rpc_method(s3_data[:op])) + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::CLOUD::CLOUD_PROVIDER, 'aws') + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::CLOUD::CLOUD_REGION, s3_data[:region]) add_attribute(attributes, 'aws.s3.bucket', s3_data[:bucket]) add_attribute(attributes, 'aws.s3.key', s3_data[:key]) - add_attribute(attributes, 'aws.s3.operation', s3_data[:op]) + end + + def s3_rpc_method(operation) + return unless operation + + "S3.#{operation}" end def convert_lambda_attributes(attributes, lambda_data) @@ -140,7 +151,7 @@ def convert_lambda_attributes(attributes, lambda_data) add_attribute(attributes, 'aws.service', 'lambda') add_attribute(attributes, 'aws.lambda.function_name', lambda_data[:function]) add_attribute(attributes, 'aws.lambda.invocation_type', lambda_data[:type]) - add_attribute(attributes, 'faas.invoked_name', lambda_data[:function]) + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::FAAS::FAAS_INVOKED_NAME, lambda_data[:function]) end end end diff --git a/test/exporter/otlp/aws_converter_test.rb b/test/exporter/otlp/aws_converter_test.rb index fa43ede6..f04e335f 100644 --- a/test/exporter/otlp/aws_converter_test.rb +++ b/test/exporter/otlp/aws_converter_test.rb @@ -207,10 +207,13 @@ def test_s3_basic_conversion converter = Instana::Exporter::Otlp::AwsConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 's3', attrs['aws.service'] + assert_equal 'aws-api', attrs['rpc.system.name'] + assert_equal 'S3.GetObject', attrs['rpc.method'] + assert_equal 'aws', attrs['cloud.provider'] assert_equal 'my-bucket', attrs['aws.s3.bucket'] assert_equal 'path/to/file.txt', attrs['aws.s3.key'] - assert_equal 'GetObject', attrs['aws.s3.operation'] + assert_nil attrs['aws.service'] + assert_nil attrs['aws.s3.operation'] end def test_s3_put_object_operation @@ -220,10 +223,10 @@ def test_s3_put_object_operation converter = Instana::Exporter::Otlp::AwsConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 's3', attrs['aws.service'] + assert_equal 'aws-api', attrs['rpc.system.name'] + assert_equal 'S3.PutObject', attrs['rpc.method'] assert_equal 'uploads-bucket', attrs['aws.s3.bucket'] assert_equal 'uploads/image.jpg', attrs['aws.s3.key'] - assert_equal 'PutObject', attrs['aws.s3.operation'] end def test_s3_delete_object_operation @@ -233,10 +236,10 @@ def test_s3_delete_object_operation converter = Instana::Exporter::Otlp::AwsConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 's3', attrs['aws.service'] + assert_equal 'aws-api', attrs['rpc.system.name'] + assert_equal 'S3.DeleteObject', attrs['rpc.method'] assert_equal 'temp-bucket', attrs['aws.s3.bucket'] assert_equal 'temp/file.tmp', attrs['aws.s3.key'] - assert_equal 'DeleteObject', attrs['aws.s3.operation'] end def test_s3_list_objects_operation @@ -246,10 +249,47 @@ def test_s3_list_objects_operation converter = Instana::Exporter::Otlp::AwsConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 's3', attrs['aws.service'] + assert_equal 'aws-api', attrs['rpc.system.name'] + assert_equal 'S3.ListObjects', attrs['rpc.method'] assert_equal 'data-bucket', attrs['aws.s3.bucket'] assert_nil attrs['aws.s3.key'] - assert_equal 'ListObjects', attrs['aws.s3.operation'] + assert_nil attrs['aws.s3.operation'] + end + + def test_s3_with_region + span = create_span('aws.s3', { + s3: { bucket: 'regional-bucket', op: 'PutObject', region: 'us-west-2' } + }) + converter = Instana::Exporter::Otlp::AwsConverter.new(span) + attrs = converter.send(:convert_attributes) + + assert_equal 'aws-api', attrs['rpc.system.name'] + assert_equal 'S3.PutObject', attrs['rpc.method'] + assert_equal 'aws', attrs['cloud.provider'] + assert_equal 'us-west-2', attrs['cloud.region'] + assert_equal 'regional-bucket', attrs['aws.s3.bucket'] + end + + def test_s3_without_region_omits_cloud_region + span = create_span('aws.s3', { + s3: { bucket: 'no-region-bucket', op: 'GetObject' } + }) + converter = Instana::Exporter::Otlp::AwsConverter.new(span) + attrs = converter.send(:convert_attributes) + + assert_equal 'aws', attrs['cloud.provider'] + assert_nil attrs['cloud.region'] + end + + def test_s3_without_op_omits_rpc_method + span = create_span('aws.s3', { + s3: { bucket: 'some-bucket' } + }) + converter = Instana::Exporter::Otlp::AwsConverter.new(span) + attrs = converter.send(:convert_attributes) + + assert_equal 'aws-api', attrs['rpc.system.name'] + assert_nil attrs['rpc.method'] end # AWS Lambda Tests From defcba95f00dea041a56340be97b0867e7e6d255 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Mon, 28 Sep 2026 19:20:12 +0530 Subject: [PATCH 04/12] feat(otlp-exporter): stop populating span name as exception and error type Signed-off-by: Arjun Rajappa --- lib/instana/exporter/otlp/base_converter.rb | 4 ++-- test/exporter/otlp/base_converter_test.rb | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/instana/exporter/otlp/base_converter.rb b/lib/instana/exporter/otlp/base_converter.rb index 1075d24d..40e49f71 100644 --- a/lib/instana/exporter/otlp/base_converter.rb +++ b/lib/instana/exporter/otlp/base_converter.rb @@ -312,12 +312,12 @@ def build_error_events(error_count, error_msg, stacktrace) timestamp = calculate_end_timestamp if stacktrace - attrs = { 'exception.type' => span_name } + attrs = {} attrs['exception.message'] = error_msg if error_msg attrs['exception.stacktrace'] = stacktrace [Event.new('exception', attrs, timestamp)] else - [Event.new('error', { 'error.type' => span_name }, timestamp)] + [Event.new('error', {}, timestamp)] end end diff --git a/test/exporter/otlp/base_converter_test.rb b/test/exporter/otlp/base_converter_test.rb index bdf4a31a..7a6fdadd 100644 --- a/test/exporter/otlp/base_converter_test.rb +++ b/test/exporter/otlp/base_converter_test.rb @@ -177,7 +177,7 @@ def test_exception_event_emitted_when_ec_positive_and_stack_present assert_equal 1, events.size event = events.first assert_equal 'exception', event.name - assert_equal 'rack', event.attributes['exception.type'] + refute event.attributes.key?('exception.type'), 'exception.type must not be set to span_name' assert_equal 'Timeout', event.attributes['exception.message'] assert_equal '/app/lib/client.rb:7 in in `call`', event.attributes['exception.stacktrace'] assert_kind_of Integer, event.timestamp @@ -192,7 +192,8 @@ def test_error_event_emitted_when_ec_positive_but_no_stack assert_equal 1, events.size event = events.first assert_equal 'error', event.name - assert_equal 'rack', event.attributes['error.type'] + refute event.attributes.key?('error.type'), 'error.type must not be set to span_name' + assert_empty event.attributes end def test_convert_returns_span_data_with_events_on_error From 031bf77d026ccf3724c5daed9e82334fc6548787 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Mon, 28 Sep 2026 19:20:12 +0530 Subject: [PATCH 05/12] feat(otlp-exporter): add ActiveJob support and update messaging operation attribute to semantic conventions Signed-off-by: Arjun Rajappa --- .../exporter/otlp/background_job_converter.rb | 13 +++++++- .../otlp/background_job_converter_test.rb | 32 ++++++++++++++++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/lib/instana/exporter/otlp/background_job_converter.rb b/lib/instana/exporter/otlp/background_job_converter.rb index c40f2354..4092bb9f 100644 --- a/lib/instana/exporter/otlp/background_job_converter.rb +++ b/lib/instana/exporter/otlp/background_job_converter.rb @@ -41,6 +41,8 @@ def convert_attributes convert_job_attributes(attributes, span[:'resque-client'] || span[:data]&.[](:'resque-client'), 'resque', 'publish') elsif span_type == 'resque-worker' convert_job_attributes(attributes, span[:'resque-worker'] || span[:data]&.[](:'resque-worker'), 'resque', 'process') + elsif span_type == 'activejob' + convert_activejob_attributes(attributes, span[:data]&.[](:activejob)) end attributes @@ -53,13 +55,22 @@ def convert_job_attributes(attributes, data, system, operation) add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_SYSTEM, system) add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_DESTINATION_NAME, data[:queue] || data['queue']) - add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION, operation) + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_NAME, operation) add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_MESSAGE_ID, data[:job_id] || data['job_id']) add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_CONSUMER_GROUP_NAME, data[:job] || data['job']) add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, extract_host(data[:'redis-url'] || data['redis-url'])) add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, extract_port(data[:'redis-url'] || data['redis-url'])) end + def convert_activejob_attributes(attributes, data) + return unless data + + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_SYSTEM, 'activejob') + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_DESTINATION_NAME, data[:queue] || data['queue']) + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_NAME, data[:action] || data['action']) + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_MESSAGE_ID, data[:job_id] || data['job_id']) + end + def extract_host(connection) return nil unless connection diff --git a/test/exporter/otlp/background_job_converter_test.rb b/test/exporter/otlp/background_job_converter_test.rb index 3598582c..32f43931 100644 --- a/test/exporter/otlp/background_job_converter_test.rb +++ b/test/exporter/otlp/background_job_converter_test.rb @@ -13,7 +13,7 @@ def test_sidekiq_client_conversion assert_equal 'sidekiq', attrs['messaging.system'] assert_equal 'default', attrs['messaging.destination.name'] - assert_equal 'publish', attrs['messaging.operation'] + assert_equal 'publish', attrs['messaging.operation.name'] assert_equal '123', attrs['messaging.message.id'] assert_equal 'TestWorker', attrs['messaging.consumer.group.name'] assert_equal 'localhost', attrs['server.address'] @@ -29,7 +29,7 @@ def test_sidekiq_worker_conversion assert_equal 'sidekiq', attrs['messaging.system'] assert_equal 'critical', attrs['messaging.destination.name'] - assert_equal 'process', attrs['messaging.operation'] + assert_equal 'process', attrs['messaging.operation.name'] assert_equal '456', attrs['messaging.message.id'] assert_equal 'EmailWorker', attrs['messaging.consumer.group.name'] end @@ -43,7 +43,7 @@ def test_resque_client_conversion assert_equal 'resque', attrs['messaging.system'] assert_equal 'low', attrs['messaging.destination.name'] - assert_equal 'publish', attrs['messaging.operation'] + assert_equal 'publish', attrs['messaging.operation.name'] end def test_resque_worker_conversion @@ -54,7 +54,31 @@ def test_resque_worker_conversion attrs = converter.send(:convert_attributes) assert_equal 'resque', attrs['messaging.system'] - assert_equal 'process', attrs['messaging.operation'] + assert_equal 'process', attrs['messaging.operation.name'] + end + + def test_activejob_conversion + span = create_span('activejob', { + activejob: { queue: 'mailers', action: 'perform', job_id: 'abc-123' } + }) + converter = Instana::Exporter::Otlp::BackgroundJobConverter.new(span) + attrs = converter.send(:convert_attributes) + + assert_equal 'activejob', attrs['messaging.system'] + assert_equal 'mailers', attrs['messaging.destination.name'] + assert_equal 'perform', attrs['messaging.operation.name'] + assert_equal 'abc-123', attrs['messaging.message.id'] + end + + def test_activejob_without_job_id + span = create_span('activejob', { + activejob: { queue: 'default', action: 'perform' } + }) + attrs = Instana::Exporter::Otlp::BackgroundJobConverter.new(span).send(:convert_attributes) + + assert_equal 'activejob', attrs['messaging.system'] + assert_equal 'default', attrs['messaging.destination.name'] + assert_nil attrs['messaging.message.id'] end def test_extract_host From 447e75c2755e43a3cd791aa329c6425d63ee8c7c Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Mon, 28 Sep 2026 19:20:12 +0530 Subject: [PATCH 06/12] feat(otlp-exporter): normalize database system names and align attributes with semantic conventions Signed-off-by: Arjun Rajappa --- .../exporter/otlp/database_converter.rb | 28 ++++++-- test/exporter/otlp/database_converter_test.rb | 65 ++++++++++++++++++- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/lib/instana/exporter/otlp/database_converter.rb b/lib/instana/exporter/otlp/database_converter.rb index 3b158e29..18f0ea5e 100644 --- a/lib/instana/exporter/otlp/database_converter.rb +++ b/lib/instana/exporter/otlp/database_converter.rb @@ -82,32 +82,51 @@ def mongo_span_name(mongo) parts.empty? ? 'mongodb' : parts.join('.') end + # Normalize an ActiveRecord/Sequel adapter name to OTel db.system.name. + # Only adapters that are actively instrumented and tested are listed here. + DB_SYSTEM_NORMALIZATION = { + 'mysql2' => 'mysql', + 'sqlite3' => 'sqlite', + 'postgresql' => 'postgresql', + 'mysql' => 'mysql', + 'sqlite' => 'sqlite' + }.freeze + + def normalize_db_system(adapter) + return unless adapter + + DB_SYSTEM_NORMALIZATION.fetch(adapter.to_s.downcase, adapter.to_s.downcase) + end + def convert_activerecord_attributes(attributes, ar_data) return unless ar_data - add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, ar_data[:adapter]) + add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, normalize_db_system(ar_data[:adapter])) add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_NAMESPACE, ar_data[:db]) add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_QUERY_TEXT, ar_data[:sql]) add_attribute(attributes, OpenTelemetry::SemConv::Incubating::DB::DB_USER, ar_data[:username]) add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, ar_data[:host]) + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::DB::DB_COLLECTION_NAME, ar_data[:table]) end def convert_sequel_attributes(attributes, seq_data) return unless seq_data - add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, seq_data[:adapter]) + add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, normalize_db_system(seq_data[:adapter])) add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_NAMESPACE, seq_data[:db]) add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_QUERY_TEXT, seq_data[:sql]) add_attribute(attributes, OpenTelemetry::SemConv::Incubating::DB::DB_USER, seq_data[:username]) add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, seq_data[:host]) + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::DB::DB_COLLECTION_NAME, seq_data[:table]) end def convert_redis_attributes(attributes, redis_data) return unless redis_data add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_SYSTEM_NAME, 'redis') + add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_OPERATION_NAME, redis_data[:command]) add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_QUERY_TEXT, redis_data[:command]) - add_attribute(attributes, 'db.redis.database_index', redis_data[:db]) + add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_NAMESPACE, redis_data[:db]&.to_s) add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, extract_host(redis_data[:connection])) add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, extract_port(redis_data[:connection])) end @@ -119,7 +138,7 @@ def convert_memcache_attributes(attributes, mc_data) add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_OPERATION_NAME, mc_data[:command]) add_attribute(attributes, 'db.memcached.key', mc_data[:key]) add_attribute(attributes, 'db.memcached.keys', mc_data[:keys]) - add_attribute(attributes, 'db.memcached.namespace', mc_data[:namespace]) + add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_NAMESPACE, mc_data[:namespace]) add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, extract_host(mc_data[:server])) add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, extract_port(mc_data[:server])) end @@ -131,6 +150,7 @@ def convert_mongo_attributes(attributes, mongo_data) add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_NAMESPACE, mongo_data[:namespace]) add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_OPERATION_NAME, mongo_data[:command]) add_attribute(attributes, OpenTelemetry::SemConv::DB::DB_QUERY_TEXT, mongo_data[:json]) + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::DB::DB_COLLECTION_NAME, mongo_data[:collection]) add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, mongo_data.dig(:peer, :hostname)) add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, mongo_data.dig(:peer, :port)) end diff --git a/test/exporter/otlp/database_converter_test.rb b/test/exporter/otlp/database_converter_test.rb index 172eadfa..ad3fc584 100644 --- a/test/exporter/otlp/database_converter_test.rb +++ b/test/exporter/otlp/database_converter_test.rb @@ -18,6 +18,38 @@ def test_activerecord_conversion assert_equal 'db.example.com', attrs['server.address'] end + def test_activerecord_adapter_normalization_mysql2 + span = create_span(:activerecord, { + activerecord: { adapter: 'mysql2', db: 'mydb', sql: 'SELECT 1' } + }) + attrs = Instana::Exporter::Otlp::DatabaseConverter.new(span).send(:convert_attributes) + assert_equal 'mysql', attrs['db.system.name'] + end + + def test_activerecord_adapter_normalization_sqlite3 + span = create_span(:activerecord, { + activerecord: { adapter: 'sqlite3', db: 'dev.db', sql: 'SELECT 1' } + }) + attrs = Instana::Exporter::Otlp::DatabaseConverter.new(span).send(:convert_attributes) + assert_equal 'sqlite', attrs['db.system.name'] + end + + def test_activerecord_with_table_sets_collection_name + span = create_span(:activerecord, { + activerecord: { adapter: 'postgresql', db: 'mydb', sql: 'SELECT * FROM users', table: 'users' } + }) + attrs = Instana::Exporter::Otlp::DatabaseConverter.new(span).send(:convert_attributes) + assert_equal 'users', attrs['db.collection.name'] + end + + def test_activerecord_without_table_omits_collection_name + span = create_span(:activerecord, { + activerecord: { adapter: 'postgresql', db: 'mydb', sql: 'SELECT 1' } + }) + attrs = Instana::Exporter::Otlp::DatabaseConverter.new(span).send(:convert_attributes) + assert_nil attrs['db.collection.name'] + end + def test_sequel_conversion span = create_span(:sequel, { sequel: { adapter: 'mysql2', db: 'testdb', sql: 'INSERT INTO logs', username: 'root', host: 'localhost' } @@ -25,13 +57,21 @@ def test_sequel_conversion converter = Instana::Exporter::Otlp::DatabaseConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 'mysql2', attrs['db.system.name'] + assert_equal 'mysql', attrs['db.system.name'] assert_equal 'testdb', attrs['db.namespace'] assert_equal 'INSERT INTO logs', attrs['db.query.text'] assert_equal 'root', attrs['db.user'] assert_equal 'localhost', attrs['server.address'] end + def test_sequel_with_table_sets_collection_name + span = create_span(:sequel, { + sequel: { adapter: 'postgresql', db: 'mydb', sql: 'SELECT 1', table: 'orders' } + }) + attrs = Instana::Exporter::Otlp::DatabaseConverter.new(span).send(:convert_attributes) + assert_equal 'orders', attrs['db.collection.name'] + end + def test_redis_conversion span = create_span(:redis, { redis: { command: 'GET key', db: 2, connection: 'redis.local:6379' } @@ -40,8 +80,10 @@ def test_redis_conversion attrs = converter.send(:convert_attributes) assert_equal 'redis', attrs['db.system.name'] + assert_equal 'GET key', attrs['db.operation.name'] assert_equal 'GET key', attrs['db.query.text'] - assert_equal 2, attrs['db.redis.database_index'] + assert_equal '2', attrs['db.namespace'] + assert_nil attrs['db.redis.database_index'] assert_equal 'redis.local', attrs['server.address'] assert_equal 6379, attrs['server.port'] end @@ -56,7 +98,8 @@ def test_memcache_conversion assert_equal 'memcached', attrs['db.system.name'] assert_equal 'get', attrs['db.operation.name'] assert_equal 'user:123', attrs['db.memcached.key'] - assert_equal 'app', attrs['db.memcached.namespace'] + assert_equal 'app', attrs['db.namespace'] + assert_nil attrs['db.memcached.namespace'] assert_equal '127.0.0.1', attrs['server.address'] assert_equal 11211, attrs['server.port'] end @@ -86,6 +129,22 @@ def test_mongodb_conversion assert_equal 27017, attrs['server.port'] end + def test_mongodb_with_collection_sets_collection_name + span = create_span(:mongo, { + mongo: { namespace: 'mydb', command: 'insert', collection: 'users', peer: { hostname: 'localhost', port: 27017 } } + }) + attrs = Instana::Exporter::Otlp::DatabaseConverter.new(span).send(:convert_attributes) + assert_equal 'users', attrs['db.collection.name'] + end + + def test_mongodb_without_collection_omits_collection_name + span = create_span(:mongo, { + mongo: { namespace: 'mydb', command: 'find', peer: { hostname: 'localhost', port: 27017 } } + }) + attrs = Instana::Exporter::Otlp::DatabaseConverter.new(span).send(:convert_attributes) + assert_nil attrs['db.collection.name'] + end + def test_extract_host span = create_span(:redis, {}) converter = Instana::Exporter::Otlp::DatabaseConverter.new(span) From 1206c54a3ee8943d9f7b96d3715f392e02cc49cb Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Mon, 28 Sep 2026 19:20:12 +0530 Subject: [PATCH 07/12] feat(otlp-exporter): align RPC converter attributes and gRPC status codes with semantic conventions Signed-off-by: Arjun Rajappa --- lib/instana/exporter/otlp/rpc_converter.rb | 68 ++++++++++++++++++++-- test/exporter/otlp/rpc_converter_test.rb | 42 +++++++++++-- 2 files changed, 102 insertions(+), 8 deletions(-) diff --git a/lib/instana/exporter/otlp/rpc_converter.rb b/lib/instana/exporter/otlp/rpc_converter.rb index f641cd43..8617f037 100644 --- a/lib/instana/exporter/otlp/rpc_converter.rb +++ b/lib/instana/exporter/otlp/rpc_converter.rb @@ -6,6 +6,7 @@ require 'opentelemetry/semconv/incubating/rpc' require 'opentelemetry/semconv/incubating/code' require 'opentelemetry/semconv/server' +require 'opentelemetry/semconv/network' module Instana module Exporter @@ -48,10 +49,13 @@ def convert_attributes private + # gRPC status code mapping: 0 = OK for success, otherwise derive from error + GRPC_STATUS_CODE_OK = 0 + # Convert gRPC span attributes def convert_grpc_attributes(attributes, rpc_data) # RPC system - add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_SYSTEM, 'grpc') + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_SYSTEM_NAME, 'grpc') # RPC service and method if rpc_data[:call] @@ -60,9 +64,20 @@ def convert_grpc_attributes(attributes, rpc_data) add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_METHOD, method) end - # Network peer + # Server host and port add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, rpc_data[:host]) - add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, rpc_data.dig(:peer, :address)) + add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, parse_port(rpc_data[:port])) + + # Network peer address (peer.address → network.peer.address) + peer = rpc_data[:peer] + if peer + add_attribute(attributes, OpenTelemetry::SemConv::NETWORK::NETWORK_PEER_ADDRESS, peer[:address]) + add_attribute(attributes, OpenTelemetry::SemConv::NETWORK::NETWORK_PEER_PORT, parse_port(peer[:port])) + end + + # gRPC status code (integer): 0 = OK for success spans + grpc_status = grpc_status_code(rpc_data) + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_GRPC_STATUS_CODE, grpc_status) # gRPC-specific attributes add_attribute(attributes, 'rpc.grpc.call_type', rpc_data[:call_type]) @@ -71,7 +86,7 @@ def convert_grpc_attributes(attributes, rpc_data) # Convert ActionCable span attributes def convert_action_cable_attributes(attributes, rpc_data) # RPC system - add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_SYSTEM, 'actioncable') + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_SYSTEM_NAME, 'actioncable') # ActionCable-specific attributes add_attribute(attributes, 'rails.actioncable.channel', rpc_data[:call]) @@ -96,6 +111,51 @@ def parse_grpc_call(call) [parts[1], parts[2]] end + + # Parse port value to integer, returns nil if invalid or out of range + def parse_port(port) + return nil unless port + + int_port = port.to_i + int_port if int_port >= 0 && int_port <= 65_535 + end + + # Map to gRPC status code integer. + # For successful spans (ec = 0) → 0 (OK). + # For error spans with a recognized gRPC status string in the error → map it. + # Otherwise → nil (omit the attribute). + GRPC_STATUS_CODES = { + 'OK' => 0, + 'CANCELLED' => 1, + 'UNKNOWN' => 2, + 'INVALID_ARGUMENT' => 3, + 'DEADLINE_EXCEEDED' => 4, + 'NOT_FOUND' => 5, + 'ALREADY_EXISTS' => 6, + 'PERMISSION_DENIED' => 7, + 'RESOURCE_EXHAUSTED' => 8, + 'FAILED_PRECONDITION' => 9, + 'ABORTED' => 10, + 'OUT_OF_RANGE' => 11, + 'UNIMPLEMENTED' => 12, + 'INTERNAL' => 13, + 'UNAVAILABLE' => 14, + 'DATA_LOSS' => 15, + 'UNAUTHENTICATED' => 16 + }.freeze + + def grpc_status_code(rpc_data) + error_count = span[:ec].to_i + return GRPC_STATUS_CODE_OK if error_count.zero? + + # Attempt to extract gRPC status string from error message + error_msg = rpc_data[:error].to_s + GRPC_STATUS_CODES.each do |name, code| + return code if error_msg.include?(name) + end + + GRPC_STATUS_CODES['UNKNOWN'] + end end end end diff --git a/test/exporter/otlp/rpc_converter_test.rb b/test/exporter/otlp/rpc_converter_test.rb index a4c7438b..5dcf4695 100644 --- a/test/exporter/otlp/rpc_converter_test.rb +++ b/test/exporter/otlp/rpc_converter_test.rb @@ -11,21 +11,55 @@ def test_grpc_conversion converter = Instana::Exporter::Otlp::RpcConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 'grpc', attrs['rpc.system'] + assert_equal 'grpc', attrs['rpc.system.name'] assert_equal 'package.Service', attrs['rpc.service'] assert_equal 'Method', attrs['rpc.method'] assert_equal 'grpc.example.com', attrs['server.address'] assert_equal 'unary', attrs['rpc.grpc.call_type'] + assert_equal 0, attrs['rpc.grpc.status_code'] + end + + def test_grpc_with_port + span = create_span(:grpc, { + rpc: { call: '/test.API/Get', host: 'grpc.example.com', port: '50051' } + }) + converter = Instana::Exporter::Otlp::RpcConverter.new(span) + attrs = converter.send(:convert_attributes) + + assert_equal 'grpc.example.com', attrs['server.address'] + assert_equal 50_051, attrs['server.port'] end def test_grpc_with_peer_address span = create_span(:grpc, { - rpc: { call: '/test.API/Get', peer: { address: '10.0.0.1' } } + rpc: { call: '/test.API/Get', peer: { address: '10.0.0.1', port: 50_051 } } }) converter = Instana::Exporter::Otlp::RpcConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal '10.0.0.1', attrs['server.address'] + assert_equal '10.0.0.1', attrs['network.peer.address'] + assert_equal 50_051, attrs['network.peer.port'] + assert_nil attrs['server.address'] + end + + def test_grpc_status_code_ok_on_success + span = create_span(:grpc, { rpc: { call: '/svc/Method', host: 'h' } }) + attrs = Instana::Exporter::Otlp::RpcConverter.new(span).send(:convert_attributes) + assert_equal 0, attrs['rpc.grpc.status_code'] + end + + def test_grpc_status_code_from_error_message + span = create_span(:grpc, { rpc: { call: '/svc/Method', error: 'DEADLINE_EXCEEDED: context deadline exceeded' } }) + span[:ec] = 1 + attrs = Instana::Exporter::Otlp::RpcConverter.new(span).send(:convert_attributes) + assert_equal 4, attrs['rpc.grpc.status_code'] + end + + def test_grpc_status_code_unknown_on_unrecognized_error + span = create_span(:grpc, { rpc: { call: '/svc/Method', error: 'some unknown error' } }) + span[:ec] = 1 + attrs = Instana::Exporter::Otlp::RpcConverter.new(span).send(:convert_attributes) + assert_equal 2, attrs['rpc.grpc.status_code'] end def test_actioncable_conversion @@ -36,7 +70,7 @@ def test_actioncable_conversion converter = Instana::Exporter::Otlp::RpcConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 'actioncable', attrs['rpc.system'] + assert_equal 'actioncable', attrs['rpc.system.name'] assert_equal 'ChatChannel#speak', attrs['rails.actioncable.channel'] assert_equal 'action', attrs['rails.actioncable.call_type'] assert_equal 'my-app', attrs['rpc.service'] From 8426ed8a76ddd829fcea4cfc4a42f86a74b9389e Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Mon, 28 Sep 2026 19:20:12 +0530 Subject: [PATCH 08/12] feat(otlp-exporter): add Kafka support and enhance messaging semantic conventions Signed-off-by: Arjun Rajappa --- .../exporter/otlp/messaging_converter.rb | 88 ++++++++++++++----- .../exporter/otlp/messaging_converter_test.rb | 44 +++++++++- 2 files changed, 110 insertions(+), 22 deletions(-) diff --git a/lib/instana/exporter/otlp/messaging_converter.rb b/lib/instana/exporter/otlp/messaging_converter.rb index aa0c88a1..2d9ca138 100644 --- a/lib/instana/exporter/otlp/messaging_converter.rb +++ b/lib/instana/exporter/otlp/messaging_converter.rb @@ -11,15 +11,36 @@ module Exporter module Otlp # Converter for messaging spans to OTLP format class MessagingConverter < BaseConverter - # Build OTel-compliant span name for messaging (RabbitMQ) spans + # Build OTel-compliant span name for messaging spans # - # Formula per SPAN_NAME_PATTERNS.txt Section 3 (bunny/AMQP): - # publish → "{exchange} publish" (or "{queue} publish" when no exchange) - # receive → "{queue} receive" + # Formula per SPAN_NAME_PATTERNS.txt Section 3 (bunny/AMQP / Kafka): + # RabbitMQ publish → "{exchange} publish" (or "{queue} publish" when no exchange) + # RabbitMQ receive → "{queue} receive" + # Kafka → "{service} {access}" (e.g., "orders send") # # @return [String] The span name def span_name - rabbitmq_data = span[:data]&.[](:rabbitmq) || {} + if (rabbitmq_data = span[:data]&.[](:rabbitmq)) + rabbitmq_span_name(rabbitmq_data) + elsif (kafka_data = span[:data]&.[](:kafka)) + kafka_span_name(kafka_data) + else + super + end + end + + def convert_attributes + attributes = {} + + convert_rabbitmq_attributes(attributes, span[:data]&.[](:rabbitmq)) + convert_kafka_attributes(attributes, span[:data]&.[](:kafka)) + + attributes + end + + private + + def rabbitmq_span_name(rabbitmq_data) sort = rabbitmq_data[:sort].to_s if sort == 'publish' @@ -32,26 +53,51 @@ def span_name end end - def convert_attributes - attributes = {} + def kafka_span_name(kafka_data) + service = kafka_data[:service].to_s.strip + access = kafka_data[:access].to_s.strip + parts = [service, access].reject(&:empty?) + parts.empty? ? 'kafka' : parts.join(' ') + end - rabbitmq_data = span[:data]&.[](:rabbitmq) - if rabbitmq_data - add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_SYSTEM, 'rabbitmq') - add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_DESTINATION_NAME, - rabbitmq_destination_name(rabbitmq_data)) - add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY, rabbitmq_data[:key]) - add_attribute(attributes, 'messaging.rabbitmq.queue', rabbitmq_data[:queue]) - add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, rabbitmq_data[:address]) - - operation = rabbitmq_data[:sort] == 'publish' ? 'send' : 'receive' - add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_TYPE, operation) - end + def convert_rabbitmq_attributes(attributes, rabbitmq_data) + return unless rabbitmq_data - attributes + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_SYSTEM, 'rabbitmq') + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_DESTINATION_NAME, + rabbitmq_destination_name(rabbitmq_data)) + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY, rabbitmq_data[:key]) + add_attribute(attributes, 'messaging.rabbitmq.queue', rabbitmq_data[:queue]) + add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS, extract_host(rabbitmq_data[:address])) + add_attribute(attributes, OpenTelemetry::SemConv::SERVER::SERVER_PORT, extract_port(rabbitmq_data[:address])) + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_MESSAGE_BODY_SIZE, rabbitmq_data[:size]) + + operation = rabbitmq_data[:sort] == 'publish' ? 'send' : 'receive' + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_TYPE, operation) + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_NAME, rabbitmq_data[:sort]) end - private + def convert_kafka_attributes(attributes, kafka_data) + return unless kafka_data + + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_SYSTEM, 'kafka') + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_DESTINATION_NAME, kafka_data[:service]) + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_NAME, kafka_data[:access]) + add_attribute(attributes, OpenTelemetry::SemConv::Incubating::MESSAGING::MESSAGING_OPERATION_TYPE, kafka_data[:access]) + end + + def extract_host(address) + return nil unless address + + address.to_s.split(':').first + end + + def extract_port(address) + return nil unless address + + port = address.to_s.split(':').last + port.to_i if port =~ /^\d+$/ + end # Build the composite destination name per spec: # Producer (publish): "{exchange}:{key}" — omit absent parts diff --git a/test/exporter/otlp/messaging_converter_test.rb b/test/exporter/otlp/messaging_converter_test.rb index 82867c56..8db6a7a5 100644 --- a/test/exporter/otlp/messaging_converter_test.rb +++ b/test/exporter/otlp/messaging_converter_test.rb @@ -6,7 +6,7 @@ class MessagingConverterTest < Minitest::Test def test_rabbitmq_publish_conversion span = create_span(:rabbitmq, { - rabbitmq: { exchange: 'orders', key: 'order.created', queue: 'order_queue', address: 'rabbitmq.local', sort: 'publish' } + rabbitmq: { exchange: 'orders', key: 'order.created', queue: 'order_queue', address: 'rabbitmq.local:5672', sort: 'publish' } }) converter = Instana::Exporter::Otlp::MessagingConverter.new(span) attrs = converter.send(:convert_attributes) @@ -17,7 +17,17 @@ def test_rabbitmq_publish_conversion assert_equal 'order.created', attrs['messaging.rabbitmq.destination.routing_key'] assert_equal 'order_queue', attrs['messaging.rabbitmq.queue'] assert_equal 'rabbitmq.local', attrs['server.address'] + assert_equal 5672, attrs['server.port'] assert_equal 'send', attrs['messaging.operation.type'] + assert_equal 'publish', attrs['messaging.operation.name'] + end + + def test_rabbitmq_with_body_size + span = create_span(:rabbitmq, { + rabbitmq: { exchange: 'logs', key: 'log.event', sort: 'publish', size: 512 } + }) + attrs = Instana::Exporter::Otlp::MessagingConverter.new(span).send(:convert_attributes) + assert_equal 512, attrs['messaging.message.body.size'] end def test_rabbitmq_consume_conversion @@ -56,6 +66,38 @@ def test_rabbitmq_consume_deduplicates_key_equals_queue assert_equal 'events:signup_queue', attrs['messaging.destination.name'] end + def test_kafka_send_conversion + span = create_span(:kafka, { + kafka: { service: 'orders-topic', access: 'send' } + }) + converter = Instana::Exporter::Otlp::MessagingConverter.new(span) + attrs = converter.send(:convert_attributes) + + assert_equal 'kafka', attrs['messaging.system'] + assert_equal 'orders-topic', attrs['messaging.destination.name'] + assert_equal 'send', attrs['messaging.operation.name'] + assert_equal 'send', attrs['messaging.operation.type'] + end + + def test_kafka_consume_conversion + span = create_span(:kafka, { + kafka: { service: 'events-topic', access: 'consume' } + }) + converter = Instana::Exporter::Otlp::MessagingConverter.new(span) + attrs = converter.send(:convert_attributes) + + assert_equal 'kafka', attrs['messaging.system'] + assert_equal 'events-topic', attrs['messaging.destination.name'] + assert_equal 'consume', attrs['messaging.operation.name'] + assert_equal 'consume', attrs['messaging.operation.type'] + end + + def test_span_name_kafka + span = create_span(:kafka, { kafka: { service: 'orders', access: 'send' } }) + result = Instana::Exporter::Otlp::MessagingConverter.new(span).convert + assert_equal 'orders send', result[:name] + end + def test_rabbitmq_minimal_data span = create_span(:rabbitmq, { rabbitmq: { exchange: 'logs', sort: 'publish' } From e9d90d69cce8bb7d275c9dcde59e941df5b86958 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Mon, 28 Sep 2026 19:20:12 +0530 Subject: [PATCH 09/12] feat(otlp-exporter): use network semconv constants and add config unit tests Signed-off-by: Arjun Rajappa --- lib/instana/exporter/otlp/http_converter.rb | 5 +- test/config_test.rb | 104 +++++++++++++++++++- 2 files changed, 103 insertions(+), 6 deletions(-) diff --git a/lib/instana/exporter/otlp/http_converter.rb b/lib/instana/exporter/otlp/http_converter.rb index 99a40543..332271e6 100644 --- a/lib/instana/exporter/otlp/http_converter.rb +++ b/lib/instana/exporter/otlp/http_converter.rb @@ -7,6 +7,7 @@ require 'opentelemetry/semconv/url' require 'opentelemetry/semconv/server' require 'opentelemetry/semconv/user_agent' +require 'opentelemetry/semconv/network' module Instana module Exporter @@ -120,8 +121,8 @@ def add_protocol_attributes(attributes, protocol) name = parts[0].downcase version = parts[1] - add_attribute(attributes, 'network.protocol.name', name) - add_attribute(attributes, 'network.protocol.version', version) + add_attribute(attributes, OpenTelemetry::SemConv::NETWORK::NETWORK_PROTOCOL_NAME, name) + add_attribute(attributes, OpenTelemetry::SemConv::NETWORK::NETWORK_PROTOCOL_VERSION, version) end end end diff --git a/test/config_test.rb b/test/config_test.rb index 4696c841..135691e0 100644 --- a/test/config_test.rb +++ b/test/config_test.rb @@ -711,7 +711,7 @@ def test_yaml_technology_config_not_overridden_by_agent_when_no_env # OTLP configuration tests # ============================================================================ -class OtlpConfigTest < Minitest::Test +class OtlpConfigTest < Minitest::Test # rubocop:disable Metrics/ClassLength OTLP_ENV_VARS = %w[ INSTANA_TRACING_OTLP_ENABLED OTEL_EXPORTER_OTLP_TRACES_ENDPOINT @@ -723,6 +723,9 @@ class OtlpConfigTest < Minitest::Test OTEL_EXPORTER_OTLP_CERTIFICATE OTEL_EXPORTER_OTLP_CLIENT_KEY OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE + OTEL_EXPORTER_OTLP_PROTOCOL + OTEL_SEMCONV_STABILITY_OPT_IN + OTEL_EXPORTER_OTLP_INSECURE INSTANA_CONFIG_PATH ].freeze @@ -872,6 +875,71 @@ def test_general_headers_used_when_no_traces_headers assert_equal({ 'x-general-key' => 'general-secret' }, subject[:otlp][:headers]) end + # ── protocol ────────────────────────────────────────────────────────────── + + def test_protocol_from_env + ENV['OTEL_EXPORTER_OTLP_PROTOCOL'] = 'grpc' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + assert_equal 'grpc', subject[:otlp][:protocol] + end + + def test_protocol_default_is_http_protobuf + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + assert_equal 'http/protobuf', subject[:otlp][:protocol] + end + + # ── semconv_stability ───────────────────────────────────────────────────── + + def test_semconv_stability_from_env + ENV['OTEL_SEMCONV_STABILITY_OPT_IN'] = 'development' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + assert_equal 'development', subject[:otlp][:semconv_stability] + end + + def test_semconv_stability_default_is_stable + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + assert_equal 'stable', subject[:otlp][:semconv_stability] + end + + # ── insecure ────────────────────────────────────────────────────────────── + + def test_insecure_true_from_env + ENV['OTEL_EXPORTER_OTLP_INSECURE'] = 'true' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + assert_equal true, subject[:otlp][:insecure] + end + + def test_insecure_false_by_default + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + assert_equal false, subject[:otlp][:insecure] + end + + def test_insecure_truthy_variants + %w[1 yes YES True TRUE].each do |val| + ENV['OTEL_EXPORTER_OTLP_INSECURE'] = val + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + assert subject[:otlp][:insecure], "Expected insecure=true for OTEL_EXPORTER_OTLP_INSECURE=#{val}" + end + end + + def test_insecure_false_string_leaves_disabled + ENV['OTEL_EXPORTER_OTLP_INSECURE'] = 'false' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + assert_equal false, subject[:otlp][:insecure] + assert_equal 'env', subject[:otlp][:config_source] + end + # ── TLS fields ──────────────────────────────────────────────────────────── def test_tls_fields_from_env @@ -886,6 +954,28 @@ def test_tls_fields_from_env assert_equal '/etc/certs/client.crt', subject[:otlp][:client_certificate] end + # ── protocol, semconv_stability, insecure via YAML ──────────────────────── + + def test_yaml_protocol_and_semconv_stability + yaml_content = <<~YAML + tracing: + otlp: + enabled: true + protocol: grpc + semconv_stability: development + insecure: true + YAML + + File.write('test_otlp_config.yaml', yaml_content) + ENV['INSTANA_CONFIG_PATH'] = 'test_otlp_config.yaml' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + assert_equal 'grpc', subject[:otlp][:protocol] + assert_equal 'development', subject[:otlp][:semconv_stability] + assert_equal true, subject[:otlp][:insecure] + end + # ── YAML configuration ──────────────────────────────────────────────────── def test_yaml_populates_otlp_config @@ -976,7 +1066,10 @@ def test_agent_discovery_sets_otlp_config_when_source_is_default 'otlp' => { 'enabled' => 'true', 'endpoint' => 'http://agent.example.com/v1/traces', - 'timeout' => 8000 + 'timeout' => 8000, + 'protocol' => 'grpc', + 'semconv_stability' => 'development', + 'insecure' => 'true' } } } @@ -984,8 +1077,11 @@ def test_agent_discovery_sets_otlp_config_when_source_is_default assert_equal true, subject[:otlp][:enabled] assert_equal 'http://agent.example.com/v1/traces', subject[:otlp][:endpoint] - assert_equal 8000, subject[:otlp][:timeout] - assert_equal 'agent', subject[:otlp][:config_source] + assert_equal 8000, subject[:otlp][:timeout] + assert_equal 'grpc', subject[:otlp][:protocol] + assert_equal 'development', subject[:otlp][:semconv_stability] + assert_equal true, subject[:otlp][:insecure] + assert_equal 'agent', subject[:otlp][:config_source] end def test_agent_discovery_does_not_override_env_config From 1b138e82212381ce8ce8643205183ce9bb72c676 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Mon, 28 Sep 2026 19:28:49 +0530 Subject: [PATCH 10/12] feat(otlp-exporter): fix linting errors Signed-off-by: Arjun Rajappa --- lib/instana/exporter/otlp/database_converter.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/instana/exporter/otlp/database_converter.rb b/lib/instana/exporter/otlp/database_converter.rb index 18f0ea5e..21c604c9 100644 --- a/lib/instana/exporter/otlp/database_converter.rb +++ b/lib/instana/exporter/otlp/database_converter.rb @@ -85,11 +85,11 @@ def mongo_span_name(mongo) # Normalize an ActiveRecord/Sequel adapter name to OTel db.system.name. # Only adapters that are actively instrumented and tested are listed here. DB_SYSTEM_NORMALIZATION = { - 'mysql2' => 'mysql', - 'sqlite3' => 'sqlite', - 'postgresql' => 'postgresql', - 'mysql' => 'mysql', - 'sqlite' => 'sqlite' + 'mysql2' => 'mysql', + 'sqlite3' => 'sqlite', + 'postgresql' => 'postgresql', + 'mysql' => 'mysql', + 'sqlite' => 'sqlite' }.freeze def normalize_db_system(adapter) From 6529537bba56a03634a0a2c37c8e6407fe06b6c8 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Mon, 28 Sep 2026 21:16:34 +0530 Subject: [PATCH 11/12] feat(otlp-exporter): add support for ruby 3.2 where RPC_SYSTEM_NAME was missing Signed-off-by: Arjun Rajappa --- lib/instana/exporter/otlp/aws_converter.rb | 2 +- lib/instana/exporter/otlp/base_converter.rb | 12 ++++++++++++ lib/instana/exporter/otlp/rpc_converter.rb | 4 ++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/instana/exporter/otlp/aws_converter.rb b/lib/instana/exporter/otlp/aws_converter.rb index 5ab70787..66068a5e 100644 --- a/lib/instana/exporter/otlp/aws_converter.rb +++ b/lib/instana/exporter/otlp/aws_converter.rb @@ -131,7 +131,7 @@ def convert_dynamodb_attributes(attributes, dynamodb_data) def convert_s3_attributes(attributes, s3_data) return unless s3_data - add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_SYSTEM_NAME, 'aws-api') + add_attribute(attributes, Instana::Exporter::Otlp::RPC_SYSTEM_ATTR, 'aws-api') add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_METHOD, s3_rpc_method(s3_data[:op])) add_attribute(attributes, OpenTelemetry::SemConv::Incubating::CLOUD::CLOUD_PROVIDER, 'aws') add_attribute(attributes, OpenTelemetry::SemConv::Incubating::CLOUD::CLOUD_REGION, s3_data[:region]) diff --git a/lib/instana/exporter/otlp/base_converter.rb b/lib/instana/exporter/otlp/base_converter.rb index 40e49f71..f03de390 100644 --- a/lib/instana/exporter/otlp/base_converter.rb +++ b/lib/instana/exporter/otlp/base_converter.rb @@ -5,10 +5,22 @@ require_relative 'resource' require 'opentelemetry/trace' require 'forwardable' +require 'opentelemetry/semconv/incubating/rpc' module Instana module Exporter module Otlp + # Compatibility shim: RPC_SYSTEM_NAME was added in a newer release of + # opentelemetry-semantic_conventions. Fall back to the deprecated + # RPC_SYSTEM constant ('rpc.system') on older installs (e.g. Ruby 3.2 + # lockfiles that resolve an earlier gem version). + RPC_SYSTEM_ATTR = + if OpenTelemetry::SemConv::Incubating::RPC.const_defined?(:RPC_SYSTEM_NAME) + OpenTelemetry::SemConv::Incubating::RPC::RPC_SYSTEM_NAME + else + OpenTelemetry::SemConv::Incubating::RPC::RPC_SYSTEM + end + # Base class for all OTLP span converters # # Provides common interface and shared functionality for converting Instana spans diff --git a/lib/instana/exporter/otlp/rpc_converter.rb b/lib/instana/exporter/otlp/rpc_converter.rb index 8617f037..57dc5548 100644 --- a/lib/instana/exporter/otlp/rpc_converter.rb +++ b/lib/instana/exporter/otlp/rpc_converter.rb @@ -55,7 +55,7 @@ def convert_attributes # Convert gRPC span attributes def convert_grpc_attributes(attributes, rpc_data) # RPC system - add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_SYSTEM_NAME, 'grpc') + add_attribute(attributes, Instana::Exporter::Otlp::RPC_SYSTEM_ATTR, 'grpc') # RPC service and method if rpc_data[:call] @@ -86,7 +86,7 @@ def convert_grpc_attributes(attributes, rpc_data) # Convert ActionCable span attributes def convert_action_cable_attributes(attributes, rpc_data) # RPC system - add_attribute(attributes, OpenTelemetry::SemConv::Incubating::RPC::RPC_SYSTEM_NAME, 'actioncable') + add_attribute(attributes, Instana::Exporter::Otlp::RPC_SYSTEM_ATTR, 'actioncable') # ActionCable-specific attributes add_attribute(attributes, 'rails.actioncable.channel', rpc_data[:call]) From 061f9d02d1f95b265e637ce7c568d17543893ab9 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Tue, 29 Sep 2026 10:48:44 +0530 Subject: [PATCH 12/12] feat(otlp-exporter): use RPC_SYSTEM_ATTR constant in converter tests Replace hardcoded 'rpc.system.name' string with the RPC_SYSTEM_ATTR shim constant so tests pass on both old (rpc.system) and new (rpc.system.name) semconv gem versions. Signed-off-by: Arjun Rajappa --- test/exporter/otlp/aws_converter_test.rb | 12 ++++++------ test/exporter/otlp/rpc_converter_test.rb | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/exporter/otlp/aws_converter_test.rb b/test/exporter/otlp/aws_converter_test.rb index f04e335f..b0dde1a6 100644 --- a/test/exporter/otlp/aws_converter_test.rb +++ b/test/exporter/otlp/aws_converter_test.rb @@ -207,7 +207,7 @@ def test_s3_basic_conversion converter = Instana::Exporter::Otlp::AwsConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 'aws-api', attrs['rpc.system.name'] + assert_equal 'aws-api', attrs[Instana::Exporter::Otlp::RPC_SYSTEM_ATTR] assert_equal 'S3.GetObject', attrs['rpc.method'] assert_equal 'aws', attrs['cloud.provider'] assert_equal 'my-bucket', attrs['aws.s3.bucket'] @@ -223,7 +223,7 @@ def test_s3_put_object_operation converter = Instana::Exporter::Otlp::AwsConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 'aws-api', attrs['rpc.system.name'] + assert_equal 'aws-api', attrs[Instana::Exporter::Otlp::RPC_SYSTEM_ATTR] assert_equal 'S3.PutObject', attrs['rpc.method'] assert_equal 'uploads-bucket', attrs['aws.s3.bucket'] assert_equal 'uploads/image.jpg', attrs['aws.s3.key'] @@ -236,7 +236,7 @@ def test_s3_delete_object_operation converter = Instana::Exporter::Otlp::AwsConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 'aws-api', attrs['rpc.system.name'] + assert_equal 'aws-api', attrs[Instana::Exporter::Otlp::RPC_SYSTEM_ATTR] assert_equal 'S3.DeleteObject', attrs['rpc.method'] assert_equal 'temp-bucket', attrs['aws.s3.bucket'] assert_equal 'temp/file.tmp', attrs['aws.s3.key'] @@ -249,7 +249,7 @@ def test_s3_list_objects_operation converter = Instana::Exporter::Otlp::AwsConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 'aws-api', attrs['rpc.system.name'] + assert_equal 'aws-api', attrs[Instana::Exporter::Otlp::RPC_SYSTEM_ATTR] assert_equal 'S3.ListObjects', attrs['rpc.method'] assert_equal 'data-bucket', attrs['aws.s3.bucket'] assert_nil attrs['aws.s3.key'] @@ -263,7 +263,7 @@ def test_s3_with_region converter = Instana::Exporter::Otlp::AwsConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 'aws-api', attrs['rpc.system.name'] + assert_equal 'aws-api', attrs[Instana::Exporter::Otlp::RPC_SYSTEM_ATTR] assert_equal 'S3.PutObject', attrs['rpc.method'] assert_equal 'aws', attrs['cloud.provider'] assert_equal 'us-west-2', attrs['cloud.region'] @@ -288,7 +288,7 @@ def test_s3_without_op_omits_rpc_method converter = Instana::Exporter::Otlp::AwsConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 'aws-api', attrs['rpc.system.name'] + assert_equal 'aws-api', attrs[Instana::Exporter::Otlp::RPC_SYSTEM_ATTR] assert_nil attrs['rpc.method'] end diff --git a/test/exporter/otlp/rpc_converter_test.rb b/test/exporter/otlp/rpc_converter_test.rb index 5dcf4695..6edb854f 100644 --- a/test/exporter/otlp/rpc_converter_test.rb +++ b/test/exporter/otlp/rpc_converter_test.rb @@ -11,7 +11,7 @@ def test_grpc_conversion converter = Instana::Exporter::Otlp::RpcConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 'grpc', attrs['rpc.system.name'] + assert_equal 'grpc', attrs[Instana::Exporter::Otlp::RPC_SYSTEM_ATTR] assert_equal 'package.Service', attrs['rpc.service'] assert_equal 'Method', attrs['rpc.method'] assert_equal 'grpc.example.com', attrs['server.address'] @@ -70,7 +70,7 @@ def test_actioncable_conversion converter = Instana::Exporter::Otlp::RpcConverter.new(span) attrs = converter.send(:convert_attributes) - assert_equal 'actioncable', attrs['rpc.system.name'] + assert_equal 'actioncable', attrs[Instana::Exporter::Otlp::RPC_SYSTEM_ATTR] assert_equal 'ChatChannel#speak', attrs['rails.actioncable.channel'] assert_equal 'action', attrs['rails.actioncable.call_type'] assert_equal 'my-app', attrs['rpc.service']