nvidia-nat-opentelemetry 1.1.0a20251020__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 ADDED
@@ -0,0 +1,23 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ SPDX-License-Identifier: Apache-2.0
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ -->
17
+
18
+ ![NVIDIA NeMo Agent Toolkit](https://media.githubusercontent.com/media/NVIDIA/NeMo-Agent-Toolkit/refs/heads/main/docs/source/_static/banner.png "NeMo Agent toolkit banner image"
19
+
20
+ # NVIDIA NeMo Agent Toolkit Subpackage
21
+ This is a subpackage for OpenTelemetry integration for observability.
22
+
23
+ For more information about the NVIDIA NeMo Agent toolkit, please visit the [NeMo Agent toolkit GitHub Repo](https://github.com/NVIDIA/NeMo-Agent-Toolkit).
@@ -0,0 +1,24 @@
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
+ from nat.plugins.opentelemetry.otel_span_exporter import OtelSpanExporter
17
+ from nat.plugins.opentelemetry.otlp_span_adapter_exporter import OTLPSpanAdapterExporter
18
+ from nat.plugins.opentelemetry.otlp_span_redaction_adapter_exporter import OTLPSpanHeaderRedactionAdapterExporter
19
+
20
+ __all__ = [
21
+ "OTLPSpanHeaderRedactionAdapterExporter",
22
+ "OTLPSpanAdapterExporter",
23
+ "OtelSpanExporter",
24
+ ]
@@ -0,0 +1,14 @@
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.
@@ -0,0 +1,69 @@
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
+
18
+ from nat.plugins.opentelemetry.otel_span import OtelSpan
19
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class OTLPSpanExporterMixin:
25
+ """Mixin for OTLP span exporters.
26
+
27
+ This mixin provides OTLP-specific functionality for OpenTelemetry span exporters.
28
+ It handles OTLP protocol transmission using the standard OpenTelemetry OTLP HTTP exporter.
29
+
30
+ Key Features:
31
+ - Standard OTLP HTTP protocol support for span export
32
+ - Configurable endpoint and headers for authentication/routing
33
+ - Integration with OpenTelemetry's OTLPSpanExporter for reliable transmission
34
+ - Works with any OTLP-compatible collector or service
35
+
36
+ This mixin is designed to be used with OtelSpanExporter as a base class:
37
+
38
+ Example::
39
+
40
+ class MyOTLPExporter(OtelSpanExporter, OTLPSpanExporterMixin):
41
+ def __init__(self, endpoint, headers, **kwargs):
42
+ super().__init__(endpoint=endpoint, headers=headers, **kwargs)
43
+ """
44
+
45
+ def __init__(self, *args, endpoint: str, headers: dict[str, str] | None = None, **kwargs):
46
+ """Initialize the OTLP span exporter.
47
+
48
+ Args:
49
+ endpoint: OTLP service endpoint URL.
50
+ headers: HTTP headers for authentication and metadata.
51
+ """
52
+ # Initialize exporter before super().__init__() to ensure it's available
53
+ # if parent class initialization potentially calls export_otel_spans()
54
+ self._exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers)
55
+ super().__init__(*args, **kwargs)
56
+
57
+ async def export_otel_spans(self, spans: list[OtelSpan]) -> None:
58
+ """Export a list of OtelSpans using the OTLP exporter.
59
+
60
+ Args:
61
+ spans (list[OtelSpan]): The list of spans to export.
62
+
63
+ Raises:
64
+ Exception: If there's an error during span export (logged but not re-raised).
65
+ """
66
+ try:
67
+ self._exporter.export(spans) # type: ignore[arg-type]
68
+ except Exception as e:
69
+ logger.error("Error exporting spans: %s", e, exc_info=True)
@@ -0,0 +1,525 @@
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 json
17
+ import logging
18
+ import time
19
+ import traceback
20
+ import uuid
21
+ from collections.abc import Sequence
22
+ from enum import Enum
23
+ from typing import Any
24
+
25
+ from opentelemetry import trace as trace_api
26
+ from opentelemetry.sdk import util
27
+ from opentelemetry.sdk.resources import Resource
28
+ from opentelemetry.sdk.trace import Event
29
+ from opentelemetry.sdk.trace import InstrumentationScope
30
+ from opentelemetry.trace import Context
31
+ from opentelemetry.trace import Link
32
+ from opentelemetry.trace import SpanContext
33
+ from opentelemetry.trace import SpanKind
34
+ from opentelemetry.trace import Status
35
+ from opentelemetry.trace import StatusCode
36
+ from opentelemetry.trace import TraceFlags
37
+ from opentelemetry.trace.span import Span
38
+ from opentelemetry.util import types
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ class MimeTypes(Enum):
44
+ """Mime types for the span."""
45
+ TEXT = "text/plain"
46
+ JSON = "application/json"
47
+
48
+
49
+ class OtelSpan(Span):
50
+ """A manually created OpenTelemetry span.
51
+
52
+ This class is a wrapper around the OpenTelemetry Span class.
53
+ It provides a more convenient interface for creating and manipulating spans.
54
+
55
+ Args:
56
+ name (str): The name of the span.
57
+ context (Context | SpanContext | None): The context of the span.
58
+ parent (Span | None): The parent span.
59
+ attributes (dict[str, Any] | None): The attributes of the span.
60
+ events (list | None): The events of the span.
61
+ links (list | None): The links of the span.
62
+ kind (int | None): The kind of the span.
63
+ start_time (int | None): The start time of the span in nanoseconds.
64
+ end_time (int | None): The end time of the span in nanoseconds.
65
+ status (Status | None): The status of the span.
66
+ resource (Resource | None): The resource of the span.
67
+ instrumentation_scope (InstrumentationScope | None): The instrumentation scope of the span.
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ name: str,
73
+ context: Context | SpanContext | None,
74
+ parent: Span | None = None,
75
+ attributes: dict[str, Any] | None = None,
76
+ events: list | None = None,
77
+ links: list | None = None,
78
+ kind: int | SpanKind | None = None,
79
+ start_time: int | None = None,
80
+ end_time: int | None = None,
81
+ status: Status | None = None,
82
+ resource: Resource | None = None,
83
+ instrumentation_scope: InstrumentationScope | None = None,
84
+ ):
85
+ """Initialize the OtelSpan with the specified values."""
86
+ self._name = name
87
+ # Create a new SpanContext if none provided or if Context is provided
88
+ if context is None or isinstance(context, Context):
89
+ # Generate non-zero IDs per OTel spec (uuid4 is automatically non-zero)
90
+ trace_id = uuid.uuid4().int
91
+ span_id = uuid.uuid4().int >> 64
92
+ self._context = SpanContext(
93
+ trace_id=trace_id,
94
+ span_id=span_id,
95
+ is_remote=False,
96
+ trace_flags=TraceFlags(1), # SAMPLED
97
+ )
98
+ else:
99
+ self._context = context
100
+ self._parent = parent
101
+ self._attributes = attributes or {}
102
+ self._events = events or []
103
+ self._links = links or []
104
+ self._kind = kind or SpanKind.INTERNAL
105
+ self._start_time = start_time or int(time.time() * 1e9) # Convert to nanoseconds
106
+ self._end_time = end_time
107
+ self._status = status or Status(StatusCode.UNSET)
108
+ self._ended = False
109
+ self._resource = resource or Resource.create()
110
+ self._instrumentation_scope = instrumentation_scope or InstrumentationScope("nat", "1.0.0")
111
+ self._dropped_attributes = 0
112
+ self._dropped_events = 0
113
+ self._dropped_links = 0
114
+ self._status_description = None
115
+
116
+ # Add parent span as a link if provided
117
+ if parent is not None:
118
+ parent_context = parent.get_span_context()
119
+ # Create a new span context that inherits the trace ID from the parent
120
+ self._context = SpanContext(
121
+ trace_id=parent_context.trace_id,
122
+ span_id=self._context.span_id,
123
+ is_remote=False,
124
+ trace_flags=parent_context.trace_flags,
125
+ trace_state=parent_context.trace_state,
126
+ )
127
+ # Create a proper link object instead of a dictionary
128
+ self._links.append(Link(context=parent_context, attributes={"parent.name": self._name}))
129
+
130
+ @property
131
+ def resource(self) -> Resource:
132
+ """Get the resource associated with this span.
133
+
134
+ Returns:
135
+ Resource: The resource.
136
+ """
137
+ return self._resource
138
+
139
+ def set_resource(self, resource: Resource) -> None:
140
+ """Set the resource associated with this span.
141
+
142
+ Args:
143
+ resource (Resource): The resource to set.
144
+ """
145
+ self._resource = resource
146
+
147
+ @property
148
+ def instrumentation_scope(self) -> InstrumentationScope:
149
+ """Get the instrumentation scope associated with this span.
150
+
151
+ Returns:
152
+ InstrumentationScope: The instrumentation scope.
153
+ """
154
+ return self._instrumentation_scope
155
+
156
+ @property
157
+ def parent(self) -> Span | None:
158
+ """Get the parent span.
159
+
160
+ Returns:
161
+ Span | None: The parent span.
162
+ """
163
+ return self._parent
164
+
165
+ @property
166
+ def name(self) -> str:
167
+ """Get the name of the span.
168
+
169
+ Returns:
170
+ str: The name of the span.
171
+ """
172
+ return self._name
173
+
174
+ @property
175
+ def kind(self) -> int | SpanKind:
176
+ """Get the kind of the span.
177
+
178
+ Returns:
179
+ int | SpanKind: The kind of the span.
180
+ """
181
+ return self._kind
182
+
183
+ @property
184
+ def start_time(self) -> int:
185
+ """Get the start time of the span in nanoseconds.
186
+
187
+ Returns:
188
+ int: The start time of the span in nanoseconds.
189
+ """
190
+ return self._start_time
191
+
192
+ @property
193
+ def end_time(self) -> int | None:
194
+ """Get the end time of the span in nanoseconds.
195
+
196
+ Returns:
197
+ int | None: The end time of the span in nanoseconds.
198
+ """
199
+ return self._end_time
200
+
201
+ @property
202
+ def attributes(self) -> dict[str, Any]:
203
+ """Get all attributes of the span.
204
+
205
+ Returns:
206
+ dict[str, Any]: The attributes of the span.
207
+ """
208
+ return self._attributes
209
+
210
+ @property
211
+ def events(self) -> list:
212
+ """Get all events of the span.
213
+
214
+ Returns:
215
+ list: The events of the span.
216
+ """
217
+ return self._events
218
+
219
+ @property
220
+ def links(self) -> list:
221
+ """Get all links of the span.
222
+
223
+ Returns:
224
+ list: The links of the span.
225
+ """
226
+ return self._links
227
+
228
+ @property
229
+ def status(self) -> Status:
230
+ """Get the status of the span.
231
+
232
+ Returns:
233
+ Status: The status of the span.
234
+ """
235
+ return self._status
236
+
237
+ @property
238
+ def dropped_attributes(self) -> int:
239
+ """Get the number of dropped attributes.
240
+
241
+ Returns:
242
+ int: The number of dropped attributes.
243
+ """
244
+ return self._dropped_attributes
245
+
246
+ @property
247
+ def dropped_events(self) -> int:
248
+ """Get the number of dropped events.
249
+
250
+ Returns:
251
+ int: The number of dropped events.
252
+ """
253
+ return self._dropped_events
254
+
255
+ @property
256
+ def dropped_links(self) -> int:
257
+ """Get the number of dropped links.
258
+
259
+ Returns:
260
+ int: The number of dropped links.
261
+ """
262
+ return self._dropped_links
263
+
264
+ @property
265
+ def span_id(self) -> int:
266
+ """Get the span ID.
267
+
268
+ Returns:
269
+ int: The span ID.
270
+ """
271
+ return self._context.span_id
272
+
273
+ @property
274
+ def trace_id(self) -> int:
275
+ """Get the trace ID.
276
+
277
+ Returns:
278
+ int: The trace ID.
279
+ """
280
+ return self._context.trace_id
281
+
282
+ @property
283
+ def is_remote(self) -> bool:
284
+ """Get whether this span is remote.
285
+
286
+ Returns:
287
+ bool: True if the span is remote, False otherwise.
288
+ """
289
+ return self._context.is_remote
290
+
291
+ def end(self, end_time: int | None = None) -> None:
292
+ """End the span.
293
+
294
+ Args:
295
+ end_time (int | None): The end time of the span in nanoseconds.
296
+ """
297
+ if not self._ended:
298
+ self._ended = True
299
+ self._end_time = end_time or int(time.time() * 1e9)
300
+
301
+ def is_recording(self) -> bool:
302
+ """Check if the span is recording.
303
+
304
+ Returns:
305
+ bool: True if the span is recording, False otherwise.
306
+ """
307
+ return not self._ended
308
+
309
+ def get_span_context(self) -> SpanContext:
310
+ """Get the span context.
311
+
312
+ Returns:
313
+ SpanContext: The span context.
314
+ """
315
+ return self._context
316
+
317
+ def set_attribute(self, key: str, value: Any) -> None:
318
+ """Set an attribute on the span.
319
+
320
+ Args:
321
+ key (str): The key of the attribute.
322
+ value (Any): The value of the attribute.
323
+ """
324
+ self._attributes[key] = value
325
+
326
+ def set_attributes(self, attributes: dict[str, Any]) -> None:
327
+ """Set multiple attributes on the span.
328
+
329
+ Args:
330
+ attributes (dict[str, Any]): The attributes to set.
331
+ """
332
+ self._attributes.update(attributes)
333
+
334
+ def add_event(self, name: str, attributes: dict[str, Any] | None = None, timestamp: int | None = None) -> None:
335
+ """Add an event to the span.
336
+
337
+ Args:
338
+ name (str): The name of the event.
339
+ attributes (dict[str, Any] | None): The attributes of the event.
340
+ timestamp (int | None): The timestamp of the event in nanoseconds.
341
+ """
342
+ if timestamp is None:
343
+ timestamp = int(time.time() * 1e9)
344
+ self._events.append({"name": name, "attributes": attributes or {}, "timestamp": timestamp})
345
+
346
+ def update_name(self, name: str) -> None:
347
+ """Update the span name.
348
+
349
+ Args:
350
+ name (str): The name to set.
351
+ """
352
+ self._name = name
353
+
354
+ def set_status(self, status: Status, description: str | None = None) -> None:
355
+ """Set the span status.
356
+
357
+ Args:
358
+ status (Status): The status to set.
359
+ description (str | None): The description of the status.
360
+ """
361
+ self._status = status
362
+ self._status_description = description
363
+
364
+ def get_links(self) -> list:
365
+ """Get all links of the span.
366
+
367
+ Returns:
368
+ list: The links of the span.
369
+ """
370
+ return self._links
371
+
372
+ def get_end_time(self) -> int | None:
373
+ """Get the end time of the span.
374
+
375
+ Returns:
376
+ int | None: The end time of the span in nanoseconds.
377
+ """
378
+ return self._end_time
379
+
380
+ def get_status(self) -> Status:
381
+ """Get the status of the span.
382
+
383
+ Returns:
384
+ Status: The status of the span.
385
+ """
386
+ return self._status
387
+
388
+ def get_parent(self) -> Span | None:
389
+ """Get the parent span.
390
+
391
+ Returns:
392
+ Span | None: The parent span.
393
+ """
394
+ return self._parent
395
+
396
+ def record_exception(self,
397
+ exception: Exception,
398
+ attributes: dict[str, Any] | None = None,
399
+ timestamp: int | None = None,
400
+ escaped: bool = False) -> None:
401
+ """
402
+ Record an exception on the span.
403
+
404
+ Args:
405
+ exception: The exception to record
406
+ attributes: Optional dictionary of attributes to add to the event
407
+ timestamp: Optional timestamp for the event
408
+ escaped: Whether the exception was escaped
409
+ """
410
+
411
+ if timestamp is None:
412
+ timestamp = int(time.time() * 1e9)
413
+
414
+ # Get the exception type and message
415
+ exc_type = type(exception).__name__
416
+ exc_message = str(exception)
417
+
418
+ # Get the stack trace
419
+ exc_traceback = traceback.format_exception(type(exception), exception, exception.__traceback__)
420
+ stack_trace = "".join(exc_traceback)
421
+
422
+ # Create the event attributes
423
+ event_attrs = {
424
+ "exception.type": exc_type,
425
+ "exception.message": exc_message,
426
+ "exception.stacktrace": stack_trace,
427
+ }
428
+
429
+ # Add any additional attributes
430
+ if attributes:
431
+ event_attrs.update(attributes)
432
+
433
+ # Add the event to the span
434
+ self.add_event("exception", event_attrs)
435
+
436
+ # Set the span status to error
437
+ self.set_status(Status(StatusCode.ERROR, exc_message))
438
+
439
+ def copy(self) -> "OtelSpan":
440
+ """
441
+ Create a new OtelSpan instance with the same values as this one.
442
+ Note that this is not a deep copy - mutable objects like attributes, events, and links
443
+ will be shared between the original and the copy.
444
+
445
+ Returns:
446
+ A new OtelSpan instance with the same values
447
+ """
448
+ return OtelSpan(
449
+ name=self._name,
450
+ context=self._context,
451
+ parent=self._parent,
452
+ attributes=self._attributes.copy(),
453
+ events=self._events.copy(),
454
+ links=self._links.copy(),
455
+ kind=self._kind,
456
+ start_time=self._start_time,
457
+ end_time=self._end_time,
458
+ status=self._status,
459
+ resource=self._resource,
460
+ instrumentation_scope=self._instrumentation_scope,
461
+ )
462
+
463
+ @staticmethod
464
+ def _format_context(context: SpanContext) -> dict[str, str]:
465
+ return {
466
+ "trace_id": f"0x{trace_api.format_trace_id(context.trace_id)}",
467
+ "span_id": f"0x{trace_api.format_span_id(context.span_id)}",
468
+ "trace_state": repr(context.trace_state),
469
+ }
470
+
471
+ @staticmethod
472
+ def _format_attributes(attributes: types.Attributes, ) -> dict[str, Any] | None:
473
+ if attributes is not None and not isinstance(attributes, dict):
474
+ return dict(attributes)
475
+ return attributes
476
+
477
+ @staticmethod
478
+ def _format_events(events: Sequence[Event]) -> list[dict[str, Any]]:
479
+ return [{
480
+ "name": event.name,
481
+ "timestamp": util.ns_to_iso_str(event.timestamp),
482
+ "attributes": OtelSpan._format_attributes(event.attributes),
483
+ } for event in events]
484
+
485
+ @staticmethod
486
+ def _format_links(links: Sequence[trace_api.Link]) -> list[dict[str, Any]]:
487
+ return [{
488
+ "context": OtelSpan._format_context(link.context),
489
+ "attributes": OtelSpan._format_attributes(link.attributes),
490
+ } for link in links]
491
+
492
+ def to_json(self, indent: int | None = 4):
493
+ parent_id = None
494
+ if self.parent is not None:
495
+ parent_id = f"0x{trace_api.format_span_id(self.parent.span_id)}" # type: ignore
496
+
497
+ start_time = None
498
+ if self._start_time:
499
+ start_time = util.ns_to_iso_str(self._start_time)
500
+
501
+ end_time = None
502
+ if self._end_time:
503
+ end_time = util.ns_to_iso_str(self._end_time)
504
+
505
+ status = {
506
+ "status_code": str(self._status.status_code.name),
507
+ }
508
+ if self._status.description:
509
+ status["description"] = self._status.description
510
+
511
+ f_span = {
512
+ "name": self._name,
513
+ "context": (self._format_context(self._context) if self._context else None),
514
+ "kind": str(self.kind),
515
+ "parent_id": parent_id,
516
+ "start_time": start_time,
517
+ "end_time": end_time,
518
+ "status": status,
519
+ "attributes": self._format_attributes(self._attributes),
520
+ "events": self._format_events(self._events),
521
+ "links": self._format_links(self._links),
522
+ "resource": json.loads(self.resource.to_json()),
523
+ }
524
+
525
+ return json.dumps(f_span, indent=indent)