nvidia-nat-opentelemetry 1.2.0a20250813__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,166 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import logging
17
+ from abc import abstractmethod
18
+ from importlib.metadata import PackageNotFoundError
19
+ from importlib.metadata import version
20
+
21
+ from opentelemetry.sdk.resources import Resource
22
+
23
+ from nat.builder.context import ContextState
24
+ from nat.data_models.span import Span
25
+ from nat.observability.exporter.span_exporter import SpanExporter
26
+ from nat.observability.processor.batching_processor import BatchingProcessor
27
+ from nat.observability.processor.processor import Processor
28
+ from nat.plugins.opentelemetry.otel_span import OtelSpan
29
+ from nat.plugins.opentelemetry.span_converter import convert_span_to_otel
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def get_opentelemetry_sdk_version() -> str:
35
+ """Get the OpenTelemetry SDK version dynamically.
36
+
37
+ Returns:
38
+ The version of the opentelemetry-sdk package, or 'unknown' if not found.
39
+ """
40
+ try:
41
+ return version("opentelemetry-sdk")
42
+ except PackageNotFoundError:
43
+ logger.warning("Could not determine opentelemetry-sdk version")
44
+ return "unknown"
45
+
46
+
47
+ class SpanToOtelProcessor(Processor[Span, OtelSpan]):
48
+ """Processor that converts a Span to an OtelSpan."""
49
+
50
+ async def process(self, item: Span) -> OtelSpan:
51
+ return convert_span_to_otel(item) # type: ignore
52
+
53
+
54
+ class OtelSpanBatchProcessor(BatchingProcessor[OtelSpan]):
55
+ """Processor that batches OtelSpans with explicit type information.
56
+
57
+ This class provides explicit type information for the TypeIntrospectionMixin
58
+ by overriding the type properties directly.
59
+ """
60
+ pass
61
+
62
+
63
+ class OtelSpanExporter(SpanExporter[Span, OtelSpan]): # pylint: disable=R0901
64
+ """Abstract base class for OpenTelemetry exporters.
65
+
66
+ This class provides a specialized implementation for OpenTelemetry exporters.
67
+ It builds upon SpanExporter's span construction logic and automatically adds
68
+ a SpanToOtelProcessor to transform Span objects into OtelSpan objects.
69
+
70
+ The processing flow is:
71
+ IntermediateStep → Span → OtelSpan → Export
72
+
73
+ Key Features:
74
+ - Automatic span construction from IntermediateStep events (via SpanExporter)
75
+ - Built-in Span to OtelSpan conversion (via SpanToOtelProcessor)
76
+ - Support for additional processing steps if needed
77
+ - Type-safe processing pipeline with enhanced TypeVar compatibility
78
+ - Batching support for efficient export
79
+
80
+ Inheritance Hierarchy:
81
+ - BaseExporter: Core functionality + TypeIntrospectionMixin
82
+ - ProcessingExporter: Processor pipeline support
83
+ - SpanExporter: Span creation and lifecycle management
84
+ - OtelExporter: OpenTelemetry-specific span transformation
85
+
86
+ Generic Types:
87
+ - InputSpanT: Always Span (from IntermediateStep conversion)
88
+ - OutputSpanT: Always OtelSpan (for OpenTelemetry compatibility)
89
+ """
90
+
91
+ def __init__(self,
92
+ context_state: ContextState | None = None,
93
+ batch_size: int = 100,
94
+ flush_interval: float = 5.0,
95
+ max_queue_size: int = 1000,
96
+ drop_on_overflow: bool = False,
97
+ shutdown_timeout: float = 10.0,
98
+ resource_attributes: dict[str, str] | None = None):
99
+ """Initialize the OpenTelemetry exporter.
100
+
101
+ Args:
102
+ context_state: The context state to use for the exporter.
103
+ batch_size: The batch size for exporting spans.
104
+ flush_interval: The flush interval in seconds for exporting spans.
105
+ max_queue_size: The maximum queue size for exporting spans.
106
+ drop_on_overflow: Whether to drop spans on overflow.
107
+ shutdown_timeout: The shutdown timeout in seconds.
108
+ resource_attributes: Additional resource attributes for spans.
109
+ """
110
+ super().__init__(context_state)
111
+
112
+ # Initialize resource for span attribution
113
+ if resource_attributes is None:
114
+ resource_attributes = {}
115
+ self._resource = Resource(attributes=resource_attributes)
116
+
117
+ self.add_processor(SpanToOtelProcessor())
118
+ self.add_processor(
119
+ OtelSpanBatchProcessor(batch_size=batch_size,
120
+ flush_interval=flush_interval,
121
+ max_queue_size=max_queue_size,
122
+ drop_on_overflow=drop_on_overflow,
123
+ shutdown_timeout=shutdown_timeout))
124
+
125
+ async def export_processed(self, item: OtelSpan | list[OtelSpan]) -> None:
126
+ """Export the processed span(s).
127
+
128
+ This method handles the common logic for all OTEL exporters:
129
+ - Normalizes single spans vs. batches
130
+ - Sets resource attributes on spans
131
+ - Delegates to the abstract export_otel_spans method
132
+
133
+ Args:
134
+ item (OtelSpan | list[OtelSpan]): The processed span(s) to export.
135
+ Can be a single span or a batch of spans from BatchingProcessor.
136
+ """
137
+ try:
138
+ if isinstance(item, OtelSpan):
139
+ spans = [item]
140
+ elif isinstance(item, list):
141
+ spans = item
142
+ else:
143
+ logger.warning("Unexpected item type: %s", type(item))
144
+ return
145
+
146
+ # Set resource attributes on all spans
147
+ for span in spans:
148
+ span.set_resource(self._resource)
149
+
150
+ # Delegate to concrete implementation
151
+ await self.export_otel_spans(spans)
152
+
153
+ except Exception as e:
154
+ logger.error("Error exporting spans: %s", e, exc_info=True)
155
+
156
+ @abstractmethod
157
+ async def export_otel_spans(self, spans: list[OtelSpan]) -> None:
158
+ """Export a list of OpenTelemetry spans.
159
+
160
+ This method must be implemented by concrete exporters to handle
161
+ the actual export logic (e.g., HTTP requests, file writes, etc.).
162
+
163
+ Args:
164
+ spans (list[OtelSpan]): The list of spans to export.
165
+ """
166
+ pass
@@ -0,0 +1,93 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import logging
17
+
18
+ from nat.builder.context import ContextState
19
+ from nat.plugins.opentelemetry.mixin.otlp_span_exporter_mixin import OTLPSpanExporterMixin
20
+ from nat.plugins.opentelemetry.otel_span_exporter import OtelSpanExporter
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class OTLPSpanAdapterExporter(OTLPSpanExporterMixin, OtelSpanExporter): # pylint: disable=R0901
26
+ """An OpenTelemetry OTLP span exporter for sending traces to OTLP-compatible services.
27
+
28
+ This class combines the OtelSpanExporter base functionality with OTLP-specific
29
+ export capabilities to provide a complete solution for sending telemetry traces
30
+ to any OTLP-compatible collector or service via HTTP.
31
+
32
+ Key Features:
33
+ - Complete span processing pipeline (IntermediateStep → Span → OtelSpan → Export)
34
+ - Batching support for efficient transmission
35
+ - OTLP HTTP protocol for maximum compatibility
36
+ - Configurable authentication via headers
37
+ - Resource attribute management
38
+ - Error handling and retry logic
39
+
40
+ This exporter is commonly used with services like:
41
+ - OpenTelemetry Collector
42
+ - Jaeger (OTLP endpoint)
43
+ - Grafana Tempo
44
+ - Custom OTLP-compatible backends
45
+
46
+ Example:
47
+ exporter = OTLPSpanAdapterExporter(
48
+ endpoint="https://api.service.com/v1/traces",
49
+ headers={"Authorization": "Bearer your-token"},
50
+ batch_size=50,
51
+ flush_interval=10.0
52
+ )
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ *,
58
+ # OtelSpanExporter args
59
+ context_state: ContextState | None = None,
60
+ batch_size: int = 100,
61
+ flush_interval: float = 5.0,
62
+ max_queue_size: int = 1000,
63
+ drop_on_overflow: bool = False,
64
+ shutdown_timeout: float = 10.0,
65
+ resource_attributes: dict[str, str] | None = None,
66
+ # OTLPSpanExporterMixin args
67
+ endpoint: str,
68
+ headers: dict[str, str] | None = None,
69
+ **otlp_kwargs):
70
+ """Initialize the OTLP span exporter.
71
+
72
+ Args:
73
+ context_state: The context state for the exporter.
74
+ batch_size: Number of spans to batch before exporting.
75
+ flush_interval: Time in seconds between automatic batch flushes.
76
+ max_queue_size: Maximum number of spans to queue.
77
+ drop_on_overflow: Whether to drop spans when queue is full.
78
+ shutdown_timeout: Maximum time to wait for export completion during shutdown.
79
+ resource_attributes: Additional resource attributes for spans.
80
+ endpoint: The endpoint for the OTLP service.
81
+ headers: The headers for the OTLP service.
82
+ **otlp_kwargs: Additional keyword arguments for the OTLP service.
83
+ """
84
+ super().__init__(context_state=context_state,
85
+ batch_size=batch_size,
86
+ flush_interval=flush_interval,
87
+ max_queue_size=max_queue_size,
88
+ drop_on_overflow=drop_on_overflow,
89
+ shutdown_timeout=shutdown_timeout,
90
+ resource_attributes=resource_attributes,
91
+ endpoint=endpoint,
92
+ headers=headers,
93
+ **otlp_kwargs)
@@ -0,0 +1,195 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import logging
17
+ import os
18
+
19
+ from pydantic import Field
20
+
21
+ from nat.builder.builder import Builder
22
+ from nat.cli.register_workflow import register_telemetry_exporter
23
+ from nat.data_models.telemetry_exporter import TelemetryExporterBaseConfig
24
+ from nat.observability.mixin.batch_config_mixin import BatchConfigMixin
25
+ from nat.observability.mixin.collector_config_mixin import CollectorConfigMixin
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class LangfuseTelemetryExporter(BatchConfigMixin, TelemetryExporterBaseConfig, name="langfuse"):
31
+ """A telemetry exporter to transmit traces to externally hosted langfuse service."""
32
+
33
+ endpoint: str = Field(description="The langfuse OTEL endpoint (/api/public/otel/v1/traces)")
34
+ public_key: str = Field(description="The Langfuse public key", default="")
35
+ secret_key: str = Field(description="The Langfuse secret key", default="")
36
+ resource_attributes: dict[str, str] = Field(default_factory=dict,
37
+ description="The resource attributes to add to the span")
38
+
39
+
40
+ @register_telemetry_exporter(config_type=LangfuseTelemetryExporter)
41
+ async def langfuse_telemetry_exporter(config: LangfuseTelemetryExporter, builder: Builder): # pylint: disable=W0613
42
+
43
+ import base64
44
+
45
+ from nat.plugins.opentelemetry.otlp_span_adapter_exporter import OTLPSpanAdapterExporter
46
+
47
+ secret_key = config.secret_key or os.environ.get("LANGFUSE_SECRET_KEY")
48
+ public_key = config.public_key or os.environ.get("LANGFUSE_PUBLIC_KEY")
49
+ if not secret_key or not public_key:
50
+ raise ValueError("secret and public keys are required for langfuse")
51
+
52
+ credentials = f"{public_key}:{secret_key}".encode("utf-8")
53
+ auth_header = base64.b64encode(credentials).decode("utf-8")
54
+ headers = {"Authorization": f"Basic {auth_header}"}
55
+
56
+ yield OTLPSpanAdapterExporter(endpoint=config.endpoint,
57
+ headers=headers,
58
+ batch_size=config.batch_size,
59
+ flush_interval=config.flush_interval,
60
+ max_queue_size=config.max_queue_size,
61
+ drop_on_overflow=config.drop_on_overflow,
62
+ shutdown_timeout=config.shutdown_timeout)
63
+
64
+
65
+ class LangsmithTelemetryExporter(BatchConfigMixin, CollectorConfigMixin, TelemetryExporterBaseConfig, name="langsmith"):
66
+ """A telemetry exporter to transmit traces to externally hosted langsmith service."""
67
+
68
+ endpoint: str = Field(
69
+ description="The langsmith OTEL endpoint",
70
+ default="https://api.smith.langchain.com/otel/v1/traces",
71
+ )
72
+ api_key: str = Field(description="The Langsmith API key", default="")
73
+ resource_attributes: dict[str, str] = Field(default_factory=dict,
74
+ description="The resource attributes to add to the span")
75
+
76
+
77
+ @register_telemetry_exporter(config_type=LangsmithTelemetryExporter)
78
+ async def langsmith_telemetry_exporter(config: LangsmithTelemetryExporter, builder: Builder): # pylint: disable=W0613
79
+ """Create a Langsmith telemetry exporter."""
80
+
81
+ from nat.plugins.opentelemetry.otlp_span_adapter_exporter import OTLPSpanAdapterExporter
82
+
83
+ api_key = config.api_key or os.environ.get("LANGSMITH_API_KEY")
84
+ if not api_key:
85
+ raise ValueError("API key is required for langsmith")
86
+
87
+ headers = {"x-api-key": api_key, "Langsmith-Project": config.project}
88
+ yield OTLPSpanAdapterExporter(endpoint=config.endpoint,
89
+ headers=headers,
90
+ batch_size=config.batch_size,
91
+ flush_interval=config.flush_interval,
92
+ max_queue_size=config.max_queue_size,
93
+ drop_on_overflow=config.drop_on_overflow,
94
+ shutdown_timeout=config.shutdown_timeout)
95
+
96
+
97
+ class OtelCollectorTelemetryExporter(BatchConfigMixin,
98
+ CollectorConfigMixin,
99
+ TelemetryExporterBaseConfig,
100
+ name="otelcollector"):
101
+ """A telemetry exporter to transmit traces to externally hosted otel collector service."""
102
+
103
+ resource_attributes: dict[str, str] = Field(default_factory=dict,
104
+ description="The resource attributes to add to the span")
105
+
106
+
107
+ @register_telemetry_exporter(config_type=OtelCollectorTelemetryExporter)
108
+ async def otel_telemetry_exporter(config: OtelCollectorTelemetryExporter, builder: Builder): # pylint: disable=W0613
109
+ """Create an OpenTelemetry telemetry exporter."""
110
+
111
+ from nat.plugins.opentelemetry.otel_span_exporter import get_opentelemetry_sdk_version
112
+ from nat.plugins.opentelemetry.otlp_span_adapter_exporter import OTLPSpanAdapterExporter
113
+
114
+ # Default resource attributes
115
+ default_resource_attributes = {
116
+ "telemetry.sdk.language": "python",
117
+ "telemetry.sdk.name": "opentelemetry",
118
+ "telemetry.sdk.version": get_opentelemetry_sdk_version(),
119
+ "service.name": config.project,
120
+ }
121
+
122
+ # Merge defaults with config, giving precedence to config
123
+ merged_resource_attributes = {**default_resource_attributes, **config.resource_attributes}
124
+
125
+ yield OTLPSpanAdapterExporter(endpoint=config.endpoint,
126
+ resource_attributes=merged_resource_attributes,
127
+ batch_size=config.batch_size,
128
+ flush_interval=config.flush_interval,
129
+ max_queue_size=config.max_queue_size,
130
+ drop_on_overflow=config.drop_on_overflow,
131
+ shutdown_timeout=config.shutdown_timeout)
132
+
133
+
134
+ class PatronusTelemetryExporter(BatchConfigMixin, CollectorConfigMixin, TelemetryExporterBaseConfig, name="patronus"):
135
+ """A telemetry exporter to transmit traces to Patronus service."""
136
+
137
+ api_key: str = Field(description="The Patronus API key", default="")
138
+ resource_attributes: dict[str, str] = Field(default_factory=dict,
139
+ description="The resource attributes to add to the span")
140
+
141
+
142
+ @register_telemetry_exporter(config_type=PatronusTelemetryExporter)
143
+ async def patronus_telemetry_exporter(config: PatronusTelemetryExporter, builder: Builder): # pylint: disable=W0613
144
+ """Create a Patronus telemetry exporter."""
145
+
146
+ from nat.plugins.opentelemetry.otlp_span_adapter_exporter import OTLPSpanAdapterExporter
147
+
148
+ api_key = config.api_key or os.environ.get("PATRONUS_API_KEY")
149
+ if not api_key:
150
+ raise ValueError("API key is required for Patronus")
151
+
152
+ headers = {
153
+ "x-api-key": api_key,
154
+ "pat-project-name": config.project,
155
+ }
156
+ yield OTLPSpanAdapterExporter(endpoint=config.endpoint,
157
+ headers=headers,
158
+ batch_size=config.batch_size,
159
+ flush_interval=config.flush_interval,
160
+ max_queue_size=config.max_queue_size,
161
+ drop_on_overflow=config.drop_on_overflow,
162
+ shutdown_timeout=config.shutdown_timeout)
163
+
164
+
165
+ # pylint: disable=W0613
166
+ class GalileoTelemetryExporter(BatchConfigMixin, CollectorConfigMixin, TelemetryExporterBaseConfig, name="galileo"):
167
+ """A telemetry exporter to transmit traces to externally hosted galileo service."""
168
+
169
+ endpoint: str = Field(description="The galileo endpoint to export telemetry traces.",
170
+ default="https://app.galileo.ai/api/galileo/otel/traces")
171
+ logstream: str = Field(description="The logstream name to group the telemetry traces.")
172
+ api_key: str = Field(description="The api key to authenticate with the galileo service.")
173
+
174
+
175
+ @register_telemetry_exporter(config_type=GalileoTelemetryExporter)
176
+ async def galileo_telemetry_exporter(config: GalileoTelemetryExporter, builder: Builder): # pylint: disable=W0613
177
+ """Create a Galileo telemetry exporter."""
178
+
179
+ from nat.plugins.opentelemetry.otlp_span_adapter_exporter import OTLPSpanAdapterExporter
180
+
181
+ headers = {
182
+ "Galileo-API-Key": config.api_key,
183
+ "logstream": config.logstream,
184
+ "project": config.project,
185
+ }
186
+
187
+ yield OTLPSpanAdapterExporter(
188
+ endpoint=config.endpoint,
189
+ headers=headers,
190
+ batch_size=config.batch_size,
191
+ flush_interval=config.flush_interval,
192
+ max_queue_size=config.max_queue_size,
193
+ drop_on_overflow=config.drop_on_overflow,
194
+ shutdown_timeout=config.shutdown_timeout,
195
+ )
@@ -0,0 +1,228 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import logging
17
+ import time
18
+
19
+ from openinference.semconv.trace import OpenInferenceSpanKindValues
20
+ from openinference.semconv.trace import SpanAttributes
21
+ from opentelemetry.trace import SpanContext
22
+ from opentelemetry.trace import SpanKind
23
+ from opentelemetry.trace import Status
24
+ from opentelemetry.trace import StatusCode
25
+ from opentelemetry.trace import TraceFlags
26
+
27
+ from nat.data_models.span import Span
28
+ from nat.data_models.span import SpanStatusCode
29
+ from nat.plugins.opentelemetry.otel_span import OtelSpan
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ SPAN_EVENT_TYPE_TO_SPAN_KIND_MAP = {
34
+ "LLM_START": OpenInferenceSpanKindValues.LLM,
35
+ "LLM_END": OpenInferenceSpanKindValues.LLM,
36
+ "LLM_NEW_TOKEN": OpenInferenceSpanKindValues.LLM,
37
+ "TOOL_START": OpenInferenceSpanKindValues.TOOL,
38
+ "TOOL_END": OpenInferenceSpanKindValues.TOOL,
39
+ "FUNCTION_START": OpenInferenceSpanKindValues.CHAIN,
40
+ "FUNCTION_END": OpenInferenceSpanKindValues.CHAIN,
41
+ "WORKFLOW_START": OpenInferenceSpanKindValues.CHAIN,
42
+ "WORKFLOW_END": OpenInferenceSpanKindValues.CHAIN,
43
+ "TASK_START": OpenInferenceSpanKindValues.CHAIN,
44
+ "TASK_END": OpenInferenceSpanKindValues.CHAIN,
45
+ "CUSTOM_START": OpenInferenceSpanKindValues.CHAIN,
46
+ "CUSTOM_END": OpenInferenceSpanKindValues.CHAIN,
47
+ "EMBEDDER_START": OpenInferenceSpanKindValues.EMBEDDING,
48
+ "EMBEDDER_END": OpenInferenceSpanKindValues.EMBEDDING,
49
+ "RETRIEVER_START": OpenInferenceSpanKindValues.RETRIEVER,
50
+ "RETRIEVER_END": OpenInferenceSpanKindValues.RETRIEVER,
51
+ "AGENT_START": OpenInferenceSpanKindValues.AGENT,
52
+ "AGENT_END": OpenInferenceSpanKindValues.AGENT,
53
+ "RERANKER_START": OpenInferenceSpanKindValues.RERANKER,
54
+ "RERANKER_END": OpenInferenceSpanKindValues.RERANKER,
55
+ "GUARDRAIL_START": OpenInferenceSpanKindValues.GUARDRAIL,
56
+ "GUARDRAIL_END": OpenInferenceSpanKindValues.GUARDRAIL,
57
+ "EVALUATOR_START": OpenInferenceSpanKindValues.EVALUATOR,
58
+ "EVALUATOR_END": OpenInferenceSpanKindValues.EVALUATOR,
59
+ }
60
+
61
+
62
+ # Reuse expensive objects to avoid repeated creation
63
+ class _SharedObjects:
64
+
65
+ def __init__(self):
66
+ self.resource = None # type: ignore
67
+ self.instrumentation_scope = None # type: ignore
68
+
69
+
70
+ _shared = _SharedObjects()
71
+ _SAMPLED_TRACE_FLAGS = TraceFlags(1)
72
+
73
+
74
+ def _get_shared_resource():
75
+ """Get shared resource object to avoid repeated creation."""
76
+ if _shared.resource is None:
77
+ from opentelemetry.sdk.resources import Resource
78
+ _shared.resource = Resource.create() # type: ignore
79
+ return _shared.resource
80
+
81
+
82
+ def _get_shared_instrumentation_scope():
83
+ """Get shared instrumentation scope to avoid repeated creation."""
84
+ if _shared.instrumentation_scope is None:
85
+ from opentelemetry.sdk.trace import InstrumentationScope
86
+ _shared.instrumentation_scope = InstrumentationScope("aiq", "1.0.0") # type: ignore
87
+ return _shared.instrumentation_scope
88
+
89
+
90
+ def convert_event_type_to_span_kind(event_type: str) -> OpenInferenceSpanKindValues:
91
+ """Convert an event type to a span kind.
92
+
93
+ Args:
94
+ event_type (str): The event type to convert
95
+
96
+ Returns:
97
+ OpenInferenceSpanKindValues: The corresponding span kind
98
+ """
99
+ return SPAN_EVENT_TYPE_TO_SPAN_KIND_MAP.get(event_type, OpenInferenceSpanKindValues.UNKNOWN)
100
+
101
+
102
+ def convert_span_status_code(aiq_status_code: SpanStatusCode) -> StatusCode:
103
+ """Convert AIQ SpanStatusCode to OpenTelemetry StatusCode.
104
+
105
+ Args:
106
+ aiq_status_code (SpanStatusCode): The AIQ span status code to convert
107
+
108
+ Returns:
109
+ StatusCode: The corresponding OpenTelemetry StatusCode
110
+ """
111
+ status_map = {
112
+ SpanStatusCode.OK: StatusCode.OK,
113
+ SpanStatusCode.ERROR: StatusCode.ERROR,
114
+ SpanStatusCode.UNSET: StatusCode.UNSET,
115
+ }
116
+ return status_map.get(aiq_status_code, StatusCode.UNSET)
117
+
118
+
119
+ def convert_span_to_otel(aiq_span: Span) -> OtelSpan:
120
+ """Convert an AIQ Span to an OtelSpan using ultra-fast conversion.
121
+
122
+ Args:
123
+ aiq_span (Span): The AIQ span to convert
124
+
125
+ Returns:
126
+ OtelSpan: The converted OtelSpan with proper parent hierarchy.
127
+ """
128
+ # Fast path for spans without context
129
+ if not aiq_span.context:
130
+ # Create minimal OtelSpan bypassing expensive constructor
131
+ otel_span = object.__new__(OtelSpan) # Bypass __init__
132
+ otel_span._name = aiq_span.name
133
+ otel_span._context = None # type: ignore
134
+ otel_span._parent = None
135
+ otel_span._attributes = aiq_span.attributes.copy()
136
+ otel_span._events = []
137
+ otel_span._links = []
138
+ otel_span._kind = SpanKind.INTERNAL
139
+ otel_span._start_time = aiq_span.start_time
140
+ otel_span._end_time = aiq_span.end_time
141
+ otel_span._status = Status(StatusCode.UNSET)
142
+ otel_span._ended = False
143
+ otel_span._resource = _get_shared_resource() # type: ignore
144
+ otel_span._instrumentation_scope = _get_shared_instrumentation_scope() # type: ignore
145
+ otel_span._dropped_attributes = 0
146
+ otel_span._dropped_events = 0
147
+ otel_span._dropped_links = 0
148
+ otel_span._status_description = None
149
+ return otel_span
150
+
151
+ # Process parent efficiently (if needed)
152
+ parent_otel_span = None
153
+ trace_id = aiq_span.context.trace_id
154
+
155
+ if aiq_span.parent:
156
+ parent_otel_span = convert_span_to_otel(aiq_span.parent)
157
+ parent_context = parent_otel_span.get_span_context()
158
+ trace_id = parent_context.trace_id
159
+
160
+ # Create SpanContext efficiently
161
+ otel_span_context = SpanContext(
162
+ trace_id=trace_id,
163
+ span_id=aiq_span.context.span_id,
164
+ is_remote=False,
165
+ trace_flags=_SAMPLED_TRACE_FLAGS, # Reuse flags object
166
+ )
167
+
168
+ # Create OtelSpan bypassing expensive constructor
169
+ otel_span = object.__new__(OtelSpan) # Bypass __init__
170
+ otel_span._name = aiq_span.name
171
+ otel_span._context = otel_span_context
172
+ otel_span._parent = parent_otel_span
173
+ otel_span._attributes = aiq_span.attributes.copy()
174
+ otel_span._events = []
175
+ otel_span._links = []
176
+ otel_span._kind = SpanKind.INTERNAL
177
+ otel_span._start_time = aiq_span.start_time
178
+ otel_span._end_time = aiq_span.end_time
179
+
180
+ # Reuse status conversion
181
+ status_code = convert_span_status_code(aiq_span.status.code)
182
+ otel_span._status = Status(status_code, aiq_span.status.message)
183
+
184
+ otel_span._ended = False
185
+ otel_span._resource = _get_shared_resource() # type: ignore
186
+ otel_span._instrumentation_scope = _get_shared_instrumentation_scope() # type: ignore
187
+ otel_span._dropped_attributes = 0
188
+ otel_span._dropped_events = 0
189
+ otel_span._dropped_links = 0
190
+ otel_span._status_description = None
191
+
192
+ # Set span kind efficiently (direct attribute modification)
193
+ event_type = aiq_span.attributes.get("aiq.event_type", "UNKNOWN")
194
+ span_kind = SPAN_EVENT_TYPE_TO_SPAN_KIND_MAP.get(event_type, OpenInferenceSpanKindValues.UNKNOWN)
195
+ otel_span._attributes[SpanAttributes.OPENINFERENCE_SPAN_KIND] = span_kind.value
196
+
197
+ # Process events (only if they exist)
198
+ if aiq_span.events:
199
+ for aiq_event in aiq_span.events:
200
+ # Optimize timestamp handling
201
+ if isinstance(aiq_event.timestamp, int):
202
+ event_timestamp_ns = aiq_event.timestamp
203
+ elif aiq_event.timestamp:
204
+ event_timestamp_ns = int(aiq_event.timestamp)
205
+ else:
206
+ event_timestamp_ns = int(time.time() * 1e9)
207
+
208
+ # Add event directly to internal list (bypass add_event method)
209
+ otel_span._events.append({
210
+ "name": aiq_event.name, "attributes": aiq_event.attributes, "timestamp": event_timestamp_ns
211
+ })
212
+
213
+ return otel_span
214
+
215
+
216
+ def convert_spans_to_otel_batch(spans: list[Span]) -> list[OtelSpan]:
217
+ """Convert a list of AIQ spans to OtelSpans using stateless conversion.
218
+
219
+ This is useful for batch processing or demos. Each span is converted
220
+ independently using the stateless approach.
221
+
222
+ Args:
223
+ spans (list[Span]): List of AIQ spans to convert
224
+
225
+ Returns:
226
+ list[OtelSpan]: List of converted OtelSpans with proper parent-child relationships
227
+ """
228
+ return [convert_span_to_otel(span) for span in spans]