From 936a991aebb6f54e0f7ed040ea1540d863eb5680 Mon Sep 17 00:00:00 2001 From: milkisbad Date: Mon, 25 May 2026 16:56:51 +0200 Subject: [PATCH 1/5] feat(mcp): respect TRACELOOP_TRACE_CONTENT=false in standard MCP instrumentor The standard MCP instrumentor was unconditionally recording span input/output attributes, unlike the FastMCP instrumentor which already honored the TRACELOOP_TRACE_CONTENT env var. Guards all content attributes (TRACELOOP_ENTITY_INPUT, TRACELOOP_ENTITY_OUTPUT, MCP_RESPONSE_VALUE) behind the same _should_send_prompts() check, enabling workflows that cannot store prompts/responses to opt out. Closes #4188. --- .../instrumentation/mcp/instrumentation.py | 75 ++++++++------- .../tests/test_trace_content.py | 95 +++++++++++++++++++ 2 files changed, 137 insertions(+), 33 deletions(-) create mode 100644 packages/opentelemetry-instrumentation-mcp/tests/test_trace_content.py diff --git a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py index 688cdb2c7f..f35245039d 100644 --- a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py +++ b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py @@ -3,6 +3,7 @@ from typing import Any, AsyncGenerator, Callable, Collection, Tuple, Union, cast import json import logging +import os from opentelemetry import context, propagate from opentelemetry.instrumentation.instrumentor import BaseInstrumentor @@ -23,6 +24,10 @@ _instruments = ("mcp >= 1.6.0",) +def _should_send_prompts() -> bool: + return (os.getenv("TRACELOOP_TRACE_CONTENT") or "true").lower() == "true" + + class McpInstrumentor(BaseInstrumentor): def __init__(self, exception_logger=None): super().__init__() @@ -290,16 +295,17 @@ async def _handle_tool_call(self, tracer, method, params, args, kwargs, wrapped) span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_NAME, entity_name) # Add input - clean_input = self._extract_clean_input(method, params) - if clean_input: - try: - span.set_attribute( - SpanAttributes.TRACELOOP_ENTITY_INPUT, json.dumps(clean_input) - ) - except (TypeError, ValueError): - span.set_attribute( - SpanAttributes.TRACELOOP_ENTITY_INPUT, str(clean_input) - ) + if _should_send_prompts(): + clean_input = self._extract_clean_input(method, params) + if clean_input: + try: + span.set_attribute( + SpanAttributes.TRACELOOP_ENTITY_INPUT, json.dumps(clean_input) + ) + except (TypeError, ValueError): + span.set_attribute( + SpanAttributes.TRACELOOP_ENTITY_INPUT, str(clean_input) + ) return await self._execute_and_handle_result( span, method, args, kwargs, wrapped, clean_output=True @@ -308,9 +314,10 @@ async def _handle_tool_call(self, tracer, method, params, args, kwargs, wrapped) async def _handle_mcp_method(self, tracer, method, args, kwargs, wrapped): """Handle non-tool MCP methods with simple serialization""" with tracer.start_as_current_span(f"{method}.mcp") as span: - span.set_attribute( - SpanAttributes.TRACELOOP_ENTITY_INPUT, f"{serialize(args[0])}" - ) + if _should_send_prompts(): + span.set_attribute( + SpanAttributes.TRACELOOP_ENTITY_INPUT, f"{serialize(args[0])}" + ) return await self._execute_and_handle_result( span, method, args, kwargs, wrapped, clean_output=False ) @@ -322,23 +329,24 @@ async def _execute_and_handle_result( try: result = await wrapped(*args, **kwargs) # Add output - if clean_output: - clean_output_data = self._extract_clean_output(method, result) - if clean_output_data: - try: - span.set_attribute( - SpanAttributes.TRACELOOP_ENTITY_OUTPUT, - json.dumps(clean_output_data), - ) - except (TypeError, ValueError): - span.set_attribute( - SpanAttributes.TRACELOOP_ENTITY_OUTPUT, - str(clean_output_data), - ) - else: - span.set_attribute( - SpanAttributes.TRACELOOP_ENTITY_OUTPUT, serialize(result) - ) + if _should_send_prompts(): + if clean_output: + clean_output_data = self._extract_clean_output(method, result) + if clean_output_data: + try: + span.set_attribute( + SpanAttributes.TRACELOOP_ENTITY_OUTPUT, + json.dumps(clean_output_data), + ) + except (TypeError, ValueError): + span.set_attribute( + SpanAttributes.TRACELOOP_ENTITY_OUTPUT, + str(clean_output_data), + ) + else: + span.set_attribute( + SpanAttributes.TRACELOOP_ENTITY_OUTPUT, serialize(result) + ) # Handle errors if hasattr(result, "isError") and result.isError: span.set_attribute(ERROR_TYPE, "tool_error") @@ -565,9 +573,10 @@ async def send(self, item: Any) -> Any: with self._tracer.start_as_current_span("ResponseStreamWriter") as span: if hasattr(request, "result"): - span.set_attribute( - SpanAttributes.MCP_RESPONSE_VALUE, f"{serialize(request.result)}" - ) + if _should_send_prompts(): + span.set_attribute( + SpanAttributes.MCP_RESPONSE_VALUE, f"{serialize(request.result)}" + ) if "isError" in request.result: if request.result["isError"] is True: span.set_status( diff --git a/packages/opentelemetry-instrumentation-mcp/tests/test_trace_content.py b/packages/opentelemetry-instrumentation-mcp/tests/test_trace_content.py new file mode 100644 index 0000000000..f69b8e33b7 --- /dev/null +++ b/packages/opentelemetry-instrumentation-mcp/tests/test_trace_content.py @@ -0,0 +1,95 @@ +"""Tests for TRACELOOP_TRACE_CONTENT env var in MCP and FastMCP instrumentors.""" +import pytest + + +async def test_trace_content_false_suppresses_tool_spans( + span_exporter, tracer_provider, monkeypatch +) -> None: + """With TRACELOOP_TRACE_CONTENT=false, no input/output on any tool span.""" + monkeypatch.setenv("TRACELOOP_TRACE_CONTENT", "false") + + from fastmcp import FastMCP, Client + + server = FastMCP("no-content-server") + + @server.tool() + async def echo(message: str) -> str: + return f"Echo: {message}" + + async with Client(server) as client: + await client.call_tool("echo", {"message": "secret"}) + + spans = span_exporter.get_finished_spans() + tool_spans = [s for s in spans if s.name == "echo.tool"] + assert len(tool_spans) >= 1, f"Expected echo.tool spans, got: {[s.name for s in spans]}" + + for span in tool_spans: + assert "traceloop.entity.input" not in span.attributes, ( + f"Input must be absent when TRACELOOP_TRACE_CONTENT=false, got: {span.attributes}" + ) + assert "traceloop.entity.output" not in span.attributes, ( + f"Output must be absent when TRACELOOP_TRACE_CONTENT=false, got: {span.attributes}" + ) + # Span identity attributes must still be present + assert span.attributes.get("traceloop.span.kind") == "tool" + assert span.attributes.get("traceloop.entity.name") == "echo" + + +async def test_trace_content_false_suppresses_mcp_method_spans( + span_exporter, tracer_provider, monkeypatch +) -> None: + """With TRACELOOP_TRACE_CONTENT=false, no input/output on non-tool MCP method spans.""" + monkeypatch.setenv("TRACELOOP_TRACE_CONTENT", "false") + + from fastmcp import FastMCP, Client + + server = FastMCP("no-content-mcp-server") + + @server.tool() + async def dummy() -> str: + return "ok" + + async with Client(server) as client: + await client.list_tools() + + spans = span_exporter.get_finished_spans() + mcp_spans = [s for s in spans if s.name.endswith(".mcp")] + assert len(mcp_spans) >= 1, f"Expected .mcp spans, got: {[s.name for s in spans]}" + + for span in mcp_spans: + assert "traceloop.entity.input" not in span.attributes, ( + f"Input must be absent on {span.name} when TRACELOOP_TRACE_CONTENT=false" + ) + assert "traceloop.entity.output" not in span.attributes, ( + f"Output must be absent on {span.name} when TRACELOOP_TRACE_CONTENT=false" + ) + + +async def test_trace_content_true_includes_tool_input_output( + span_exporter, tracer_provider, monkeypatch +) -> None: + """With TRACELOOP_TRACE_CONTENT=true (default), input/output are present on tool spans.""" + monkeypatch.setenv("TRACELOOP_TRACE_CONTENT", "true") + + from fastmcp import FastMCP, Client + + server = FastMCP("with-content-server") + + @server.tool() + async def greet(name: str) -> str: + return f"Hello, {name}!" + + async with Client(server) as client: + await client.call_tool("greet", {"name": "world"}) + + spans = span_exporter.get_finished_spans() + tool_spans = [s for s in spans if s.name == "greet.tool"] + assert len(tool_spans) >= 1 + + for span in tool_spans: + assert "traceloop.entity.input" in span.attributes, ( + "Input must be present when TRACELOOP_TRACE_CONTENT=true" + ) + assert "traceloop.entity.output" in span.attributes, ( + "Output must be present when TRACELOOP_TRACE_CONTENT=true" + ) From ed254dc5e0bc3cebe24bfe605ccf28ec420816ea Mon Sep 17 00:00:00 2001 From: milkisbad Date: Mon, 25 May 2026 17:17:30 +0200 Subject: [PATCH 2/5] add docstrings --- .../opentelemetry/instrumentation/mcp/instrumentation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py index f35245039d..8b5bab9e34 100644 --- a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py +++ b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py @@ -25,6 +25,7 @@ def _should_send_prompts() -> bool: + """Check if content tracing is enabled (matches traceloop SDK)""" return (os.getenv("TRACELOOP_TRACE_CONTENT") or "true").lower() == "true" From d888241f295fe86e59312b956dde345d42833b1c Mon Sep 17 00:00:00 2001 From: milkisbad Date: Tue, 9 Jun 2026 18:13:05 +0200 Subject: [PATCH 3/5] move should_send_prompts to utils. Keep fastmcp_instrumentation the same --- .../mcp/fastmcp_instrumentation.py | 9 +++------ .../instrumentation/mcp/instrumentation.py | 19 ++++++++----------- .../instrumentation/mcp/utils.py | 5 +++++ 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py index 64fdb86024..da4a668199 100644 --- a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py +++ b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py @@ -9,7 +9,7 @@ from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from wrapt import register_post_import_hook, wrap_function_wrapper -from .utils import dont_throw +from .utils import dont_throw, should_send_prompts class FastMCPInstrumentor: @@ -153,11 +153,8 @@ async def traced_method(wrapped, instance, args, kwargs): return traced_method - def _should_send_prompts(self): - """Check if content tracing is enabled (matches traceloop SDK)""" - return ( - os.getenv("TRACELOOP_TRACE_CONTENT") or "true" - ).lower() == "true" + def _should_send_prompts(self) -> bool: + return should_send_prompts() def _get_json_encoder(self): """Get JSON encoder class (simplified - traceloop SDK uses custom JSONEncoder)""" diff --git a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py index 8b5bab9e34..74f46f6956 100644 --- a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py +++ b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py @@ -3,7 +3,6 @@ from typing import Any, AsyncGenerator, Callable, Collection, Tuple, Union, cast import json import logging -import os from opentelemetry import context, propagate from opentelemetry.instrumentation.instrumentor import BaseInstrumentor @@ -16,7 +15,7 @@ from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.instrumentation.mcp.version import __version__ -from opentelemetry.instrumentation.mcp.utils import dont_throw, Config +from opentelemetry.instrumentation.mcp.utils import dont_throw, Config, should_send_prompts from opentelemetry.instrumentation.mcp.fastmcp_instrumentation import ( FastMCPInstrumentor, ) @@ -24,11 +23,6 @@ _instruments = ("mcp >= 1.6.0",) -def _should_send_prompts() -> bool: - """Check if content tracing is enabled (matches traceloop SDK)""" - return (os.getenv("TRACELOOP_TRACE_CONTENT") or "true").lower() == "true" - - class McpInstrumentor(BaseInstrumentor): def __init__(self, exception_logger=None): super().__init__() @@ -124,6 +118,9 @@ def _uninstrument(self, **kwargs): unwrap("mcp.server.stdio", "stdio_server") self._fastmcp_instrumentor.uninstrument() + def _should_send_prompts(self) -> bool: + return should_send_prompts() + def _transport_wrapper(self, tracer): @asynccontextmanager async def traced_method( @@ -296,7 +293,7 @@ async def _handle_tool_call(self, tracer, method, params, args, kwargs, wrapped) span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_NAME, entity_name) # Add input - if _should_send_prompts(): + if self._should_send_prompts(): clean_input = self._extract_clean_input(method, params) if clean_input: try: @@ -315,7 +312,7 @@ async def _handle_tool_call(self, tracer, method, params, args, kwargs, wrapped) async def _handle_mcp_method(self, tracer, method, args, kwargs, wrapped): """Handle non-tool MCP methods with simple serialization""" with tracer.start_as_current_span(f"{method}.mcp") as span: - if _should_send_prompts(): + if self._should_send_prompts(): span.set_attribute( SpanAttributes.TRACELOOP_ENTITY_INPUT, f"{serialize(args[0])}" ) @@ -330,7 +327,7 @@ async def _execute_and_handle_result( try: result = await wrapped(*args, **kwargs) # Add output - if _should_send_prompts(): + if self._should_send_prompts(): if clean_output: clean_output_data = self._extract_clean_output(method, result) if clean_output_data: @@ -574,7 +571,7 @@ async def send(self, item: Any) -> Any: with self._tracer.start_as_current_span("ResponseStreamWriter") as span: if hasattr(request, "result"): - if _should_send_prompts(): + if should_send_prompts(): span.set_attribute( SpanAttributes.MCP_RESPONSE_VALUE, f"{serialize(request.result)}" ) diff --git a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/utils.py b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/utils.py index d4a80e58dc..9eeb697782 100644 --- a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/utils.py +++ b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/utils.py @@ -2,6 +2,7 @@ import asyncio import logging +import os import traceback @@ -9,6 +10,10 @@ class Config: exception_logger = None +def should_send_prompts() -> bool: + return (os.getenv("TRACELOOP_TRACE_CONTENT") or "true").lower() == "true" + + def dont_throw(func): """ A decorator that wraps the passed in function and logs exceptions instead of throwing them. From 77adec59a82bd894d78586a2351ac59052a92530 Mon Sep 17 00:00:00 2001 From: milkisbad Date: Tue, 16 Jun 2026 11:38:23 +0200 Subject: [PATCH 4/5] handle errors case Co-Authored-By: Claude Opus 4.8 --- .../mcp/fastmcp_instrumentation.py | 37 +++++++++--- .../instrumentation/mcp/instrumentation.py | 59 +++++++++++++++---- .../instrumentation/mcp/utils.py | 6 ++ .../tests/test_trace_content.py | 57 +++++++++++++++++- 4 files changed, 138 insertions(+), 21 deletions(-) diff --git a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py index da4a668199..96554b58c1 100644 --- a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py +++ b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py @@ -9,7 +9,7 @@ from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from wrapt import register_post_import_hook, wrap_function_wrapper -from .utils import dont_throw, should_send_prompts +from .utils import dont_throw, should_send_prompts, SUPPRESSED_ERROR_DESCRIPTION class FastMCPInstrumentor: @@ -81,8 +81,15 @@ async def traced_method(wrapped, instance, args, kwargs): entity_name = tool_key if tool_key else "unknown_tool" - # Create parent server.mcp span - with self._tracer.start_as_current_span("mcp.server") as mcp_span: + # Create parent server.mcp span. Disable the SDK's automatic + # exception recording/status so that, when content tracing is off, + # the exception message and stacktrace (which can echo tool content) + # are not leaked onto the span - we set status/record explicitly below. + with self._tracer.start_as_current_span( + "mcp.server", + record_exception=False, + set_status_on_exception=False, + ) as mcp_span: mcp_span.set_attribute(SpanAttributes.TRACELOOP_SPAN_KIND, "server") mcp_span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_NAME, "mcp.server") if self._server_name: @@ -90,7 +97,11 @@ async def traced_method(wrapped, instance, args, kwargs): # Create nested tool span span_name = f"{entity_name}.tool" - with self._tracer.start_as_current_span(span_name) as tool_span: + with self._tracer.start_as_current_span( + span_name, + record_exception=False, + set_status_on_exception=False, + ) as tool_span: tool_span.set_attribute(SpanAttributes.TRACELOOP_SPAN_KIND, TraceloopSpanKindValues.TOOL.value) tool_span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_NAME, entity_name) if self._server_name: @@ -111,13 +122,21 @@ async def traced_method(wrapped, instance, args, kwargs): try: result = await wrapped(*args, **kwargs) except Exception as e: + # The exception message/stacktrace can echo tool content + # (the tool's error text or arguments), so only record it + # when content tracing is enabled. The ERROR status and + # error type are kept either way so failures stay visible. tool_span.set_attribute(ERROR_TYPE, type(e).__name__) - tool_span.record_exception(e) - tool_span.set_status(Status(StatusCode.ERROR, str(e))) - mcp_span.set_attribute(ERROR_TYPE, type(e).__name__) - mcp_span.record_exception(e) - mcp_span.set_status(Status(StatusCode.ERROR, str(e))) + if self._should_send_prompts(): + tool_span.record_exception(e) + tool_span.set_status(Status(StatusCode.ERROR, str(e))) + mcp_span.record_exception(e) + mcp_span.set_status(Status(StatusCode.ERROR, str(e))) + else: + suppressed = f"{type(e).__name__} {SUPPRESSED_ERROR_DESCRIPTION}" + tool_span.set_status(Status(StatusCode.ERROR, suppressed)) + mcp_span.set_status(Status(StatusCode.ERROR, suppressed)) raise try: diff --git a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py index 74f46f6956..8384e3fcff 100644 --- a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py +++ b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py @@ -15,7 +15,12 @@ from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.instrumentation.mcp.version import __version__ -from opentelemetry.instrumentation.mcp.utils import dont_throw, Config, should_send_prompts +from opentelemetry.instrumentation.mcp.utils import ( + dont_throw, + Config, + should_send_prompts, + SUPPRESSED_ERROR_DESCRIPTION, +) from opentelemetry.instrumentation.mcp.fastmcp_instrumentation import ( FastMCPInstrumentor, ) @@ -285,7 +290,15 @@ async def _handle_tool_call(self, tracer, method, params, args, kwargs, wrapped) except Exception: pass - with tracer.start_as_current_span(span_name) as span: + # Disable the SDK's automatic exception recording/status so that, when + # content tracing is off, the exception message and stacktrace (which can + # echo tool content) are not leaked onto the span on re-raise - we set + # status/record explicitly in _execute_and_handle_result. + with tracer.start_as_current_span( + span_name, + record_exception=False, + set_status_on_exception=False, + ) as span: # Set tool-specific attributes span.set_attribute( SpanAttributes.TRACELOOP_SPAN_KIND, TraceloopSpanKindValues.TOOL.value @@ -311,7 +324,13 @@ async def _handle_tool_call(self, tracer, method, params, args, kwargs, wrapped) async def _handle_mcp_method(self, tracer, method, args, kwargs, wrapped): """Handle non-tool MCP methods with simple serialization""" - with tracer.start_as_current_span(f"{method}.mcp") as span: + # See _handle_tool_call: disable the SDK's automatic exception + # recording/status so suppressed error content is not re-leaked on re-raise. + with tracer.start_as_current_span( + f"{method}.mcp", + record_exception=False, + set_status_on_exception=False, + ) as span: if self._should_send_prompts(): span.set_attribute( SpanAttributes.TRACELOOP_ENTITY_INPUT, f"{serialize(args[0])}" @@ -348,17 +367,30 @@ async def _execute_and_handle_result( # Handle errors if hasattr(result, "isError") and result.isError: span.set_attribute(ERROR_TYPE, "tool_error") - if len(result.content) > 0: + if self._should_send_prompts() and len(result.content) > 0: span.set_status( Status(StatusCode.ERROR, f"{result.content[0].text}") ) + else: + span.set_status( + Status(StatusCode.ERROR, SUPPRESSED_ERROR_DESCRIPTION) + ) else: span.set_status(Status(StatusCode.OK)) return result except Exception as e: span.set_attribute(ERROR_TYPE, type(e).__name__) - span.record_exception(e) - span.set_status(Status(StatusCode.ERROR, str(e))) + # The exception message/stacktrace can echo tool content (e.g. the + # tool's error text or arguments), so only record it when content + # tracing is enabled. The ERROR status and error type are kept either + # way so failures remain visible. + if self._should_send_prompts(): + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) + else: + span.set_status( + Status(StatusCode.ERROR, f"{type(e).__name__} {SUPPRESSED_ERROR_DESCRIPTION}") + ) raise def _extract_clean_input(self, method: str, params: Any) -> dict: @@ -577,12 +609,17 @@ async def send(self, item: Any) -> Any: ) if "isError" in request.result: if request.result["isError"] is True: - span.set_status( - Status( - StatusCode.ERROR, - f"{request.result['content'][0]['text']}", + if should_send_prompts(): + span.set_status( + Status( + StatusCode.ERROR, + f"{request.result['content'][0]['text']}", + ) + ) + else: + span.set_status( + Status(StatusCode.ERROR, SUPPRESSED_ERROR_DESCRIPTION) ) - ) if hasattr(request, "id"): span.set_attribute(SpanAttributes.MCP_REQUEST_ID, f"{request.id}") diff --git a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/utils.py b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/utils.py index 9eeb697782..190805ad16 100644 --- a/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/utils.py +++ b/packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/utils.py @@ -10,6 +10,12 @@ class Config: exception_logger = None +# Generic, content-free status description used when TRACELOOP_TRACE_CONTENT is +# disabled, so error spans still convey that a call failed without leaking the +# tool's error message or arguments. +SUPPRESSED_ERROR_DESCRIPTION = "error (content suppressed by TRACELOOP_TRACE_CONTENT)" + + def should_send_prompts() -> bool: return (os.getenv("TRACELOOP_TRACE_CONTENT") or "true").lower() == "true" diff --git a/packages/opentelemetry-instrumentation-mcp/tests/test_trace_content.py b/packages/opentelemetry-instrumentation-mcp/tests/test_trace_content.py index f69b8e33b7..a28b4d25f8 100644 --- a/packages/opentelemetry-instrumentation-mcp/tests/test_trace_content.py +++ b/packages/opentelemetry-instrumentation-mcp/tests/test_trace_content.py @@ -1,5 +1,4 @@ """Tests for TRACELOOP_TRACE_CONTENT env var in MCP and FastMCP instrumentors.""" -import pytest async def test_trace_content_false_suppresses_tool_spans( @@ -93,3 +92,59 @@ async def greet(name: str) -> str: assert "traceloop.entity.output" in span.attributes, ( "Output must be present when TRACELOOP_TRACE_CONTENT=true" ) + +async def test_trace_content_false_suppresses_error_response_text( + span_exporter, tracer_provider, monkeypatch +) -> None: + """With TRACELOOP_TRACE_CONTENT=false, error response text must not leak into + the span status description or recorded exception events, but the span must + still be marked ERROR.""" + monkeypatch.setenv("TRACELOOP_TRACE_CONTENT", "false") + + from fastmcp import FastMCP, Client + + secret_ticker = "SPCX" + secret = f"super-secret-error-detail. {secret_ticker} is not valid ticker. Did you mean SPCE?" + + server = FastMCP("error-no-content-server") + + @server.tool() + async def fail_tool(ticker: str) -> str: + raise ValueError(secret) + + try: + async with Client(server) as client: + await client.call_tool("fail_tool", {"ticker": secret_ticker}) + except Exception: + pass # client re-raises the tool error — expected + + spans = span_exporter.get_finished_spans() + + from opentelemetry.trace.status import StatusCode + + error_spans = [s for s in spans if s.status.status_code == StatusCode.ERROR] + assert len(error_spans) >= 1, ( + f"Expected at least one ERROR span, got: {[s.name for s in spans]}" + ) + + # secret_ticker is both the call argument and part of the tool's error + # message, so asserting its absence covers both leak vectors: arguments + # leaking from the call and the return/error value leaking from the tool. + for span in spans: + # It must not leak into the status description... + description = span.status.description or "" + assert secret_ticker not in description, ( + f"Error response text leaked into status description of '{span.name}' " + f"when TRACELOOP_TRACE_CONTENT=false: {description!r}" + ) + + # ...nor into any recorded exception event (message/stacktrace). The SDK's + # automatic exception recording would otherwise re-leak the content even + # though the explicit set_status was suppressed. + for event in span.events: + for attr_key, attr_value in event.attributes.items(): + assert secret_ticker not in str(attr_value), ( + f"Error content leaked into event '{event.name}' " + f"attribute '{attr_key}' of span '{span.name}' " + f"when TRACELOOP_TRACE_CONTENT=false: {attr_value!r}" + ) From 1c958c855a9a273e4987ae3a1d74de87357d4edd Mon Sep 17 00:00:00 2001 From: milkisbad Date: Sun, 21 Jun 2026 15:55:39 +0200 Subject: [PATCH 5/5] add explicite info on TRACELOOP_TRACE_CONTENT to readme, update tests also probe non obfuscation of mcp calls metadata --- .../README.md | 18 +++++ .../tests/test_trace_content.py | 25 +++++++ .../opentelemetry-instrumentation-mcp/uv.lock | 70 +++++++++---------- 3 files changed, 78 insertions(+), 35 deletions(-) diff --git a/packages/opentelemetry-instrumentation-mcp/README.md b/packages/opentelemetry-instrumentation-mcp/README.md index 84b7d74684..921185010d 100644 --- a/packages/opentelemetry-instrumentation-mcp/README.md +++ b/packages/opentelemetry-instrumentation-mcp/README.md @@ -31,3 +31,21 @@ To disable logging, set the `TRACELOOP_TRACE_CONTENT` environment variable to `f ```bash TRACELOOP_TRACE_CONTENT=false ``` + +### What is and isn't gated + +The `TRACELOOP_TRACE_CONTENT` flag only gates **content**. Operational **metadata** is +always traced so that spans remain useful for monitoring even when content capture is off. + +| Always traced (metadata) | Gated behind `TRACELOOP_TRACE_CONTENT=true` (content) | +| --- | --- | +| Method / tool name (span name and `traceloop.entity.name`) | Tool arguments / prompt (`traceloop.entity.input`) | +| Span kind (`traceloop.span.kind`) | Tool / method result (`traceloop.entity.output`) | +| Request id (`mcp.request.id`) | Serialized response payload (`mcp.response.value`) | +| Duration (span start/end) | Error message text (span status description, recorded exception) | +| Status code (`OK` / `ERROR`) | | +| Error class (`error.type`) | | + +When content is disabled, a failed call still produces an `ERROR` span carrying its +`error.type`, but the status description is replaced with a generic, content-free message so +that the tool's error text and arguments cannot leak through it. diff --git a/packages/opentelemetry-instrumentation-mcp/tests/test_trace_content.py b/packages/opentelemetry-instrumentation-mcp/tests/test_trace_content.py index a28b4d25f8..3e139b0862 100644 --- a/packages/opentelemetry-instrumentation-mcp/tests/test_trace_content.py +++ b/packages/opentelemetry-instrumentation-mcp/tests/test_trace_content.py @@ -55,6 +55,8 @@ async def dummy() -> str: mcp_spans = [s for s in spans if s.name.endswith(".mcp")] assert len(mcp_spans) >= 1, f"Expected .mcp spans, got: {[s.name for s in spans]}" + from opentelemetry.trace.status import StatusCode + for span in mcp_spans: assert "traceloop.entity.input" not in span.attributes, ( f"Input must be absent on {span.name} when TRACELOOP_TRACE_CONTENT=false" @@ -62,6 +64,21 @@ async def dummy() -> str: assert "traceloop.entity.output" not in span.attributes, ( f"Output must be absent on {span.name} when TRACELOOP_TRACE_CONTENT=false" ) + # Metadata must survive content suppression: the method name is encoded + # in the span name, and the call's success status is still recorded. + method = span.name[: -len(".mcp")] + assert method, ( + f"Method name metadata must be preserved in span name, got: {span.name!r}" + ) + assert span.status.status_code == StatusCode.OK, ( + f"Status metadata must be preserved on {span.name}, " + f"got: {span.status.status_code}" + ) + + # The specific method invoked must be among the traced metadata. + assert any(s.name == "tools/list.mcp" for s in mcp_spans), ( + f"Expected a tools/list.mcp span, got: {[s.name for s in mcp_spans]}" + ) async def test_trace_content_true_includes_tool_input_output( @@ -127,6 +144,14 @@ async def fail_tool(ticker: str) -> str: f"Expected at least one ERROR span, got: {[s.name for s in spans]}" ) + # Error metadata must survive content suppression: even though the message + # text is redacted, the error class is still recorded so failures remain + # diagnosable by type. + assert all(s.attributes.get("error.type") for s in error_spans), ( + "error.type metadata must be preserved on ERROR spans when " + f"TRACELOOP_TRACE_CONTENT=false, got: {[dict(s.attributes) for s in error_spans]}" + ) + # secret_ticker is both the call argument and part of the tool's error # message, so asserting its absence covers both leak vectors: arguments # leaking from the call and the return/error value leaking from the tool. diff --git a/packages/opentelemetry-instrumentation-mcp/uv.lock b/packages/opentelemetry-instrumentation-mcp/uv.lock index 6a682f5566..9ae27fd170 100644 --- a/packages/opentelemetry-instrumentation-mcp/uv.lock +++ b/packages/opentelemetry-instrumentation-mcp/uv.lock @@ -2,7 +2,8 @@ version = 1 revision = 3 requires-python = ">=3.10, <4" resolution-markers = [ - "python_full_version >= '3.13'", + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", "python_full_version < '3.13'", ] @@ -641,7 +642,7 @@ name = "importlib-metadata" version = "8.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ @@ -905,45 +906,44 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.39.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, ] [[package]] name = "opentelemetry-exporter-otlp" -version = "1.39.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/9c/3ab1db90f32da200dba332658f2bbe602369e3d19f6aba394031a42635be/opentelemetry_exporter_otlp-1.39.1.tar.gz", hash = "sha256:7cf7470e9fd0060c8a38a23e4f695ac686c06a48ad97f8d4867bc9b420180b9c", size = 6147, upload-time = "2025-12-11T13:32:40.309Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/94/8637919a5d01f81dacf510234bc0110b944f4687a6e96b0a02adf2f6bdce/opentelemetry_exporter_otlp-1.42.1.tar.gz", hash = "sha256:2d9ebaed714377a67d224d46795ddcc11d2c877fa5de35fda70b6f3b010729a9", size = 6086, upload-time = "2026-05-21T16:32:51.963Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/6c/bdc82a066e6fb1dcf9e8cc8d4e026358fe0f8690700cc6369a6bf9bd17a7/opentelemetry_exporter_otlp-1.39.1-py3-none-any.whl", hash = "sha256:68ae69775291f04f000eb4b698ff16ff685fdebe5cb52871bc4e87938a7b00fe", size = 7019, upload-time = "2025-12-11T13:32:19.387Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4d/c26080295a36fd22e201fefd7cb9c22cd203189b1af8cd73b158382b7ad8/opentelemetry_exporter_otlp-1.42.1-py3-none-any.whl", hash = "sha256:aedd54545bb0587cd45210abdc8be545af9c01413f3307786e276df1e3c83bee", size = 6733, upload-time = "2026-05-21T16:32:31.261Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.39.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.39.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -954,14 +954,14 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/48/b329fed2c610c2c32c9366d9dc597202c9d1e58e631c137ba15248d8850f/opentelemetry_exporter_otlp_proto_grpc-1.39.1.tar.gz", hash = "sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad", size = 24650, upload-time = "2025-12-11T13:32:41.429Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/87/ca7fc790dfdbcf4f9e9aab14a39ef1b7508ead13707e283de0b3131478d2/opentelemetry_exporter_otlp_proto_grpc-1.42.1.tar.gz", hash = "sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15", size = 27140, upload-time = "2026-05-21T16:32:56.162Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/a3/cc9b66575bd6597b98b886a2067eea2693408d2d5f39dad9ab7fc264f5f3/opentelemetry_exporter_otlp_proto_grpc-1.39.1-py3-none-any.whl", hash = "sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18", size = 19766, upload-time = "2025-12-11T13:32:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/89/2b/28ba5b128f47fe8c3bab541000d6feb4b5a9bd26623ca013406f01c0fb60/opentelemetry_exporter_otlp_proto_grpc-1.42.1-py3-none-any.whl", hash = "sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc", size = 19617, upload-time = "2026-05-21T16:32:34.278Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.39.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -972,28 +972,28 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, + { url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" }, ] [[package]] name = "opentelemetry-exporter-prometheus" -version = "0.60b1" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, { name = "prometheus-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/14/39/7dafa6fff210737267bed35a8855b6ac7399b9e582b8cf1f25f842517012/opentelemetry_exporter_prometheus-0.60b1.tar.gz", hash = "sha256:a4011b46906323f71724649d301b4dc188aaa068852e814f4df38cc76eac616b", size = 14976, upload-time = "2025-12-11T13:32:42.944Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/2a/dfeddff262b12eff0c72f4ad9e258aab8889f48c4dc1417a0377a13bc427/opentelemetry_exporter_prometheus-0.63b1.tar.gz", hash = "sha256:31902e22c89431058a95b6dcdb644f9309f226aa4872cc755f0a780d2895e97f", size = 15234, upload-time = "2026-05-21T16:32:57.797Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/0d/4be6bf5477a3eb3d917d2f17d3c0b6720cd6cb97898444a61d43cc983f5c/opentelemetry_exporter_prometheus-0.60b1-py3-none-any.whl", hash = "sha256:49f59178de4f4590e3cef0b8b95cf6e071aae70e1f060566df5546fad773b8fd", size = 13019, upload-time = "2025-12-11T13:32:23.974Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ec/d7c7435e9000fb69837cf7753b7cbbbdeb5d0585203daf1b6ebf8fa93e02/opentelemetry_exporter_prometheus-0.63b1-py3-none-any.whl", hash = "sha256:0efd00aa6b1939345ddcc6de141b83ebffa2b4401a37a68f880e54217602701d", size = 12466, upload-time = "2026-05-21T16:32:36.622Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.60b1" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1001,14 +1001,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", size = 41081, upload-time = "2026-05-21T16:36:14.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, + { url = "https://files.pythonhosted.org/packages/35/a1/9314e621c143e4d82a5bf7a43c2ff7a745d31023506336857607c8c543cc/opentelemetry_instrumentation-0.63b1-py3-none-any.whl", hash = "sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c", size = 35577, upload-time = "2026-05-21T16:34:56.818Z" }, ] [[package]] name = "opentelemetry-instrumentation-mcp" -version = "0.60.0" +version = "0.61.0" source = { editable = "." } dependencies = [ { name = "opentelemetry-api" }, @@ -1046,7 +1046,7 @@ requires-dist = [ { name = "mcp", marker = "extra == 'instruments'" }, { name = "opentelemetry-api", specifier = ">=1.38.0,<2" }, { name = "opentelemetry-instrumentation", specifier = ">=0.59b0" }, - { name = "opentelemetry-semantic-conventions", specifier = ">=0.59b0" }, + { name = "opentelemetry-semantic-conventions", specifier = ">=0.63b1" }, { name = "opentelemetry-semantic-conventions-ai", specifier = ">=0.5.1,<0.6.0" }, ] provides-extras = ["instruments"] @@ -1072,41 +1072,41 @@ test = [ [[package]] name = "opentelemetry-proto" -version = "1.39.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, + { url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.39.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.60b1" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, ] [[package]]