-
Notifications
You must be signed in to change notification settings - Fork 77
fix(python): support public tracer providers #859
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,7 @@ | |
|
|
||
| import logging | ||
| import os | ||
| from typing import List, Optional, Sequence | ||
| from typing import List, Optional, Protocol, Sequence, cast | ||
|
|
||
| from opentelemetry import trace | ||
| from opentelemetry.sdk.trace import TracerProvider, SpanProcessor | ||
|
|
@@ -25,6 +25,10 @@ | |
| _initialized = False | ||
|
|
||
|
|
||
| class _SpanProcessorProvider(Protocol): | ||
| def add_span_processor(self, span_processor: SpanProcessor) -> None: ... | ||
|
|
||
|
|
||
| def setup_scenario_tracing( | ||
| *, | ||
| span_filter: Optional[SpanFilter] = None, | ||
|
|
@@ -88,7 +92,7 @@ def ensure_tracing_initialized(observability: Optional[dict] = None) -> None: | |
| _initialized = True | ||
|
|
||
|
|
||
| def _get_concrete_provider(provider) -> Optional[TracerProvider]: | ||
| def _get_concrete_provider(provider) -> Optional[_SpanProcessorProvider]: | ||
| """Returns the concrete TracerProvider if one exists. | ||
|
|
||
| Checks the provider itself and one level of delegation | ||
|
|
@@ -97,6 +101,12 @@ def _get_concrete_provider(provider) -> Optional[TracerProvider]: | |
| if isinstance(provider, TracerProvider): | ||
| return provider | ||
|
|
||
| # OpenTelemetry's public provider interface does not require an SDK | ||
| # TracerProvider subclass. Providers such as Temporal's replay-safe wrapper | ||
| # expose the processor hook directly and are safe to configure in place. | ||
| if callable(getattr(provider, "add_span_processor", None)): | ||
| return cast(_SpanProcessorProvider, provider) | ||
|
|
||
|
Comment on lines
+104
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate files =="
git ls-files | rg 'python/scenario/_tracing/setup\.py|python/tests/test_tracing_setup\.py|pyproject\.toml|requirements|poetry.lock|uv.lock' || true
echo "== setup relevant lines =="
wc -l python/scenario/_tracing/setup.py python/tests/test_tracing_setup.py
sed -n '1,170p' python/scenario/_tracing/setup.py
sed -n '80,170p' python/tests/test_tracing_setup.py
echo "== package opentelemetry deps =="
rg -n "opentelemetry|otel" python pyproject.toml Pipfile requirements.txt uv.lock poetry.lock 2>/dev/null || true
echo "== source references for get_tracers/add_span_processor =="
rg -n "get_tracer|_get_concrete_provider|add_span_processor|_do_setup|tracer_provider" python/scenario/_tracing/setup.py python/tests/test_tracing_setup.pyRepository: langwatch/scenario Length of output: 39749 🌐 Web query:
💡 Result: In the OpenTelemetry Python SDK, the TracerProvider.get_tracer method is used to acquire a Tracer instance [1][2]. The argument often referred to as the "instrumentation source" is the instrumenting_module_name [1][3][2]. The get_tracer method signature is as follows [1][4]: get_tracer(instrumenting_module_name, instrumenting_library_version=None, schema_url=None, attributes=None) Key details regarding this argument include: 1. Identification: The instrumenting_module_name acts as a unique identifier for the instrumentation scope [1][2]. It should represent the library, package, module, or class that is performing the instrumentation [1][3]. 2. Naming Convention: It is explicitly recommended to use a fixed string—such as the full name of the instrumentation package—rather than using the dynamic name variable, as name can vary depending on which file the tracer is initialized in [1][4][2]. 3. Instrumentation vs. Instrumented: A critical distinction is that this name should identify the code doing the instrumentation, not the library being instrumented [1][2][5]. For example, when instrumenting the "requests" library, one should use "opentelemetry.instrumentation.requests" rather than just "requests" [1][3][2]. 4. Convenience Wrapper: The opentelemetry.trace.get_tracer function serves as a convenience wrapper for the TracerProvider.get_tracer method [1][2]. If a TracerProvider is not explicitly provided, it defaults to the currently configured global TracerProvider [1][4][2]. These identifiers (name and version) are crucial for telemetry processing, allowing users to understand the source of the data, filter signals, and debug instrumentation issues [6][5][7]. Citations:
🌐 Web query:
💡 Result: In OpenTelemetry Python, the Citations:
🌐 Web query:
💡 Result: In OpenTelemetry Python, the expression tracer_provider.get_tracer refers to the method used to retrieve a tracer instance from a specific TracerProvider object [1]. While users often access the global tracer provider via trace.get_tracer_provider [2][3], libraries and instrumentations frequently accept an optional tracer_provider argument [4][5]. When this argument is provided, the instrumentation uses that specific TracerProvider instance to create its tracer via the get_tracer method, falling back to the global provider only if one is not explicitly provided [4][5][6]. The standard signature for this method is typically: tracer = tracer_provider.get_tracer( instrumenting_module_name, instrumenting_library_version=None, schema_url=None) This mechanism ensures that instrumented code can be configured with a specific provider (which may have unique resource attributes or span processors) rather than relying solely on the global state [4][5]. Citations:
🌐 Web query:
💡 Result: In the OpenTelemetry Python instrumentation for the requests library, the components you mentioned—get_tracer, tracer_provider, and Instrumentor._instrument—are part of the internal implementation used to set up and manage telemetry collection [1][2]. 1. tracer_provider: This is an optional argument passed to the instrumentation methods [3][4]. It allows you to specify a custom TracerProvider if you do not wish to use the global default provider [3][4]. 2. get_tracer: Inside the instrumentation package, this function is called to retrieve a tracer instance [1][2]. It typically uses the provided tracer_provider (or the global default) along with library-specific information (like name and version) to initialize a tracer that records spans for outgoing HTTP requests [1][2]. 3. Instrumentor._instrument: The RequestsInstrumentor class inherits from BaseInstrumentor [3][4]. The _instrument method is an internal implementation method that you generally do not call directly [5]. When you call.instrument on the RequestsInstrumentor instance, it internally calls this _instrument method, passing along configuration options like the tracer_provider [4][5]. This method then proceeds to perform the actual monkey-patching of the requests library to enable tracing [4][6]. In typical usage, you do not need to interact with these internal methods. Instead, you use the public API: from opentelemetry.instrumentation.requests import RequestsInstrumentor # Automatically instruments 'requests' using global providers RequestsInstrumentor.instrument # Or, optionally pass a custom TracerProvider RequestsInstrumentor.instrument(tracer_provider=my_custom_tracer_provider) Citations:
🌐 Web query:
💡 Result: In OpenTelemetry Python instrumentation, the BaseInstrumentor class serves as an abstract base class for creating instrumentation libraries [1][2]. While BaseInstrumentor itself provides methods for instrumenting and uninstrumenting libraries [2], the acquisition of a tracer is handled by the OpenTelemetry API's tracer provider mechanisms [3][4]. To acquire a tracer within an instrumentation, you use the TracerProvider's get_tracer method [3][4]. The recommended approach involves the following steps: 1. Tracer Acquisition: Use opentelemetry.trace.get_tracer. This is a convenience wrapper that delegates to the configured TracerProvider [3][4]. 2. Tracer Provider Handling: While you can call trace.get_tracer directly (which defaults to the global tracer provider), many custom instrumentors are designed to accept an optional tracer_provider as a keyword argument (e.g., in their init or instrument method) [5][6]. If a tracer_provider is provided, it should be used to create the tracer; otherwise, the global tracer provider is used [3][7][4]. 3. Scope Identification: When calling get_tracer, you must provide a unique identifier for the instrumentation scope (instrumenting_module_name), typically the name of the package or module performing the instrumentation [3][4]. It is best practice to use a fixed string rather than name, as the latter can result in inconsistent tracer names if the instrumentation spans multiple files [4][8]. Example usage pattern: from opentelemetry import trace # Inside your instrumentor tracer = trace.get_tracer( instrumenting_module_name="my.instrumentation.package", instrumenting_library_version="1.0.0", tracer_provider=self.tracer_provider # If optional provider was passed to the instrumentor) This pattern ensures that your instrumentation can be easily integrated into different environments, including those that require custom tracer providers rather than the global default [7][5][6]. Citations:
🌐 Web query:
💡 Result: In the context of the OpenTelemetry Python instrumentation libraries, tracer_provider and get_tracer are used to manage how spans are created and reported [1][2]. The standard pattern for OpenTelemetry instrumentations involves using a tracer provider to create tracer instances, which are then used to instrument library code [1][2][3]. Key aspects of this pattern include: 1. Tracer Provider Usage: Instrumentations are designed to accept an optional tracer_provider argument [1][2][3]. If a tracer provider is provided, the instrumentation uses it to create its tracer; if it is not provided, the instrumentation falls back to the global tracer provider [1][2][3]. This is important because different tracer providers can have different configurations, such as distinct Resource attributes [1][2]. 2. Implementation in instrumentor.py: The instrumentor.py file, located in the opentelemetry-instrumentation package, defines the BaseInstrumentor class [4]. While the base class provides the structure for instrumentation lifecycle management (such as instrument and uninstrument methods) [5][4], the actual tracer initialization typically occurs within the specific instrumentation packages (e.g., opentelemetry-instrumentation-fastapi or opentelemetry-instrumentation-aws-lambda) [6][3]. 3. Creating the Tracer: The get_tracer function from the opentelemetry.trace API is used to instantiate the tracer [3]. It is commonly called as follows: tracer = get_tracer(name, version, tracer_provider=tracer_provider, schema_url=...) [3] This ensures that the instrumentation is properly associated with the correct tracer provider and semantic convention schema [3]. Citations:
🌐 Web query:
💡 Result: In the OpenTelemetry Python Requests instrumentation library, the tracer_provider is an optional keyword argument that can be passed to the instrument method of the RequestsInstrumentor class [1][2]. When invoking RequestsInstrumentor.instrument, you can provide a custom TracerProvider instance to control where spans are sent [1][3]. If no tracer_provider is specified, the instrumentation defaults to using the global TracerProvider (configured via trace.set_tracer_provider) [4][2]. Example usage: from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.sdk.trace import TracerProvider # Create a custom tracer provider provider = TracerProvider # Pass the provider to the instrumentor RequestsInstrumentor.instrument(tracer_provider=provider) The tracer_provider argument allows developers to use specific, isolated tracing configurations for their requests, rather than relying on the globally registered provider [4][2]. Internal implementations of the instrumentation use this provider when initializing the tracer via get_tracer [5][2]. Citations:
Require the full tracer-provider contract before instrumenting.
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| # Check delegation pattern | ||
| delegate = None | ||
| if hasattr(provider, "get_delegate"): | ||
|
|
@@ -122,16 +132,17 @@ def _do_setup( | |
| concrete = _get_concrete_provider(existing_provider) | ||
|
|
||
| if concrete is not None: | ||
| _attach_to_existing(concrete, span_filter, span_processors, trace_exporter) | ||
| _attach_to_existing(concrete, span_filter, span_processors, trace_exporter, instrumentors) | ||
| else: | ||
| _full_setup(span_filter, span_processors, trace_exporter, instrumentors) | ||
|
|
||
|
|
||
| def _attach_to_existing( | ||
| provider: TracerProvider, | ||
| provider: _SpanProcessorProvider, | ||
| span_filter: Optional[SpanFilter], | ||
| span_processors: Optional[List[SpanProcessor]], | ||
| trace_exporter: Optional[SpanExporter], | ||
| instrumentors: Optional[Sequence], | ||
| ) -> None: | ||
| """Attach processors to an existing TracerProvider.""" | ||
| provider.add_span_processor(judge_span_collector) | ||
|
|
@@ -150,6 +161,10 @@ def _attach_to_existing( | |
| # Add LangWatch exporter | ||
| _add_langwatch_exporter(provider, span_filter) | ||
|
|
||
| if instrumentors: | ||
| for instrumentor in instrumentors: | ||
| instrumentor.instrument(tracer_provider=provider) | ||
|
|
||
|
|
||
| def _full_setup( | ||
| span_filter: Optional[SpanFilter], | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Complete the new Python annotations.
python/scenario/_tracing/setup.py#L95-L95: annotateprovider.python/scenario/_tracing/setup.py#L140-L145: parameterizeSequencewith the instrumentor type.python/tests/test_tracing_setup.py#L98-L99: annotate the firstspan_processorparameter.python/tests/test_tracing_setup.py#L146-L147: annotate the secondspan_processorparameter.As per coding guidelines, Python functions under
python/**/*.pymust use explicit, specific type annotations.📍 Affects 2 files
python/scenario/_tracing/setup.py#L95-L95(this comment)python/scenario/_tracing/setup.py#L140-L145python/tests/test_tracing_setup.py#L98-L99python/tests/test_tracing_setup.py#L146-L147🤖 Prompt for AI Agents
Source: Coding guidelines