Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions instana.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ Gem::Specification.new do |spec|
spec.add_runtime_dependency('csv', '>= 0.1')
spec.add_runtime_dependency('sys-proctable', '>= 1.2.2')
spec.add_runtime_dependency('opentelemetry-api', '~> 1.4')
# TODO: pin the versions of otel gems which are actual implementation
spec.add_runtime_dependency('opentelemetry-common')
spec.add_runtime_dependency('opentelemetry-semantic_conventions')
spec.add_runtime_dependency('opentelemetry-exporter-otlp')
spec.add_runtime_dependency('cgi')
spec.add_runtime_dependency('oj', '>=3.0.11') unless RUBY_PLATFORM =~ /java/i
end
29 changes: 25 additions & 4 deletions lib/instana/backend/host_agent_reporting_observer.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# (c) Copyright IBM Corp. 2021
# (c) Copyright Instana Inc. 2021
require 'opentelemetry/exporter/otlp'
require_relative '../exporter/otlp/converter_factory'

module Instana
module Backend
Expand All @@ -22,7 +24,13 @@ def initialize(client, discovery, logger: ::Instana.logger, timer_class: Concurr
@timer_class = timer_class
@nonce = Time.now
@processor = processor

if ENV["INSTANA_OTLP_ENABLED"]
@otlp_exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(
endpoint: 'http://localhost:4318/v1/traces',
Comment thread
arjun-rajappa marked this conversation as resolved.
timeout: 5.0, # in seconds
compression: 'gzip'
)
end
# Initialize timers with default 1 second interval
@metrics_timer = @timer_class.new(execution_interval: 1, run_now: true) { report_metrics_to_backend }
@traces_timer = @timer_class.new(execution_interval: 1, run_now: true) { report_traces_to_backend }
Expand Down Expand Up @@ -90,10 +98,23 @@ def report_traces
path = format(TRACES_DATA_URL, discovery['pid'])

@processor.send do |spans|
response = @client.send_request('POST', path, spans)
success = false
if @otlp_exporter
converted_spans = spans.map do |span|
::Instana::Exporter::Otlp::ConverterFactory.create(span).convert
end
Instana.logger.info(converted_spans)
result_code = @otlp_exporter.export(converted_spans)
Instana.logger.debug("using otlp exporter to export result code: #{result_code}")
success = result_code == OpenTelemetry::SDK::Trace::Export::SUCCESS
else
response = @client.send_request('POST', path, spans)
Instana.logger.debug("using instana native exporter to export result code: #{response}")
success = response&.ok?
end

unless response.ok?
@logger.warn("Failed to send `#{spans.count}` spans to `#{path}`. Response: #{response.code} - #{response.body}")
unless success
@logger.warn("Failed to send `#{spans.count}` spans to `#{path}`.")
trigger_rediscovery
break
end
Expand Down
6 changes: 3 additions & 3 deletions lib/instana/exporter/otlp/base_converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,9 @@ def calculate_end_timestamp
#
# @return [Symbol] Inferred span kind
def infer_span_kind_from_name
span_name = span[:n]&.to_sym
return :server if ::Instana::SpanKind::ENTRY_SPANS.include?(span_name)
return :client if ::Instana::SpanKind::EXIT_SPANS.include?(span_name)
name = span[:n]&.to_sym
return :server if ::Instana::SpanKind::ENTRY_SPANS.include?(name)
return :client if ::Instana::SpanKind::EXIT_SPANS.include?(name)

:internal
end
Expand Down
10 changes: 5 additions & 5 deletions lib/instana/exporter/otlp/converter_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,31 +70,31 @@ def get_converter_class(span_type)
# Check if span is an HTTP span
# Uses the HTTP_SPANS constant to identify HTTP spans
def http_span?(span)
Instana::SpanKind::HTTP_SPANS.include?(span.name&.to_sym)
Instana::SpanKind::HTTP_SPANS.include?(span[:n]&.to_sym)
end

# Check if span is a database span
# Instana native spans always have a name, so we only check the name
def database_span?(span)
span.name&.match?(/sql|database|query|activerecord|sequel|mongo|redis|dalli/i)
span[:n]&.match?(/sql|database|query|activerecord|sequel|mongo|redis|dalli/i)
end

# Check if span is a messaging span
# Instana native spans always have a name, so we only check the name
def messaging_span?(span)
span.name&.match?(/kafka|rabbitmq|sqs|sns|message|bunny|shoryuken/i)
span[:n]&.match?(/kafka|rabbitmq|sqs|sns|message|bunny|shoryuken/i)
end

# Check if span is an RPC span
# Instana native spans always have a name, so we only check the name
def rpc_span?(span)
span.name&.match?(/grpc|rpc/i)
span[:n]&.match?(/grpc|rpc/i)
end

# Check if span is a custom span
# Instana native spans always have a name, so we only check the name
def custom_span?(span)
span.name&.match?(/custom|sdk/i)
span[:n]&.match?(/custom|sdk/i)
end
end
end
Expand Down
2 changes: 0 additions & 2 deletions lib/instana/exporter/otlp/http_converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ module Otlp
# Converter for HTTP spans to OTLP format
# Handles conversion of HTTP-related spans with specific attributes
class HttpConverter < BaseConverter
private

# Extract HTTP-specific attributes as plain key/value pairs
# @return [Hash] HTTP attributes
def convert_attributes
Expand Down
263 changes: 262 additions & 1 deletion test/backend/host_agent_reporting_observer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

require 'test_helper'

class HostAgentReportingObserverTest < Minitest::Test
class HostAgentReportingObserverTest < Minitest::Test # rubocop:disable Metrics/ClassLength
def test_start_stop
client = Instana::Backend::RequestClient.new('10.10.10.10', 9292)
discovery = Concurrent::Atom.new(nil)
Expand Down Expand Up @@ -318,4 +318,265 @@ def test_poll_rate_changes_metrics_timer_interval
# Verify traces_timer always stays at 1 second
assert_equal 1, subject.traces_timer.opts[:execution_interval]
end

# ============================================================================
# OTLP EXPORT TESTS (INSTANA_OTLP_ENABLED environment variable)
# ============================================================================

def test_otlp_export_enabled_with_env_variable
ENV['INSTANA_OTLP_ENABLED'] = 'true'

stub_request(:post, "http://10.10.10.10:9292/com.instana.plugin.ruby.1234")
.to_return(status: 200)

client = Instana::Backend::RequestClient.new('10.10.10.10', 9292)
discovery = Concurrent::Atom.new({'pid' => 1234})

exported_spans = nil
otlp_exporter = Minitest::Mock.new
otlp_exporter.expect(:export, OpenTelemetry::SDK::Trace::Export::SUCCESS) do |spans|
exported_spans = spans
OpenTelemetry::SDK::Trace::Export::SUCCESS
end

processor = Class.new do
def send
yield([{n: 'test', t: '1234', s: '5678'}])
end
end.new

OpenTelemetry::Exporter::OTLP::Exporter.stub(:new, otlp_exporter) do
subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer, processor: processor)

subject.traces_timer.block.call
end

refute_nil exported_spans, "OTLP exporter should have received spans"
assert exported_spans.is_a?(Array), "Exported spans should be an array"
assert_equal 1, exported_spans.length, "Should export 1 converted span"
otlp_exporter.verify
refute_nil discovery.value, "Discovery should remain valid after successful export"
ensure
ENV.delete('INSTANA_OTLP_ENABLED')
end

def test_otlp_export_disabled_without_env_variable
ENV.delete('INSTANA_OTLP_ENABLED')

stub_request(:post, "http://10.10.10.10:9292/com.instana.plugin.ruby.1234")
.to_return(status: 200)

stub_request(:post, "http://10.10.10.10:9292/com.instana.plugin.ruby/traces.1234")
.to_return(status: 200)

client = Instana::Backend::RequestClient.new('10.10.10.10', 9292)
discovery = Concurrent::Atom.new({'pid' => 1234})

processor = Class.new do
def send
yield([{n: 'test'}])
end
end.new

# Should not create OTLP exporter
subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer, processor: processor)

assert_nil subject.instance_variable_get(:@otlp_exporter), "OTLP exporter should not be initialized without env variable"

subject.traces_timer.block.call
refute_nil discovery.value, "Discovery should remain valid"
end

def test_otlp_export_converts_spans_correctly
ENV['INSTANA_OTLP_ENABLED'] = 'true'

stub_request(:post, "http://10.10.10.10:9292/com.instana.plugin.ruby.1234")
.to_return(status: 200)

client = Instana::Backend::RequestClient.new('10.10.10.10', 9292)
discovery = Concurrent::Atom.new({'pid' => 1234})

test_span = {
n: 'rack',
t: '1234567890abcdef',
s: 'fedcba0987654321',
ts: Time.now.to_i * 1000,
d: 100,
k: 1,
data: {
http: {
method: 'GET',
url: 'http://example.com/test',
status: 200
}
}
}

exported_spans = nil
otlp_exporter = Minitest::Mock.new
otlp_exporter.expect(:export, OpenTelemetry::SDK::Trace::Export::SUCCESS) do |spans|
exported_spans = spans
OpenTelemetry::SDK::Trace::Export::SUCCESS
end

processor = Class.new do
attr_reader :test_span

def initialize(span)
@test_span = span
end

def send
yield([@test_span])
end
end.new(test_span)

OpenTelemetry::Exporter::OTLP::Exporter.stub(:new, otlp_exporter) do
subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer, processor: processor)

subject.traces_timer.block.call
end

refute_nil exported_spans, "Should export converted spans"
assert exported_spans.is_a?(Array), "Exported spans should be an array"
assert_equal 1, exported_spans.length, "Should export 1 converted span"

# The converter returns OpenTelemetry::SDK::Trace::SpanData
# Just verify we got a converted span object
refute_nil exported_spans.first, "Converted span should not be nil"

otlp_exporter.verify
ensure
ENV.delete('INSTANA_OTLP_ENABLED')
end

def test_otlp_export_failure_triggers_rediscovery
ENV['INSTANA_OTLP_ENABLED'] = 'true'

stub_request(:post, "http://10.10.10.10:9292/com.instana.plugin.ruby.1234")
.to_return(status: 200)

stub_request(:get, "http://127.0.0.1:42699/")
.to_return(status: 200)
stub_request(:put, "http://127.0.0.1:42699/com.instana.plugin.ruby.discovery")
.to_return(status: 200, body: '{"pid": 1234}')
stub_request(:head, "http://127.0.0.1:42699/com.instana.plugin.ruby.1234")
.to_return(status: 200)

client = Instana::Backend::RequestClient.new('10.10.10.10', 9292)
discovery = Concurrent::Atom.new({'pid' => 1234})

otlp_exporter = Minitest::Mock.new
# Return FAILURE status code
otlp_exporter.expect(:export, OpenTelemetry::SDK::Trace::Export::FAILURE) do |_spans|
OpenTelemetry::SDK::Trace::Export::FAILURE
end

processor = Class.new do
def send
yield([{n: 'test'}])
end
end.new

OpenTelemetry::Exporter::OTLP::Exporter.stub(:new, otlp_exporter) do
subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer, processor: processor)

subject.traces_timer.block.call
end

otlp_exporter.verify
assert_nil discovery.value, "Discovery should be reset after export failure"
ensure
ENV.delete('INSTANA_OTLP_ENABLED')
end

def test_otlp_export_with_multiple_spans
ENV['INSTANA_OTLP_ENABLED'] = 'true'

stub_request(:post, "http://10.10.10.10:9292/com.instana.plugin.ruby.1234")
.to_return(status: 200)

client = Instana::Backend::RequestClient.new('10.10.10.10', 9292)
discovery = Concurrent::Atom.new({'pid' => 1234})

test_spans = [
{n: 'rack', t: '1111', s: '2222'},
{n: 'activerecord', t: '1111', s: '3333', p: '2222'},
{n: 'redis', t: '1111', s: '4444', p: '2222'}
]

exported_spans = nil
otlp_exporter = Minitest::Mock.new
otlp_exporter.expect(:export, OpenTelemetry::SDK::Trace::Export::SUCCESS) do |spans|
exported_spans = spans
OpenTelemetry::SDK::Trace::Export::SUCCESS
end

processor = Class.new do
attr_reader :test_spans

def initialize(spans)
@test_spans = spans
end

def send
yield(@test_spans)
end
end.new(test_spans)

OpenTelemetry::Exporter::OTLP::Exporter.stub(:new, otlp_exporter) do
subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer, processor: processor)

subject.traces_timer.block.call
end

refute_nil exported_spans, "Should export converted spans"
assert_equal 3, exported_spans.length, "Should export all 3 converted spans"
otlp_exporter.verify
ensure
ENV.delete('INSTANA_OTLP_ENABLED')
end

def test_otlp_exporter_initialization_with_env_variable
ENV['INSTANA_OTLP_ENABLED'] = 'true'

client = Instana::Backend::RequestClient.new('10.10.10.10', 9292)
discovery = Concurrent::Atom.new(nil)

subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer)

refute_nil subject.instance_variable_get(:@otlp_exporter), "OTLP exporter should be initialized when env variable is set"
ensure
ENV.delete('INSTANA_OTLP_ENABLED')
end

def test_otlp_export_handles_empty_span_batch
ENV['INSTANA_OTLP_ENABLED'] = 'true'

stub_request(:post, "http://10.10.10.10:9292/com.instana.plugin.ruby.1234")
.to_return(status: 200)

client = Instana::Backend::RequestClient.new('10.10.10.10', 9292)
discovery = Concurrent::Atom.new({'pid' => 1234})

otlp_exporter = Minitest::Mock.new
# Should not be called for empty batch

processor = Class.new do
def send
yield([])
end
end.new

OpenTelemetry::Exporter::OTLP::Exporter.stub(:new, otlp_exporter) do
subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer, processor: processor)

subject.traces_timer.block.call
end

# Discovery should remain valid even with empty batch
refute_nil discovery.value, "Discovery should remain valid with empty span batch"
ensure
ENV.delete('INSTANA_OTLP_ENABLED')
end
end
Loading