nvidia-nat-opentelemetry 1.2.0__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.
- nat/meta/pypi.md +23 -0
- nat/plugins/opentelemetry/__init__.py +14 -0
- nat/plugins/opentelemetry/mixin/__init__.py +14 -0
- nat/plugins/opentelemetry/mixin/otlp_span_exporter_mixin.py +69 -0
- nat/plugins/opentelemetry/otel_span.py +524 -0
- nat/plugins/opentelemetry/otel_span_exporter.py +166 -0
- nat/plugins/opentelemetry/otlp_span_adapter_exporter.py +93 -0
- nat/plugins/opentelemetry/register.py +195 -0
- nat/plugins/opentelemetry/span_converter.py +228 -0
- nvidia_nat_opentelemetry-1.2.0.dist-info/METADATA +36 -0
- nvidia_nat_opentelemetry-1.2.0.dist-info/RECORD +14 -0
- nvidia_nat_opentelemetry-1.2.0.dist-info/WHEEL +5 -0
- nvidia_nat_opentelemetry-1.2.0.dist-info/entry_points.txt +2 -0
- nvidia_nat_opentelemetry-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -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("nat", "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(nat_status_code: SpanStatusCode) -> StatusCode:
|
|
103
|
+
"""Convert NAT SpanStatusCode to OpenTelemetry StatusCode.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
nat_status_code (SpanStatusCode): The NAT 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(nat_status_code, StatusCode.UNSET)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def convert_span_to_otel(nat_span: Span) -> OtelSpan:
|
|
120
|
+
"""Convert a NAT Span to an OtelSpan using ultra-fast conversion.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
nat_span (Span): The NAT 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 nat_span.context:
|
|
130
|
+
# Create minimal OtelSpan bypassing expensive constructor
|
|
131
|
+
otel_span = object.__new__(OtelSpan) # Bypass __init__
|
|
132
|
+
otel_span._name = nat_span.name
|
|
133
|
+
otel_span._context = None # type: ignore
|
|
134
|
+
otel_span._parent = None
|
|
135
|
+
otel_span._attributes = nat_span.attributes.copy()
|
|
136
|
+
otel_span._events = []
|
|
137
|
+
otel_span._links = []
|
|
138
|
+
otel_span._kind = SpanKind.INTERNAL
|
|
139
|
+
otel_span._start_time = nat_span.start_time
|
|
140
|
+
otel_span._end_time = nat_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 = nat_span.context.trace_id
|
|
154
|
+
|
|
155
|
+
if nat_span.parent:
|
|
156
|
+
parent_otel_span = convert_span_to_otel(nat_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=nat_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 = nat_span.name
|
|
171
|
+
otel_span._context = otel_span_context
|
|
172
|
+
otel_span._parent = parent_otel_span
|
|
173
|
+
otel_span._attributes = nat_span.attributes.copy()
|
|
174
|
+
otel_span._events = []
|
|
175
|
+
otel_span._links = []
|
|
176
|
+
otel_span._kind = SpanKind.INTERNAL
|
|
177
|
+
otel_span._start_time = nat_span.start_time
|
|
178
|
+
otel_span._end_time = nat_span.end_time
|
|
179
|
+
|
|
180
|
+
# Reuse status conversion
|
|
181
|
+
status_code = convert_span_status_code(nat_span.status.code)
|
|
182
|
+
otel_span._status = Status(status_code, nat_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 = nat_span.attributes.get("nat.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 nat_span.events:
|
|
199
|
+
for nat_event in nat_span.events:
|
|
200
|
+
# Optimize timestamp handling
|
|
201
|
+
if isinstance(nat_event.timestamp, int):
|
|
202
|
+
event_timestamp_ns = nat_event.timestamp
|
|
203
|
+
elif nat_event.timestamp:
|
|
204
|
+
event_timestamp_ns = int(nat_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": nat_event.name, "attributes": nat_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 NAT 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 NAT 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]
|