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 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,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,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 opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
19
+
20
+ from nat.plugins.opentelemetry.otel_span import OtelSpan
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class OTLPSpanExporterMixin:
26
+ """Mixin for OTLP span exporters.
27
+
28
+ This mixin provides OTLP-specific functionality for OpenTelemetry span exporters.
29
+ It handles OTLP protocol transmission using the standard OpenTelemetry OTLP HTTP exporter.
30
+
31
+ Key Features:
32
+ - Standard OTLP HTTP protocol support for span export
33
+ - Configurable endpoint and headers for authentication/routing
34
+ - Integration with OpenTelemetry's OTLPSpanExporter for reliable transmission
35
+ - Works with any OTLP-compatible collector or service
36
+
37
+ This mixin is designed to be used with OtelSpanExporter as a base class:
38
+
39
+ Example:
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,524 @@
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): # pylint: disable=too-many-public-methods
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
+ trace_id = uuid.uuid4().int & ((1 << 128) - 1)
90
+ span_id = uuid.uuid4().int & ((1 << 64) - 1)
91
+ self._context = SpanContext(
92
+ trace_id=trace_id,
93
+ span_id=span_id,
94
+ is_remote=False,
95
+ trace_flags=TraceFlags(1), # SAMPLED
96
+ )
97
+ else:
98
+ self._context = context
99
+ self._parent = parent
100
+ self._attributes = attributes or {}
101
+ self._events = events or []
102
+ self._links = links or []
103
+ self._kind = kind or SpanKind.INTERNAL
104
+ self._start_time = start_time or int(time.time() * 1e9) # Convert to nanoseconds
105
+ self._end_time = end_time
106
+ self._status = status or Status(StatusCode.UNSET)
107
+ self._ended = False
108
+ self._resource = resource or Resource.create()
109
+ self._instrumentation_scope = instrumentation_scope or InstrumentationScope("nat", "1.0.0")
110
+ self._dropped_attributes = 0
111
+ self._dropped_events = 0
112
+ self._dropped_links = 0
113
+ self._status_description = None
114
+
115
+ # Add parent span as a link if provided
116
+ if parent is not None:
117
+ parent_context = parent.get_span_context()
118
+ # Create a new span context that inherits the trace ID from the parent
119
+ self._context = SpanContext(
120
+ trace_id=parent_context.trace_id,
121
+ span_id=self._context.span_id,
122
+ is_remote=False,
123
+ trace_flags=parent_context.trace_flags,
124
+ trace_state=parent_context.trace_state,
125
+ )
126
+ # Create a proper link object instead of a dictionary
127
+ self._links.append(Link(context=parent_context, attributes={"parent.name": self._name}))
128
+
129
+ @property
130
+ def resource(self) -> Resource:
131
+ """Get the resource associated with this span.
132
+
133
+ Returns:
134
+ Resource: The resource.
135
+ """
136
+ return self._resource
137
+
138
+ def set_resource(self, resource: Resource) -> None:
139
+ """Set the resource associated with this span.
140
+
141
+ Args:
142
+ resource (Resource): The resource to set.
143
+ """
144
+ self._resource = resource
145
+
146
+ @property
147
+ def instrumentation_scope(self) -> InstrumentationScope:
148
+ """Get the instrumentation scope associated with this span.
149
+
150
+ Returns:
151
+ InstrumentationScope: The instrumentation scope.
152
+ """
153
+ return self._instrumentation_scope
154
+
155
+ @property
156
+ def parent(self) -> Span | None:
157
+ """Get the parent span.
158
+
159
+ Returns:
160
+ Span | None: The parent span.
161
+ """
162
+ return self._parent
163
+
164
+ @property
165
+ def name(self) -> str:
166
+ """Get the name of the span.
167
+
168
+ Returns:
169
+ str: The name of the span.
170
+ """
171
+ return self._name
172
+
173
+ @property
174
+ def kind(self) -> int | SpanKind:
175
+ """Get the kind of the span.
176
+
177
+ Returns:
178
+ int | SpanKind: The kind of the span.
179
+ """
180
+ return self._kind
181
+
182
+ @property
183
+ def start_time(self) -> int:
184
+ """Get the start time of the span in nanoseconds.
185
+
186
+ Returns:
187
+ int: The start time of the span in nanoseconds.
188
+ """
189
+ return self._start_time
190
+
191
+ @property
192
+ def end_time(self) -> int | None:
193
+ """Get the end time of the span in nanoseconds.
194
+
195
+ Returns:
196
+ int | None: The end time of the span in nanoseconds.
197
+ """
198
+ return self._end_time
199
+
200
+ @property
201
+ def attributes(self) -> dict[str, Any]:
202
+ """Get all attributes of the span.
203
+
204
+ Returns:
205
+ dict[str, Any]: The attributes of the span.
206
+ """
207
+ return self._attributes
208
+
209
+ @property
210
+ def events(self) -> list:
211
+ """Get all events of the span.
212
+
213
+ Returns:
214
+ list: The events of the span.
215
+ """
216
+ return self._events
217
+
218
+ @property
219
+ def links(self) -> list:
220
+ """Get all links of the span.
221
+
222
+ Returns:
223
+ list: The links of the span.
224
+ """
225
+ return self._links
226
+
227
+ @property
228
+ def status(self) -> Status:
229
+ """Get the status of the span.
230
+
231
+ Returns:
232
+ Status: The status of the span.
233
+ """
234
+ return self._status
235
+
236
+ @property
237
+ def dropped_attributes(self) -> int:
238
+ """Get the number of dropped attributes.
239
+
240
+ Returns:
241
+ int: The number of dropped attributes.
242
+ """
243
+ return self._dropped_attributes
244
+
245
+ @property
246
+ def dropped_events(self) -> int:
247
+ """Get the number of dropped events.
248
+
249
+ Returns:
250
+ int: The number of dropped events.
251
+ """
252
+ return self._dropped_events
253
+
254
+ @property
255
+ def dropped_links(self) -> int:
256
+ """Get the number of dropped links.
257
+
258
+ Returns:
259
+ int: The number of dropped links.
260
+ """
261
+ return self._dropped_links
262
+
263
+ @property
264
+ def span_id(self) -> int:
265
+ """Get the span ID.
266
+
267
+ Returns:
268
+ int: The span ID.
269
+ """
270
+ return self._context.span_id
271
+
272
+ @property
273
+ def trace_id(self) -> int:
274
+ """Get the trace ID.
275
+
276
+ Returns:
277
+ int: The trace ID.
278
+ """
279
+ return self._context.trace_id
280
+
281
+ @property
282
+ def is_remote(self) -> bool:
283
+ """Get whether this span is remote.
284
+
285
+ Returns:
286
+ bool: True if the span is remote, False otherwise.
287
+ """
288
+ return self._context.is_remote
289
+
290
+ def end(self, end_time: int | None = None) -> None:
291
+ """End the span.
292
+
293
+ Args:
294
+ end_time (int | None): The end time of the span in nanoseconds.
295
+ """
296
+ if not self._ended:
297
+ self._ended = True
298
+ self._end_time = end_time or int(time.time() * 1e9)
299
+
300
+ def is_recording(self) -> bool:
301
+ """Check if the span is recording.
302
+
303
+ Returns:
304
+ bool: True if the span is recording, False otherwise.
305
+ """
306
+ return not self._ended
307
+
308
+ def get_span_context(self) -> SpanContext:
309
+ """Get the span context.
310
+
311
+ Returns:
312
+ SpanContext: The span context.
313
+ """
314
+ return self._context
315
+
316
+ def set_attribute(self, key: str, value: Any) -> None:
317
+ """Set an attribute on the span.
318
+
319
+ Args:
320
+ key (str): The key of the attribute.
321
+ value (Any): The value of the attribute.
322
+ """
323
+ self._attributes[key] = value
324
+
325
+ def set_attributes(self, attributes: dict[str, Any]) -> None:
326
+ """Set multiple attributes on the span.
327
+
328
+ Args:
329
+ attributes (dict[str, Any]): The attributes to set.
330
+ """
331
+ self._attributes.update(attributes)
332
+
333
+ def add_event(self, name: str, attributes: dict[str, Any] | None = None, timestamp: int | None = None) -> None:
334
+ """Add an event to the span.
335
+
336
+ Args:
337
+ name (str): The name of the event.
338
+ attributes (dict[str, Any] | None): The attributes of the event.
339
+ timestamp (int | None): The timestamp of the event in nanoseconds.
340
+ """
341
+ if timestamp is None:
342
+ timestamp = int(time.time() * 1e9)
343
+ self._events.append({"name": name, "attributes": attributes or {}, "timestamp": timestamp})
344
+
345
+ def update_name(self, name: str) -> None:
346
+ """Update the span name.
347
+
348
+ Args:
349
+ name (str): The name to set.
350
+ """
351
+ self._name = name
352
+
353
+ def set_status(self, status: Status, description: str | None = None) -> None:
354
+ """Set the span status.
355
+
356
+ Args:
357
+ status (Status): The status to set.
358
+ description (str | None): The description of the status.
359
+ """
360
+ self._status = status
361
+ self._status_description = description
362
+
363
+ def get_links(self) -> list:
364
+ """Get all links of the span.
365
+
366
+ Returns:
367
+ list: The links of the span.
368
+ """
369
+ return self._links
370
+
371
+ def get_end_time(self) -> int | None:
372
+ """Get the end time of the span.
373
+
374
+ Returns:
375
+ int | None: The end time of the span in nanoseconds.
376
+ """
377
+ return self._end_time
378
+
379
+ def get_status(self) -> Status:
380
+ """Get the status of the span.
381
+
382
+ Returns:
383
+ Status: The status of the span.
384
+ """
385
+ return self._status
386
+
387
+ def get_parent(self) -> Span | None:
388
+ """Get the parent span.
389
+
390
+ Returns:
391
+ Span | None: The parent span.
392
+ """
393
+ return self._parent
394
+
395
+ def record_exception(self,
396
+ exception: Exception,
397
+ attributes: dict[str, Any] | None = None,
398
+ timestamp: int | None = None,
399
+ escaped: bool = False) -> None:
400
+ """
401
+ Record an exception on the span.
402
+
403
+ Args:
404
+ exception: The exception to record
405
+ attributes: Optional dictionary of attributes to add to the event
406
+ timestamp: Optional timestamp for the event
407
+ escaped: Whether the exception was escaped
408
+ """
409
+
410
+ if timestamp is None:
411
+ timestamp = int(time.time() * 1e9)
412
+
413
+ # Get the exception type and message
414
+ exc_type = type(exception).__name__
415
+ exc_message = str(exception)
416
+
417
+ # Get the stack trace
418
+ exc_traceback = traceback.format_exception(type(exception), exception, exception.__traceback__)
419
+ stack_trace = "".join(exc_traceback)
420
+
421
+ # Create the event attributes
422
+ event_attrs = {
423
+ "exception.type": exc_type,
424
+ "exception.message": exc_message,
425
+ "exception.stacktrace": stack_trace,
426
+ }
427
+
428
+ # Add any additional attributes
429
+ if attributes:
430
+ event_attrs.update(attributes)
431
+
432
+ # Add the event to the span
433
+ self.add_event("exception", event_attrs)
434
+
435
+ # Set the span status to error
436
+ self.set_status(Status(StatusCode.ERROR, exc_message))
437
+
438
+ def copy(self) -> "OtelSpan":
439
+ """
440
+ Create a new OtelSpan instance with the same values as this one.
441
+ Note that this is not a deep copy - mutable objects like attributes, events, and links
442
+ will be shared between the original and the copy.
443
+
444
+ Returns:
445
+ A new OtelSpan instance with the same values
446
+ """
447
+ return OtelSpan(
448
+ name=self._name,
449
+ context=self._context,
450
+ parent=self._parent,
451
+ attributes=self._attributes.copy(),
452
+ events=self._events.copy(),
453
+ links=self._links.copy(),
454
+ kind=self._kind,
455
+ start_time=self._start_time,
456
+ end_time=self._end_time,
457
+ status=self._status,
458
+ resource=self._resource,
459
+ instrumentation_scope=self._instrumentation_scope,
460
+ )
461
+
462
+ @staticmethod
463
+ def _format_context(context: SpanContext) -> dict[str, str]:
464
+ return {
465
+ "trace_id": f"0x{trace_api.format_trace_id(context.trace_id)}",
466
+ "span_id": f"0x{trace_api.format_span_id(context.span_id)}",
467
+ "trace_state": repr(context.trace_state),
468
+ }
469
+
470
+ @staticmethod
471
+ def _format_attributes(attributes: types.Attributes, ) -> dict[str, Any] | None:
472
+ if attributes is not None and not isinstance(attributes, dict):
473
+ return dict(attributes)
474
+ return attributes
475
+
476
+ @staticmethod
477
+ def _format_events(events: Sequence[Event]) -> list[dict[str, Any]]:
478
+ return [{
479
+ "name": event.name,
480
+ "timestamp": util.ns_to_iso_str(event.timestamp),
481
+ "attributes": OtelSpan._format_attributes(event.attributes),
482
+ } for event in events]
483
+
484
+ @staticmethod
485
+ def _format_links(links: Sequence[trace_api.Link]) -> list[dict[str, Any]]:
486
+ return [{
487
+ "context": OtelSpan._format_context(link.context),
488
+ "attributes": OtelSpan._format_attributes(link.attributes),
489
+ } for link in links]
490
+
491
+ def to_json(self, indent: int | None = 4):
492
+ parent_id = None
493
+ if self.parent is not None:
494
+ parent_id = f"0x{trace_api.format_span_id(self.parent.span_id)}" # type: ignore
495
+
496
+ start_time = None
497
+ if self._start_time:
498
+ start_time = util.ns_to_iso_str(self._start_time)
499
+
500
+ end_time = None
501
+ if self._end_time:
502
+ end_time = util.ns_to_iso_str(self._end_time)
503
+
504
+ status = {
505
+ "status_code": str(self._status.status_code.name),
506
+ }
507
+ if self._status.description:
508
+ status["description"] = self._status.description
509
+
510
+ f_span = {
511
+ "name": self._name,
512
+ "context": (self._format_context(self._context) if self._context else None),
513
+ "kind": str(self.kind),
514
+ "parent_id": parent_id,
515
+ "start_time": start_time,
516
+ "end_time": end_time,
517
+ "status": status,
518
+ "attributes": self._format_attributes(self._attributes),
519
+ "events": self._format_events(self._events),
520
+ "links": self._format_links(self._links),
521
+ "resource": json.loads(self.resource.to_json()),
522
+ }
523
+
524
+ return json.dumps(f_span, indent=indent)