Skip to content

Commit 869388b

Browse files
authored
feat: add HTTP OTLP trace exporter support for auto instrumentation
Co-authored-by: hope <hope.leong@sap.com>
1 parent fd8aeb5 commit 869388b

4 files changed

Lines changed: 52 additions & 12 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies = [
1616
"pydantic~=2.12.3",
1717
"hatchling~=1.27.0",
1818
"opentelemetry-exporter-otlp-proto-grpc~=1.38.0",
19+
"opentelemetry-exporter-otlp-proto-http~=1.38.0",
1920
"traceloop-sdk~=0.52.0"
2021
]
2122

src/sap_cloud_sdk/core/telemetry/auto_instrument.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import logging
22
import os
33

4-
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
4+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
5+
OTLPSpanExporter as GRPCSpanExporter,
6+
)
7+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
8+
OTLPSpanExporter as HTTPSpanExporter,
9+
)
510
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
611
from traceloop.sdk import Traceloop
712

@@ -41,8 +46,19 @@ def auto_instrument():
4146
else:
4247
if "v1/traces" not in otel_endpoint:
4348
otel_endpoint = otel_endpoint.rstrip("/") + "/v1/traces"
44-
logger.info(f"Initializing auto instrumentation with endpoint: {otel_endpoint}")
45-
base_exporter = OTLPSpanExporter(endpoint=otel_endpoint)
49+
protocol = os.getenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc").lower()
50+
exporters = {"grpc": GRPCSpanExporter, "http/protobuf": HTTPSpanExporter}
51+
if protocol not in exporters:
52+
raise ValueError(
53+
f"Unsupported OTEL_EXPORTER_OTLP_PROTOCOL: '{protocol}'. "
54+
"Supported values are 'grpc' and 'http/protobuf'."
55+
)
56+
57+
logger.info(
58+
f"Initializing auto instrumentation with endpoint: {otel_endpoint} "
59+
f"(protocol: {protocol})"
60+
)
61+
base_exporter = exporters[protocol](endpoint=otel_endpoint)
4662

4763
exporter = GenAIAttributeTransformer(base_exporter)
4864

tests/core/unit/telemetry/test_auto_instrument.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ def mock_traceloop_components():
1313
with ExitStack() as stack:
1414
mocks = {
1515
'traceloop': stack.enter_context(patch('sap_cloud_sdk.core.telemetry.auto_instrument.Traceloop')),
16-
'exporter': stack.enter_context(patch('sap_cloud_sdk.core.telemetry.auto_instrument.OTLPSpanExporter')),
16+
'grpc_exporter': stack.enter_context(patch('sap_cloud_sdk.core.telemetry.auto_instrument.GRPCSpanExporter')),
17+
'http_exporter': stack.enter_context(patch('sap_cloud_sdk.core.telemetry.auto_instrument.HTTPSpanExporter')),
1718
'console_exporter': stack.enter_context(patch('sap_cloud_sdk.core.telemetry.auto_instrument.ConsoleSpanExporter')),
1819
'transformer': stack.enter_context(patch('sap_cloud_sdk.core.telemetry.auto_instrument.GenAIAttributeTransformer')),
1920
'create_resource': stack.enter_context(patch('sap_cloud_sdk.core.telemetry.auto_instrument.create_resource_attributes_from_env')),
@@ -48,8 +49,8 @@ def test_auto_instrument_appends_v1_traces_to_endpoint(self, mock_traceloop_comp
4849
with patch.dict('os.environ', {'OTEL_EXPORTER_OTLP_ENDPOINT': 'http://localhost:4317'}, clear=True):
4950
auto_instrument()
5051

51-
# Verify exporter was called with /v1/traces appended
52-
mock_traceloop_components['exporter'].assert_called_once_with(
52+
# Verify exporter was called with /v1/traces appended (grpc by default)
53+
mock_traceloop_components['grpc_exporter'].assert_called_once_with(
5354
endpoint='http://localhost:4317/v1/traces'
5455
)
5556

@@ -61,8 +62,8 @@ def test_auto_instrument_preserves_existing_v1_traces(self, mock_traceloop_compo
6162
with patch.dict('os.environ', {'OTEL_EXPORTER_OTLP_ENDPOINT': 'http://localhost:4317/v1/traces'}, clear=True):
6263
auto_instrument()
6364

64-
# Verify exporter was called with original endpoint
65-
mock_traceloop_components['exporter'].assert_called_once_with(
65+
# Verify exporter was called with original endpoint (grpc by default)
66+
mock_traceloop_components['grpc_exporter'].assert_called_once_with(
6667
endpoint='http://localhost:4317/v1/traces'
6768
)
6869

@@ -110,11 +111,29 @@ def test_auto_instrument_with_trailing_slash(self, mock_traceloop_components):
110111
with patch.dict('os.environ', {'OTEL_EXPORTER_OTLP_ENDPOINT': 'http://localhost:4317/'}, clear=True):
111112
auto_instrument()
112113

113-
# Verify trailing slash is removed before appending /v1/traces
114-
mock_traceloop_components['exporter'].assert_called_once_with(
114+
# Verify trailing slash is removed before appending /v1/traces (grpc by default)
115+
mock_traceloop_components['grpc_exporter'].assert_called_once_with(
115116
endpoint='http://localhost:4317/v1/traces'
116117
)
117118

119+
def test_auto_instrument_with_http_protobuf_protocol(self, mock_traceloop_components):
120+
"""Test that auto_instrument uses HTTP exporter when OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf."""
121+
mock_traceloop_components['get_app_name'].return_value = 'test-app'
122+
mock_traceloop_components['create_resource'].return_value = {}
123+
124+
with patch.dict('os.environ', {
125+
'OTEL_EXPORTER_OTLP_ENDPOINT': 'http://localhost:4318',
126+
'OTEL_EXPORTER_OTLP_PROTOCOL': 'http/protobuf'
127+
}, clear=True):
128+
auto_instrument()
129+
130+
# Verify HTTP exporter was called with /v1/traces appended
131+
mock_traceloop_components['http_exporter'].assert_called_once_with(
132+
endpoint='http://localhost:4318/v1/traces'
133+
)
134+
# Verify gRPC exporter was not called
135+
mock_traceloop_components['grpc_exporter'].assert_not_called()
136+
118137
def test_auto_instrument_passes_transformer_to_traceloop(self, mock_traceloop_components):
119138
"""Test that auto_instrument passes the GenAIAttributeTransformer as exporter to Traceloop."""
120139
mock_traceloop_components['get_app_name'].return_value = 'test-app'
@@ -155,7 +174,8 @@ def test_auto_instrument_with_console_exporter(self, mock_traceloop_components):
155174
auto_instrument()
156175

157176
mock_traceloop_components['console_exporter'].assert_called_once_with()
158-
mock_traceloop_components['exporter'].assert_not_called()
177+
mock_traceloop_components['grpc_exporter'].assert_not_called()
178+
mock_traceloop_components['http_exporter'].assert_not_called()
159179
mock_traceloop_components['traceloop'].init.assert_called_once()
160180

161181
def test_auto_instrument_console_exporter_case_insensitive(self, mock_traceloop_components):
@@ -182,7 +202,8 @@ def test_auto_instrument_console_wins_when_both_set(self, mock_traceloop_compone
182202
auto_instrument()
183203

184204
mock_traceloop_components['console_exporter'].assert_called_once_with()
185-
mock_traceloop_components['exporter'].assert_not_called()
205+
mock_traceloop_components['grpc_exporter'].assert_not_called()
206+
mock_traceloop_components['http_exporter'].assert_not_called()
186207

187208
def test_auto_instrument_console_wraps_with_transformer(self, mock_traceloop_components):
188209
"""Test that ConsoleSpanExporter is wrapped with GenAIAttributeTransformer."""

uv.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)