diff --git a/README.md b/README.md index 825d55f0..13ffd2a2 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,179 @@ Although the gem has no configuration required for out of the box metrics and tr This Ruby gem provides a simple API for tracing and also supports [OpenTracing](http://opentracing.io/). See the [Ruby Tracing SDK](https://www.ibm.com/docs/en/instana-observability/current?topic=ruby-tracing-sdk) and [OpenTracing](https://www.ibm.com/docs/en/instana-observability/current?topic=ruby-opentracing) pages for details. +## OTLP Export + +The `instana` gem supports exporting traces via the **OpenTelemetry Protocol (OTLP)** +directly to the Instana agent's OTLP receiver, bypassing the native agent trace format. +OTLP export is **opt-in** — the gem behaves identically to previous versions when +`INSTANA_TRACING_OTLP_ENABLED` is not set. + +### Overview + +When OTLP export is enabled: +- Spans are converted from Instana's internal format to OTLP protobuf +- The converted spans are sent via HTTP/protobuf to the agent's OTLP port (`4318`) +- The endpoint is auto-derived from the same host where the Instana agent was discovered +- The native agent reporting path (`/com.instana.plugin.ruby/traces`) is replaced by OTLP + +### Quick Start + +The only required change is setting one environment variable before starting your application: + +```bash +INSTANA_TRACING_OTLP_ENABLED=true bundle exec rails server +``` + +Expected log output (with `INSTANA_LOG_LEVEL=debug`): +``` +[Instana] Using OTLP Exporter to export result code: 0 +``` + +Result code `0` = `SUCCESS`. No other configuration is needed — the endpoint is +automatically set to `http://:4318/v1/traces`. + +### Gem Dependencies + +`opentelemetry-exporter-otlp` is a **runtime dependency** of the `instana` gem and +is installed automatically. No additional gems are required. + +> **Note:** gRPC (`OTEL_EXPORTER_OTLP_PROTOCOL=grpc`) is not supported. +> Only `http/protobuf` is supported and tested. + +### Environment Variables + +| Variable | Type | Default | Description | +|---|---|---|---| +| `INSTANA_TRACING_OTLP_ENABLED` | Boolean | `false` | Enables OTLP export. Valid values: `true`, `1`, `yes` | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | String (URL) | auto-derived from agent host | **Base** endpoint URL. `/v1/traces` is appended automatically. | +| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | String (URL) | `/v1/traces` | **Fully-qualified** trace endpoint. Used as-is; overrides `OTEL_EXPORTER_OTLP_ENDPOINT`. | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | String | `http/protobuf` | Transport protocol. Only `http/protobuf` is supported. | +| `OTEL_EXPORTER_OTLP_HEADERS` | String | — | Comma-separated `key=value` auth headers. e.g. `api-key=secret,x-tenant-id=myorg` | +| `OTEL_EXPORTER_OTLP_TIMEOUT` | Integer (ms) | `10000` | Maximum time to wait for export completion. | +| `OTEL_EXPORTER_OTLP_COMPRESSION` | String | — | Compression algorithm. Valid value: `gzip`. | +| `OTEL_EXPORTER_OTLP_CERTIFICATE` | String (path) | — | Path to CA certificate file for TLS verification. | +| `OTEL_EXPORTER_OTLP_CLIENT_KEY` | String (path) | — | Path to client private key file for mutual TLS. | +| `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` | String (path) | — | Path to client certificate file for mutual TLS. | +| `OTEL_EXPORTER_OTLP_INSECURE` | Boolean | `false` | Disable TLS certificate verification. | +| `OTEL_SEMCONV_STABILITY_OPT_IN` | String | `stable` | Semantic convention stability level. Valid values: `stable`, `development`. | + +> **Endpoint precedence:** `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` > `OTEL_EXPORTER_OTLP_ENDPOINT` > auto-derived from agent host + +### YAML Configuration + +When `INSTANA_CONFIG_PATH` points to a YAML file, OTLP settings are placed under +the `tracing.otlp` key. YAML configuration takes precedence over environment variables. + +**Minimal example** (`config/instana.yml`): +```yaml +tracing: + otlp: + enabled: true +``` + +**Full example:** +```yaml +tracing: + otlp: + enabled: true + endpoint: "http://localhost:4318/v1/traces" + protocol: "http/protobuf" + timeout: 10000 + compression: "gzip" + headers: + x-instana-key: "YOUR_API_KEY" + x-tenant-id: "YOUR_TENANT" + certificate: "/path/to/ca-cert.pem" + client_key: "/path/to/client-key.pem" + client_certificate: "/path/to/client-cert.pem" + insecure: false + semconv_stability: "stable" +``` + +Start the application: +```bash +INSTANA_CONFIG_PATH=config/instana.yml bundle exec rails server +``` + +### Configuration Precedence + +Settings are resolved in this order (highest wins): + +``` +INSTANA_CONFIG_PATH YAML → Environment variables → Agent config → Defaults +``` + +This means a YAML file always wins over env vars. Agent-provided config (pushed +via the Instana agent's discovery payload) is applied after env vars but can be +overridden by YAML or env vars set at startup. + +### TLS / mTLS + +**Server-side TLS** (verify the collector's certificate): +```bash +INSTANA_TRACING_OTLP_ENABLED=true \ +OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4318/v1/traces \ +OTEL_EXPORTER_OTLP_CERTIFICATE=/path/to/ca-cert.pem \ +bundle exec rails server +``` + +**Mutual TLS** (client certificate authentication): +```bash +INSTANA_TRACING_OTLP_ENABLED=true \ +OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4318/v1/traces \ +OTEL_EXPORTER_OTLP_CERTIFICATE=/path/to/ca-cert.pem \ +OTEL_EXPORTER_OTLP_CLIENT_KEY=/path/to/client-key.pem \ +OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE=/path/to/client-cert.pem \ +bundle exec rails server +``` + +### Troubleshooting + +**OTLP export is not happening — spans go via the native path** + +OTLP is disabled by default. Check that `INSTANA_TRACING_OTLP_ENABLED=true` is set +in the process environment before the gem initialises (i.e. before `require 'instana'` +or Rails boot). Verify with: +```bash +INSTANA_LOG_LEVEL=debug bundle exec rails server +# Look for: "Using OTLP Exporter" vs "Using Instana Native Exporter" +``` + +**`Failed to initialize OTLP exporter` in the log** + +The exporter could not be constructed. Common causes: +- `OTEL_EXPORTER_OTLP_CERTIFICATE` path does not exist or is not readable +- `OTEL_EXPORTER_OTLP_CLIENT_KEY` / `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` path invalid +- An unsupported protocol (e.g. `grpc`) was set — only `http/protobuf` is supported + +The gem falls back to `@otlp_exporter = nil` and logs an `ERROR` line with the +exception message. Check that message for the root cause. + +**`result code: 1` (FAILURE) — spans not arriving** + +The exporter initialised but the export request failed. Common causes: +- The Instana agent is not running or OTLP is not enabled on the agent side +- `OTEL_EXPORTER_OTLP_ENDPOINT` points to a wrong host or port +- `OTEL_EXPORTER_OTLP_ENDPOINT` was set to a base URL like `http://host:4318` + without `/v1/traces` — this was a known bug fixed in v2.9.0 (the sensor now + appends the path automatically) +- A firewall is blocking port `4318` + +Verify the agent OTLP port is reachable: +```bash +curl -s -o /dev/null -w "%{http_code}" \ + -X POST http://localhost:4318/v1/traces \ + -H "Content-Type: application/x-protobuf" +# 200 or 415 = port open; "connection refused" = agent OTLP not enabled +``` + +**`HTTP request path is empty` exception (pre-v2.9.0)** + +If you are on a version earlier than 2.9.0 and pass a base URL to +`OTEL_EXPORTER_OTLP_ENDPOINT` (without `/v1/traces`), the `opentelemetry-exporter-otlp` +gem crashes with `HTTP request path is empty`. Upgrade to v2.9.0 or later, or use +`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` with the full path instead. + ## Documentation You can find more documentation covering supported components and minimum versions in the Instana [documentation portal](https://www.ibm.com/docs/en/instana-observability/current?topic=technologies-monitoring-ruby). diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..56b639c5 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,137 @@ +# Instana Ruby Sensor — Examples + +This directory contains runnable example scripts for the `instana` gem. + +## Files + +| File | What it demonstrates | +|---|---| +| [`otel.rb`](otel.rb) | Tracing API: `in_span`, `start_span`, nested spans, error recording, tags | +| [`tracing.rb`](tracing.rb) | Legacy tracing API examples | +| [`otlp_http.rb`](otlp_http.rb) | OTLP export via HTTP/protobuf — minimal quickstart | +| [`otlp_with_headers.rb`](otlp_with_headers.rb) | OTLP export with authentication headers and TLS/mTLS | + +--- + +## Prerequisites + +- Ruby 3.2 or newer (tested on Ruby 4.0.0) +- `instana` gem installed (or Gemfile pointing to a local checkout) +- **Instana agent running locally** with OTLP enabled on port `4318` + +Verify the agent is reachable before running OTLP examples: + +```bash +curl -s http://localhost:42699/ +# Should return a JSON response + +curl -s -o /dev/null -w "%{http_code}" \ + -X POST http://localhost:4318/v1/traces \ + -H "Content-Type: application/x-protobuf" +# Expect 200 or 415 — not "connection refused" +``` + +--- + +## Running the examples + +### Tracing API (`otel.rb`) + +```bash +bundle exec ruby examples/otel.rb +``` + +No environment variables needed — uses the native agent reporting path. + +--- + +### OTLP HTTP/protobuf quickstart (`otlp_http.rb`) + +Sends 5 spans (basic, HTTP exit, database, nested, error) to the Instana +agent via OTLP HTTP/protobuf. The endpoint is auto-derived from the agent +host — no explicit endpoint configuration required. + +```bash +INSTANA_TRACING_OTLP_ENABLED=true \ +bundle exec ruby examples/otlp_http.rb +``` + +**Optional — explicit endpoint:** +```bash +INSTANA_TRACING_OTLP_ENABLED=true \ +OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces \ +bundle exec ruby examples/otlp_http.rb +``` + +> **Note on `OTEL_EXPORTER_OTLP_ENDPOINT` vs `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`:** +> - `OTEL_EXPORTER_OTLP_ENDPOINT` — base URL only (e.g. `http://localhost:4318`). +> The sensor appends `/v1/traces` automatically per the OTLP spec. +> - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` — fully-qualified URL including path. +> Used as-is; takes precedence over `OTEL_EXPORTER_OTLP_ENDPOINT`. + +**Expected log output:** +``` +Using OTLP Exporter to export result code: 0 +``` + +**Result code meanings:** +- `0` — `SUCCESS` — spans delivered +- `1` — `FAILURE` — connection error or endpoint unreachable +- `2` — `DROPPED` — batch rejected by the receiver + +--- + +### OTLP with headers and TLS (`otlp_with_headers.rb`) + +#### Headers only (no TLS) + +```bash +INSTANA_TRACING_OTLP_ENABLED=true \ +OTEL_EXPORTER_OTLP_HEADERS="x-instana-key=YOUR_API_KEY,x-tenant-id=YOUR_TENANT" \ +bundle exec ruby examples/otlp_with_headers.rb +``` + +Header format: comma-separated `key=value` pairs, no spaces around `=` or `,`. + +#### TLS — server certificate verification + +```bash +INSTANA_TRACING_OTLP_ENABLED=true \ +OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4318/v1/traces \ +OTEL_EXPORTER_OTLP_CERTIFICATE=/path/to/ca-cert.pem \ +bundle exec ruby examples/otlp_with_headers.rb +``` + +#### Mutual TLS (mTLS) + +```bash +INSTANA_TRACING_OTLP_ENABLED=true \ +OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4318/v1/traces \ +OTEL_EXPORTER_OTLP_CERTIFICATE=/path/to/ca-cert.pem \ +OTEL_EXPORTER_OTLP_CLIENT_KEY=/path/to/client-key.pem \ +OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE=/path/to/client-cert.pem \ +bundle exec ruby examples/otlp_with_headers.rb +``` + +#### Equivalent YAML config + +```bash +cat > /tmp/instana_otlp.yml << 'EOF' +tracing: + otlp: + enabled: true + endpoint: "https://your-collector:4318/v1/traces" + headers: + x-instana-key: "YOUR_API_KEY" + x-tenant-id: "YOUR_TENANT" + certificate: "/path/to/ca-cert.pem" + client_key: "/path/to/client-key.pem" + client_certificate: "/path/to/client-cert.pem" +EOF + +INSTANA_CONFIG_PATH=/tmp/instana_otlp.yml \ +bundle exec ruby examples/otlp_with_headers.rb +``` + +> **Note:** gRPC (`OTEL_EXPORTER_OTLP_PROTOCOL=grpc`) is not supported. +> Only `http/protobuf` is supported. diff --git a/examples/otel.rb b/examples/otel.rb index ab93a587..6f589ffd 100644 --- a/examples/otel.rb +++ b/examples/otel.rb @@ -1,8 +1,17 @@ -# Simple Instana Tracing Examples -# =========================== +# Simple Instana Tracing Examples — Tracing API +# ============================================== # (c) Copyright IBM Corp. 2025 +# This file demonstrates the Instana tracing API (in_span / start_span). +# It covers how to create spans, nest them, record errors, and set tags. +# +# For OTLP-specific setup (enabling export to the Instana agent via +# HTTP/protobuf), see: +# - examples/otlp_http.rb — HTTP/protobuf export (quickstart) +# - examples/otlp_with_headers.rb — authentication headers and TLS/mTLS +# - examples/README.md — prerequisites and how to run + ####################################### ## in_span Method Examples ####################################### @@ -94,5 +103,3 @@ def finish_work(span) child.finish parent.finish - -# Made with Bob diff --git a/examples/otlp_http.rb b/examples/otlp_http.rb new file mode 100644 index 00000000..8db6ae16 --- /dev/null +++ b/examples/otlp_http.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +# (c) Copyright IBM Corp. 2026 + +# OTLP HTTP/protobuf Export Example +# ================================== +# +# Demonstrates enabling Instana OTLP export using the HTTP/protobuf protocol. +# Spans are sent directly to the Instana agent's OTLP receiver on port 4318, +# bypassing the native agent trace format. +# +# Prerequisites: +# - Instana agent running locally with OTLP enabled on port 4318 +# - instana gem pointing to this repo (or installed) +# +# Run: +# INSTANA_TRACING_OTLP_ENABLED=true \ +# OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \ +# ruby examples/otlp_http.rb +# +# Optional — explicit endpoint (agent auto-discovers if omitted): +# OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces +# +# Expected output: +# [Instana] Using OTLP Exporter to export result code: 0 + +require 'instana' + +# --------------------------------------------------------------------------- +# Wait for the Instana agent to be ready before creating spans. +# +# Spans created while the agent is not ready are silently dropped — +# `Instana.tracer.start_span` returns a non-recording span when +# `Instana.agent.ready?` is false (discovery has not yet completed). +# +# In long-running servers (Rails, Rack) this is never an issue because +# the agent background thread finishes well before the first request +# arrives. In short-lived scripts the agent must be ready first. +# --------------------------------------------------------------------------- +deadline = Time.now + 15 +sleep(0.1) until Instana.agent.ready? || Time.now > deadline +abort('[otlp_http] Agent not ready after 15 s — is the Instana agent running?') unless Instana.agent.ready? + +puts "[otlp_http] Agent ready — sending 5 spans via OTLP" + +# --------------------------------------------------------------------------- +# 1. Basic span — minimal tracing, just a name +# --------------------------------------------------------------------------- +Instana.tracer.in_span('otlp.example.basic') do + # Simulate work + sleep(0.01) +end + +# --------------------------------------------------------------------------- +# 2. HTTP exit span — outbound HTTP call with standard attributes +# +# Uses SpanKind::CLIENT (kind 2) so the OTLP converter maps this to an +# http.client span with url.full, http.request.method, http.response.status_code. +# --------------------------------------------------------------------------- +Instana.tracer.in_span( + 'GET', + attributes: { + 'http.method' => 'GET', + 'http.url' => 'https://api.example.com/users/42', + 'http.status_code' => 200 + }, + kind: Instana::Trace::SpanKind::CLIENT +) do + # Actual Net::HTTP call would go here + sleep(0.01) +end + +# --------------------------------------------------------------------------- +# 3. Database span — SQL query with sanitised statement +# +# Matches the activerecord / sequel span type. The database converter maps +# db.statement, db.system, and server.address to OTLP semantic conventions. +# --------------------------------------------------------------------------- +Instana.tracer.in_span( + 'activerecord', + attributes: { + 'activerecord.sql' => 'SELECT "users".* FROM "users" WHERE "users"."id" = ?', + 'activerecord.adapter' => 'PostgreSQL' + } +) do + sleep(0.01) +end + +# --------------------------------------------------------------------------- +# 4. Nested spans — parent / child relationship +# +# The child span captures the parent_span_id from SpanContext, which the +# OTLP exporter serialises into the parent_span_id field of the protobuf. +# --------------------------------------------------------------------------- +Instana.tracer.in_span('otlp.example.parent') do + Instana.tracer.in_span('otlp.example.child') do + sleep(0.01) + end +end + +# --------------------------------------------------------------------------- +# 5. Error span — exception recorded as an OTLP span event +# +# When ec (error count) > 0, BaseConverter#build_error_events generates an +# "exception" span event with exception.message and exception.stacktrace. +# The span status is set to ERROR. +# --------------------------------------------------------------------------- +begin + Instana.tracer.in_span('otlp.example.error') do + raise StandardError, 'Something went wrong in the OTLP example' + end +rescue StandardError + # Error is captured inside the span; rescue here to allow script to continue +end + +# Allow the background timer to flush the span batch to the agent +sleep(10) + +puts 'otlp_http.rb complete — check agent/Instana UI for 5 spans' diff --git a/examples/otlp_with_headers.rb b/examples/otlp_with_headers.rb new file mode 100644 index 00000000..44e34ff8 --- /dev/null +++ b/examples/otlp_with_headers.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +# (c) Copyright IBM Corp. 2026 + +# OTLP Authenticated Export Example — Headers and TLS/mTLS +# ========================================================= +# +# Demonstrates configuring authentication headers and TLS certificates +# for OTLP export. Use this when your OTLP endpoint requires an API key, +# tenant header, or mutual TLS client certificates. +# +# Prerequisites: +# - Instana agent (or OTel Collector) running with TLS/auth enabled +# - instana gem pointing to this repo (or installed) +# +# --- +# Run with headers only (no TLS): +# +# INSTANA_TRACING_OTLP_ENABLED=true \ +# OTEL_EXPORTER_OTLP_HEADERS="x-instana-key=YOUR_API_KEY,x-tenant-id=YOUR_TENANT" \ +# ruby examples/otlp_with_headers.rb +# +# --- +# Run with TLS (server certificate verification): +# +# INSTANA_TRACING_OTLP_ENABLED=true \ +# OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4318/v1/traces \ +# OTEL_EXPORTER_OTLP_CERTIFICATE=/path/to/ca-cert.pem \ +# ruby examples/otlp_with_headers.rb +# +# --- +# Run with mutual TLS (mTLS — client cert + key): +# +# INSTANA_TRACING_OTLP_ENABLED=true \ +# OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4318/v1/traces \ +# OTEL_EXPORTER_OTLP_CERTIFICATE=/path/to/ca-cert.pem \ +# OTEL_EXPORTER_OTLP_CLIENT_KEY=/path/to/client-key.pem \ +# OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE=/path/to/client-cert.pem \ +# ruby examples/otlp_with_headers.rb +# +# --- +# Equivalent YAML config (INSTANA_CONFIG_PATH=/path/to/config.yml): +# +# tracing: +# otlp: +# enabled: true +# endpoint: "https://your-collector:4318/v1/traces" +# headers: +# x-instana-key: "YOUR_API_KEY" +# x-tenant-id: "YOUR_TENANT" +# certificate: "/path/to/ca-cert.pem" +# client_key: "/path/to/client-key.pem" +# client_certificate: "/path/to/client-cert.pem" +# +# --- +# Header format for OTEL_EXPORTER_OTLP_HEADERS: +# Comma-separated key=value pairs (no spaces around = or ,) +# e.g. "api-key=secret123,x-tenant-id=myorg,x-custom=value" + +require 'instana' + +# --------------------------------------------------------------------------- +# Wait for the Instana agent to be ready before creating spans. +# See otlp_http.rb for a full explanation of why this is needed in scripts. +# --------------------------------------------------------------------------- +deadline = Time.now + 15 +sleep(0.1) until Instana.agent.ready? || Time.now > deadline +abort('[otlp_with_headers] Agent not ready after 15 s — is the Instana agent running?') unless Instana.agent.ready? + +puts "[otlp_with_headers] Agent ready — sending 5 spans via OTLP" + +# --------------------------------------------------------------------------- +# Emit a representative set of spans to exercise the export path. +# The headers and certificates are set via env vars above; the span +# content here is identical to otlp_http.rb for easy comparison. +# --------------------------------------------------------------------------- + +# 1. Basic span +Instana.tracer.in_span('otlp.auth.basic') do + sleep(0.01) +end + +# 2. HTTP exit span +Instana.tracer.in_span( + 'GET', + attributes: { + 'http.method' => 'GET', + 'http.url' => 'https://api.example.com/profile', + 'http.status_code' => 200 + }, + kind: Instana::Trace::SpanKind::CLIENT +) do + sleep(0.01) +end + +# 3. Database span +Instana.tracer.in_span( + 'activerecord', + attributes: { + 'activerecord.sql' => 'SELECT "profiles".* FROM "profiles" WHERE "profiles"."id" = ?', + 'activerecord.adapter' => 'PostgreSQL' + } +) do + sleep(0.01) +end + +# 4. Nested spans +Instana.tracer.in_span('otlp.auth.parent') do + Instana.tracer.in_span('otlp.auth.child') do + sleep(0.01) + end +end + +# 5. Error span +begin + Instana.tracer.in_span('otlp.auth.error') do + raise StandardError, 'Auth example error' + end +rescue StandardError + # captured inside span +end + +sleep(2) + +puts 'otlp_with_headers.rb complete — check agent/Instana UI for 5 spans' diff --git a/lib/instana/backend/host_agent_reporting_observer.rb b/lib/instana/backend/host_agent_reporting_observer.rb index d1e4026e..4b63cb09 100644 --- a/lib/instana/backend/host_agent_reporting_observer.rb +++ b/lib/instana/backend/host_agent_reporting_observer.rb @@ -199,21 +199,32 @@ def initialize_otlp_exporter @otlp_exporter = nil end - # Derive the OTLP endpoint from the discovered agent host when no explicit - # endpoint has been configured (config_source == 'default'). OTLP_DEFAULT_PORT = 4318 OTLP_TRACES_PATH = '/v1/traces'.freeze + # Returns a fully-qualified OTLP traces endpoint. + # + # When config_source is 'default', builds the endpoint from the discovered + # agent host so OTLP traffic goes to the same host as metrics/traces. + # + # For user-supplied endpoints, appends '/v1/traces' when the URL has no + # path, because OTEL_EXPORTER_OTLP_ENDPOINT is a base URL and the + # opentelemetry-exporter-otlp gem only auto-appends the path when no + # endpoint is given at all. def resolve_otlp_endpoint(endpoint, config_source) - return endpoint unless config_source == 'default' + if config_source == 'default' + agent_host = @client&.host + return endpoint unless agent_host - # Use the host that was discovered by HostAgentLookup (same host the - # metrics/traces client is already talking to) and append the standard - # OTLP HTTP port and traces path. - agent_host = @client&.host - return endpoint unless agent_host + return "http://#{agent_host}:#{OTLP_DEFAULT_PORT}#{OTLP_TRACES_PATH}" + end - "http://#{agent_host}:#{OTLP_DEFAULT_PORT}#{OTLP_TRACES_PATH}" + # User-supplied: append the traces path if no path was given. + uri = URI.parse(endpoint) + uri.path = OTLP_TRACES_PATH if uri.path.empty? || uri.path == '/' + uri.to_s + rescue URI::InvalidURIError + endpoint end end end diff --git a/sonar-project.properties b/sonar-project.properties index 01961ca3..58fea52b 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -5,7 +5,8 @@ sonar.sources=lib sonar.tests=test sonar.test.inclusions=test/**/* sonar.ruby.coverage.reportPaths=coverage/coverage.json -sonar.coverage.exclusions=bin/*,examples/*,extras/*,lib/instana/version.rb +sonar.coverage.exclusions=.github/**/*,bin/**/*,examples/**/*,extras/**/*,lib/instana/version.rb +sonar.cpd.exclusions=examples/**/* sonar.links.homepage=https://github.com/instana/ruby-sensor/ sonar.links.ci=https://circleci.com/gh/instana/ruby-sensor sonar.links.issue=https://github.com/instana/ruby-sensor/issues diff --git a/test/backend/host_agent_reporting_observer_test.rb b/test/backend/host_agent_reporting_observer_test.rb index ddb3133e..39c98bf0 100644 --- a/test/backend/host_agent_reporting_observer_test.rb +++ b/test/backend/host_agent_reporting_observer_test.rb @@ -506,6 +506,86 @@ def test_otlp_exporter_omits_cert_keys_when_nil refute received_opts.key?(:client_key_file), 'client_key_file should not be set when nil' end + # OTEL_EXPORTER_OTLP_ENDPOINT is a base URL — the sensor must append + # '/v1/traces' before passing it to the exporter. + # Also covers the rescue URI::InvalidURIError branch: an unparseable + # endpoint must be passed through to the exporter unchanged. + def test_otlp_exporter_resolves_user_supplied_endpoint + client = Instana::Backend::RequestClient.new('10.10.10.10', 9292) + discovery = Concurrent::Atom.new(nil) + received_opts = nil + + # Base URL — path must be appended + with_otlp_config(enabled: true, endpoint: 'http://127.0.0.1:4318', config_source: 'env') do + capture = lambda { |**opts| + received_opts = opts + Minitest::Mock.new + } + OpenTelemetry::Exporter::OTLP::Exporter.stub(:new, capture) do + Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) + end + end + + assert_equal 'http://127.0.0.1:4318/v1/traces', received_opts[:endpoint], + 'Base URL without path must have /v1/traces appended' + + # Invalid URI — must be returned as-is (exercises rescue URI::InvalidURIError) + invalid_endpoint = "not a valid\turi %%" + with_otlp_config(enabled: true, endpoint: invalid_endpoint, config_source: 'env') do + capture = lambda { |**opts| + received_opts = opts + Minitest::Mock.new + } + OpenTelemetry::Exporter::OTLP::Exporter.stub(:new, capture) do + Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) + end + end + + assert_equal invalid_endpoint, received_opts[:endpoint], + 'Invalid URI must be passed through unchanged' + end + + # OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is already a fully-qualified URL + # and must be passed to the exporter unchanged. + # Also covers the config_source == 'default' + nil agent_host early-return: + # when the client has no host yet, the configured endpoint is used as-is. + def test_otlp_exporter_preserves_fully_qualified_traces_endpoint + client = Instana::Backend::RequestClient.new('10.10.10.10', 9292) + discovery = Concurrent::Atom.new(nil) + received_opts = nil + + # Fully-qualified user-supplied URL — must pass through unchanged + with_otlp_config(enabled: true, endpoint: 'http://127.0.0.1:4318/v1/traces', config_source: 'env') do + capture = lambda { |**opts| + received_opts = opts + Minitest::Mock.new + } + OpenTelemetry::Exporter::OTLP::Exporter.stub(:new, capture) do + Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) + end + end + + assert_equal 'http://127.0.0.1:4318/v1/traces', received_opts[:endpoint], + 'Fully-qualified URL must be passed through unchanged' + + # config_source 'default' with no agent host — exercises `return endpoint unless agent_host` + clientless = Instana::Backend::RequestClient.new('10.10.10.10', 9292) + clientless.define_singleton_method(:host) { nil } + fallback_endpoint = 'http://fallback:4318/v1/traces' + with_otlp_config(enabled: true, endpoint: fallback_endpoint, config_source: 'default') do + capture = lambda { |**opts| + received_opts = opts + Minitest::Mock.new + } + OpenTelemetry::Exporter::OTLP::Exporter.stub(:new, capture) do + Instana::Backend::HostAgentReportingObserver.new(clientless, discovery, timer_class: MockTimer) + end + end + + assert_equal fallback_endpoint, received_opts[:endpoint], + 'Endpoint must be returned unchanged when agent host is not yet known' + end + def test_otlp_export_enabled_exports_spans stub_request(:post, "http://10.10.10.10:9292/com.instana.plugin.ruby.1234") .to_return(status: 200)