Skip to content

Commit 80ba17f

Browse files
authored
feat: enhance tracing system with OpenTelemetry semantic conventions (#1331)
* feat: enhance tracing system with OpenTelemetry semantic conventions and configurable span formats Introduces a major enhancement to the NeMo Guardrails tracing and telemetry infrastructure with support for multiple span formats, OpenTelemetry semantic convention compliance, and privacy-focused content capture controls. The system now supports both legacy and OpenTelemetry-compliant span formats while maintaining backward compatibility. Key changes: - Add configurable span format support (flat/opentelemetry) - Implement OpenTelemetry semantic conventions for GenAI - Add privacy controls for prompt/response content capture - Enhance LLM call tracking with model provider information - Improve span extraction and modeling architecture - Add comprehensive test coverage for new functionality
1 parent a5e8b4c commit 80ba17f

28 files changed

+4804
-587
lines changed

nemoguardrails/actions/llm/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ def _infer_model_name(llm: BaseLanguageModel):
6666
async def llm_call(
6767
llm: BaseLanguageModel,
6868
prompt: Union[str, List[dict]],
69+
model_name: Optional[str] = None,
70+
model_provider: Optional[str] = None,
6971
stop: Optional[List[str]] = None,
7072
custom_callback_handlers: Optional[List[AsyncCallbackHandler]] = None,
7173
) -> str:
@@ -76,7 +78,8 @@ async def llm_call(
7678
llm_call_info = LLMCallInfo()
7779
llm_call_info_var.set(llm_call_info)
7880

79-
llm_call_info.llm_model_name = _infer_model_name(llm)
81+
llm_call_info.llm_model_name = model_name or _infer_model_name(llm)
82+
llm_call_info.llm_provider_name = model_provider
8083

8184
if custom_callback_handlers and custom_callback_handlers != [None]:
8285
all_callbacks = BaseCallbackManager(

nemoguardrails/logging/explain.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ class LLMCallInfo(LLMCallSummary):
5959
default="unknown",
6060
description="The name of the model use for the LLM call.",
6161
)
62+
llm_provider_name: Optional[str] = Field(
63+
default="unknown",
64+
description="The provider of the model used for the LLM call, e.g. 'openai', 'nvidia'.",
65+
)
6266

6367

6468
class ExplainInfo(BaseModel):

nemoguardrails/rails/llm/config.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,12 +358,29 @@ class LogAdapterConfig(BaseModel):
358358
model_config = ConfigDict(extra="allow")
359359

360360

361+
class SpanFormat(str, Enum):
362+
legacy = "legacy"
363+
opentelemetry = "opentelemetry"
364+
365+
361366
class TracingConfig(BaseModel):
362367
enabled: bool = False
363368
adapters: List[LogAdapterConfig] = Field(
364369
default_factory=lambda: [LogAdapterConfig()],
365370
description="The list of tracing adapters to use. If not specified, the default adapters are used.",
366371
)
372+
span_format: str = Field(
373+
default=SpanFormat.opentelemetry,
374+
description="The span format to use. Options are 'legacy' (simple metrics) or 'opentelemetry' (OpenTelemetry semantic conventions).",
375+
)
376+
enable_content_capture: bool = Field(
377+
default=False,
378+
description=(
379+
"Capture prompts and responses (user/assistant/tool message content) in tracing/telemetry events. "
380+
"Disabled by default for privacy and alignment with OpenTelemetry GenAI semantic conventions. "
381+
"WARNING: Enabling this may include PII and sensitive data in your telemetry backend."
382+
),
383+
)
367384

368385

369386
class EmbeddingsCacheConfig(BaseModel):

nemoguardrails/rails/llm/llmrails.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ def __init__(
244244
from nemoguardrails.tracing import create_log_adapters
245245

246246
self._log_adapters = create_log_adapters(config.tracing)
247+
else:
248+
self._log_adapters = None
247249

248250
# We run some additional checks on the config
249251
self._validate_config()
@@ -1167,9 +1169,19 @@ async def generate_async(
11671169
# lazy import to avoid circular dependency
11681170
from nemoguardrails.tracing import Tracer
11691171

1170-
# Create a Tracer instance with instantiated adapters
1172+
span_format = getattr(
1173+
self.config.tracing, "span_format", "opentelemetry"
1174+
)
1175+
enable_content_capture = getattr(
1176+
self.config.tracing, "enable_content_capture", False
1177+
)
1178+
# Create a Tracer instance with instantiated adapters and span configuration
11711179
tracer = Tracer(
1172-
input=messages, response=res, adapters=self._log_adapters
1180+
input=messages,
1181+
response=res,
1182+
adapters=self._log_adapters,
1183+
span_format=span_format,
1184+
enable_content_capture=enable_content_capture,
11731185
)
11741186
await tracer.export_async()
11751187

nemoguardrails/tracing/__init__.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,24 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
from .tracer import InteractionLog, Tracer, create_log_adapters
16+
from .interaction_types import InteractionLog, InteractionOutput
17+
from .span_extractors import (
18+
SpanExtractor,
19+
SpanExtractorV1,
20+
SpanExtractorV2,
21+
create_span_extractor,
22+
)
23+
from .spans import SpanEvent, SpanLegacy, SpanOpentelemetry
24+
from .tracer import Tracer, create_log_adapters
25+
26+
___all__ = [
27+
SpanExtractor,
28+
SpanExtractorV1,
29+
SpanExtractorV2,
30+
create_span_extractor,
31+
Tracer,
32+
create_log_adapters,
33+
SpanEvent,
34+
SpanLegacy,
35+
SpanOpentelemetry,
36+
]

nemoguardrails/tracing/adapters/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from abc import ABC, abstractmethod
1717
from typing import Optional
1818

19-
from nemoguardrails.eval.models import InteractionLog
19+
from nemoguardrails.tracing.interaction_types import InteractionLog
2020

2121

2222
class InteractionLogAdapter(ABC):

nemoguardrails/tracing/adapters/filesystem.py

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
from nemoguardrails.tracing import InteractionLog
2525

2626
from nemoguardrails.tracing.adapters.base import InteractionLogAdapter
27+
from nemoguardrails.tracing.span_formatting import (
28+
format_span_for_filesystem,
29+
get_schema_version_for_filesystem,
30+
)
2731

2832

2933
class FileSystemAdapter(InteractionLogAdapter):
@@ -38,56 +42,46 @@ def __init__(self, filepath: Optional[str] = None):
3842

3943
def transform(self, interaction_log: "InteractionLog"):
4044
"""Transforms the InteractionLog into a JSON string."""
41-
spans = []
42-
43-
for span_data in interaction_log.trace:
44-
span_dict = {
45-
"name": span_data.name,
46-
"span_id": span_data.span_id,
47-
"parent_id": span_data.parent_id,
48-
"trace_id": interaction_log.id,
49-
"start_time": span_data.start_time,
50-
"end_time": span_data.end_time,
51-
"duration": span_data.duration,
52-
"metrics": span_data.metrics,
53-
}
54-
spans.append(span_dict)
45+
spans = [
46+
format_span_for_filesystem(span_data) for span_data in interaction_log.trace
47+
]
48+
49+
if not interaction_log.trace:
50+
schema_version = None
51+
else:
52+
schema_version = get_schema_version_for_filesystem(interaction_log.trace[0])
5553

5654
log_dict = {
55+
"schema_version": schema_version,
5756
"trace_id": interaction_log.id,
5857
"spans": spans,
5958
}
6059

61-
with open(self.filepath, "a") as f:
62-
f.write(json.dumps(log_dict, indent=2) + "\n")
60+
with open(self.filepath, "a", encoding="utf-8") as f:
61+
f.write(json.dumps(log_dict) + "\n")
6362

6463
async def transform_async(self, interaction_log: "InteractionLog"):
6564
try:
6665
import aiofiles
6766
except ImportError:
6867
raise ImportError(
69-
"aiofiles is required for async file writing. Please install it using `pip install aiofiles"
68+
"aiofiles is required for async file writing. Please install it using `pip install aiofiles`"
7069
)
7170

72-
spans = []
73-
74-
for span_data in interaction_log.trace:
75-
span_dict = {
76-
"name": span_data.name,
77-
"span_id": span_data.span_id,
78-
"parent_id": span_data.parent_id,
79-
"trace_id": interaction_log.id,
80-
"start_time": span_data.start_time,
81-
"end_time": span_data.end_time,
82-
"duration": span_data.duration,
83-
"metrics": span_data.metrics,
84-
}
85-
spans.append(span_dict)
71+
spans = [
72+
format_span_for_filesystem(span_data) for span_data in interaction_log.trace
73+
]
74+
75+
if not interaction_log.trace:
76+
schema_version = None
77+
else:
78+
schema_version = get_schema_version_for_filesystem(interaction_log.trace[0])
8679

8780
log_dict = {
81+
"schema_version": schema_version,
8882
"trace_id": interaction_log.id,
8983
"spans": spans,
9084
}
9185

92-
async with aiofiles.open(self.filepath, "a") as f:
93-
await f.write(json.dumps(log_dict, indent=2) + "\n")
86+
async with aiofiles.open(self.filepath, "a", encoding="utf-8") as f:
87+
await f.write(json.dumps(log_dict) + "\n")

0 commit comments

Comments
 (0)