Skip to content

Commit 96626bc

Browse files
Pouyanpiwinstonallo
authored andcommitted
feat(tracing)!: update tracing to use otel api (NVIDIA-NeMo#1269)
* feat(tracing)!: migrate OpenTelemetry adapter to use otel API Refactored the OpenTelemetry tracing adapter to follow OpenTelemetry library best practices. The adapter now uses only the OpenTelemetry API, does not modify global state, and relies on the application to configure the SDK and tracer provider. Removed legacy exporter and span processor configuration from the adapter. Updated tests to reflect the new initialization logic and ensure backward compatibility with old config parameters. Signed-off-by: Pouyan <13303554+Pouyanpi@users.noreply.github.com> Add handling for config directory with .yml/.yaml extension Fixes `config.yml/` directory being accepted as a valid path and attempted to open as a file, resulting in a `ENOPERM` Signed-off-by: Arthur Bied-Charreton <136271426+winstonallo@users.noreply.github.com> Add test config with .yml extension Add test verifying that general.yml/ is interpreted as a directory Run pre-commit Simplify condition in `from_path` Add test config with .yml extension Add test verifying that general.yml/ is interpreted as a directory Simplify condition in `from_path` Add test config with .yml extension Add test verifying that general.yml/ is interpreted as a directory Simplify condition in `from_path`
1 parent ef97795 commit 96626bc

File tree

10 files changed

+862
-170
lines changed

10 files changed

+862
-170
lines changed

examples/configs/tracing/README.md

Lines changed: 397 additions & 23 deletions
Large diffs are not rendered by default.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env python3
2+
# SPDX-FileCopyrightText: Copyright (c) 2023 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+
Complete working example of NeMo Guardrails with OpenTelemetry tracing.
19+
20+
This example uses the ConsoleSpanExporter so you can see traces immediately
21+
without needing to set up any external infrastructure.
22+
23+
Usage:
24+
pip install nemoguardrails[tracing] opentelemetry-sdk
25+
python working_example.py
26+
"""
27+
28+
from opentelemetry import trace
29+
from opentelemetry.sdk.resources import Resource
30+
from opentelemetry.sdk.trace import TracerProvider
31+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
32+
33+
from nemoguardrails import LLMRails, RailsConfig
34+
35+
36+
def setup_opentelemetry():
37+
"""Configure OpenTelemetry SDK with console output."""
38+
39+
print("Setting up OpenTelemetry...")
40+
41+
# configure resource (metadata about your service)
42+
resource = Resource.create(
43+
{
44+
"service.name": "nemo-guardrails-example",
45+
"service.version": "1.0.0",
46+
"deployment.environment": "development",
47+
},
48+
schema_url="https://opentelemetry.io/schemas/1.26.0",
49+
)
50+
51+
# set up the tracer provider
52+
tracer_provider = TracerProvider(resource=resource)
53+
trace.set_tracer_provider(tracer_provider)
54+
55+
# configure console exporter (prints traces to stdout)
56+
console_exporter = ConsoleSpanExporter()
57+
span_processor = BatchSpanProcessor(console_exporter)
58+
tracer_provider.add_span_processor(span_processor)
59+
60+
print(" OpenTelemetry configured with ConsoleSpanExporter")
61+
print(" Traces will be printed to the console below\n")
62+
63+
64+
def create_guardrails_config():
65+
"""Create a simple guardrails configuration with tracing enabled."""
66+
67+
return RailsConfig.from_content(
68+
colang_content="""
69+
define user express greeting
70+
"hello"
71+
"hi"
72+
"hey"
73+
74+
define flow
75+
user express greeting
76+
bot express greeting
77+
78+
define bot express greeting
79+
"Hello! I'm a guardrails-enabled assistant."
80+
"Hi there! How can I help you today?"
81+
""",
82+
config={
83+
"models": [
84+
{
85+
"type": "main",
86+
"engine": "openai",
87+
"model": "gpt-4o",
88+
}
89+
],
90+
"tracing": {"enabled": True, "adapters": [{"name": "OpenTelemetry"}]},
91+
# Note: The following old-style configuration is deprecated and will be ignored:
92+
# "tracing": {
93+
# "enabled": True,
94+
# "adapters": [{
95+
# "name": "OpenTelemetry",
96+
# "service_name": "my-service", # DEPRECATED - configure in Resource
97+
# "exporter": "console", # DEPRECATED - configure SDK
98+
# "resource_attributes": { # DEPRECATED - configure in Resource
99+
# "env": "production"
100+
# }
101+
# }]
102+
# }
103+
},
104+
)
105+
106+
107+
def main():
108+
"""Main function demonstrating NeMo Guardrails with OpenTelemetry."""
109+
print(" NeMo Guardrails + OpenTelemetry Example")
110+
print("=" * 50)
111+
112+
# step 1: configure OpenTelemetry (APPLICATION'S RESPONSIBILITY)
113+
setup_opentelemetry()
114+
115+
# step 2: create guardrails configuration
116+
print(" Creating guardrails configuration...")
117+
config = create_guardrails_config()
118+
rails = LLMRails(config)
119+
print(" Guardrails configured with tracing enabled\n")
120+
121+
# step 3: test the guardrails with tracing
122+
print(" Testing guardrails (traces will appear below)...")
123+
print("-" * 50)
124+
125+
# this will create spans that get exported to the console
126+
response = rails.generate(
127+
messages=[{"role": "user", "content": "What can you do?"}]
128+
)
129+
130+
print(f"User: What can you do?")
131+
print(f"Bot: {response.response}")
132+
print("-" * 50)
133+
134+
# force export any remaining spans
135+
print("\n Flushing remaining traces...")
136+
trace.get_tracer_provider().force_flush(1000)
137+
138+
print("\n Example completed!")
139+
print("\n Tips:")
140+
print(" - Traces were printed above (look for JSON output)")
141+
print(" - In production, replace ConsoleSpanExporter with OTLP/Jaeger")
142+
print(" - The spans show the internal flow of guardrails processing")
143+
144+
145+
if __name__ == "__main__":
146+
main()

nemoguardrails/rails/llm/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1468,7 +1468,7 @@ def from_path(
14681468
"""
14691469
# If the config path is a file, we load the YAML content.
14701470
# Otherwise, if it's a folder, we iterate through all files.
1471-
if config_path.endswith(".yaml") or config_path.endswith(".yml"):
1471+
if os.path.isfile(config_path) and config_path.endswith((".yaml", ".yml")):
14721472
with open(config_path) as f:
14731473
raw_config = yaml.safe_load(f.read())
14741474

nemoguardrails/tracing/adapters/opentelemetry.py

Lines changed: 123 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -13,78 +13,156 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
from __future__ import annotations
16+
"""
17+
OpenTelemetry Adapter for NeMo Guardrails
18+
19+
This adapter follows OpenTelemetry best practices for libraries:
20+
- Uses only the OpenTelemetry API (not SDK)
21+
- Does not modify global state
22+
- Relies on the application to configure the SDK
23+
24+
Usage:
25+
Applications using NeMo Guardrails with OpenTelemetry should configure
26+
the OpenTelemetry SDK before using this adapter:
27+
28+
```python
29+
from opentelemetry import trace
30+
from opentelemetry.sdk.trace import TracerProvider
31+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
32+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
33+
34+
# application configures the SDK
35+
trace.set_tracer_provider(TracerProvider())
36+
tracer_provider = trace.get_tracer_provider()
37+
38+
exporter = OTLPSpanExporter(endpoint="http://localhost:4317")
39+
span_processor = BatchSpanProcessor(exporter)
40+
tracer_provider.add_span_processor(span_processor)
41+
42+
# now NeMo Guardrails can use the configured tracer
43+
config = RailsConfig.from_content(
44+
config={
45+
"tracing": {
46+
"enabled": True,
47+
"adapters": [{"name": "OpenTelemetry"}]
48+
}
49+
}
50+
)
51+
```
52+
"""
1753

18-
from typing import TYPE_CHECKING, Dict, Optional, Type
54+
from __future__ import annotations
1955

20-
from opentelemetry.sdk.trace.export import SpanExporter
56+
import warnings
57+
from importlib.metadata import version
58+
from typing import TYPE_CHECKING, Optional, Type
2159

2260
if TYPE_CHECKING:
2361
from nemoguardrails.tracing import InteractionLog
2462
try:
2563
from opentelemetry import trace
26-
from opentelemetry.sdk.resources import Attributes, Resource
27-
from opentelemetry.sdk.trace import SpanProcessor, TracerProvider
28-
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
64+
from opentelemetry.trace import NoOpTracerProvider
2965

3066
except ImportError:
3167
raise ImportError(
32-
"opentelemetry is not installed. Please install it using `pip install opentelemetry-api opentelemetry-sdk`."
68+
"OpenTelemetry API is not installed. Please install NeMo Guardrails with tracing support: "
69+
"`pip install nemoguardrails[tracing]` or install the API directly: `pip install opentelemetry-api`."
3370
)
3471

3572
from nemoguardrails.tracing.adapters.base import InteractionLogAdapter
3673

37-
# Global dictionary to store registered exporters
38-
_exporter_name_cls_map: Dict[str, Type[SpanExporter]] = {
39-
"console": ConsoleSpanExporter,
40-
}
41-
42-
43-
def register_otel_exporter(name: str, exporter_cls: Type[SpanExporter]):
44-
"""Register a new exporter."""
74+
# DEPRECATED: global dictionary to store registered exporters
75+
# will be removed in v0.16.0
76+
_exporter_name_cls_map: dict[str, Type] = {}
77+
78+
79+
def register_otel_exporter(name: str, exporter_cls: Type):
80+
"""Register a new exporter.
81+
82+
Args:
83+
name: The name to register the exporter under.
84+
exporter_cls: The exporter class to register.
85+
86+
Deprecated:
87+
This function is deprecated and will be removed in version 0.16.0.
88+
Please configure OpenTelemetry exporters directly in your application code.
89+
See the migration guide at:
90+
https://github.com/NVIDIA/NeMo-Guardrails/blob/main/examples/configs/tracing/README.md#migration-guide
91+
"""
92+
warnings.warn(
93+
"register_otel_exporter is deprecated and will be removed in version 0.16.0. "
94+
"Please configure OpenTelemetry exporters directly in your application code. "
95+
"See the migration guide at: "
96+
"https://github.com/NVIDIA/NeMo-Guardrails/blob/develop/examples/configs/tracing/README.md#migration-guide",
97+
DeprecationWarning,
98+
stacklevel=2,
99+
)
45100
_exporter_name_cls_map[name] = exporter_cls
46101

47102

48103
class OpenTelemetryAdapter(InteractionLogAdapter):
104+
"""
105+
OpenTelemetry adapter that follows library best practices.
106+
107+
This adapter uses only the OpenTelemetry API and relies on the application
108+
to configure the SDK. It does not modify global state or create its own
109+
tracer provider.
110+
"""
111+
49112
name = "OpenTelemetry"
50113

51114
def __init__(
52115
self,
53-
service_name="nemo_guardrails_service",
54-
span_processor: Optional[SpanProcessor] = None,
55-
exporter: Optional[str] = None,
56-
exporter_cls: Optional[SpanExporter] = None,
57-
resource_attributes: Optional[Attributes] = None,
116+
service_name: str = "nemo_guardrails",
58117
**kwargs,
59118
):
60-
resource_attributes = resource_attributes or {}
61-
resource = Resource.create(
62-
{"service.name": service_name, **resource_attributes}
63-
)
64-
65-
if exporter_cls and exporter:
66-
raise ValueError(
67-
"Only one of 'exporter' or 'exporter_name' should be provided"
119+
"""
120+
Initialize the OpenTelemetry adapter.
121+
122+
Args:
123+
service_name: Service name for instrumentation scope (not used for resource)
124+
**kwargs: Additional arguments (for backward compatibility)
125+
126+
Note:
127+
Applications must configure the OpenTelemetry SDK before using this adapter.
128+
The adapter will use the globally configured tracer provider.
129+
"""
130+
# check for deprecated parameters and warn users
131+
deprecated_params = [
132+
"exporter",
133+
"exporter_cls",
134+
"resource_attributes",
135+
"span_processor",
136+
]
137+
used_deprecated = [param for param in deprecated_params if param in kwargs]
138+
139+
if used_deprecated:
140+
warnings.warn(
141+
f"OpenTelemetry configuration parameters {used_deprecated} in YAML/config are deprecated "
142+
"and will be ignored. Please configure OpenTelemetry in your application code. "
143+
"See the migration guide at: "
144+
"https://github.com/NVIDIA/NeMo-Guardrails/blob/main/examples/configs/tracing/README.md#migration-guide",
145+
DeprecationWarning,
146+
stacklevel=2,
68147
)
69-
# Set up the tracer provider
70-
provider = TracerProvider(resource=resource)
71-
72-
# Init the span processor and exporter
73-
exporter_cls = None
74-
if exporter:
75-
exporter_cls = self.get_exporter(exporter, **kwargs)
76148

77-
if exporter_cls is None:
78-
exporter_cls = ConsoleSpanExporter()
79-
80-
if span_processor is None:
81-
span_processor = BatchSpanProcessor(exporter_cls)
82-
83-
provider.add_span_processor(span_processor)
84-
trace.set_tracer_provider(provider)
149+
# validate that OpenTelemetry is properly configured
150+
provider = trace.get_tracer_provider()
151+
if provider is None or isinstance(provider, NoOpTracerProvider):
152+
warnings.warn(
153+
"No OpenTelemetry TracerProvider configured. Traces will not be exported. "
154+
"Please configure OpenTelemetry in your application code before using NeMo Guardrails. "
155+
"See setup guide at: "
156+
"https://github.com/NVIDIA/NeMo-Guardrails/blob/main/examples/configs/tracing/README.md#opentelemetry-setup",
157+
UserWarning,
158+
stacklevel=2,
159+
)
85160

86-
self.tracer_provider = provider
87-
self.tracer = trace.get_tracer(__name__)
161+
self.tracer = trace.get_tracer(
162+
service_name,
163+
instrumenting_library_version=version("nemoguardrails"),
164+
schema_url="https://opentelemetry.io/schemas/1.26.0",
165+
)
88166

89167
def transform(self, interaction_log: "InteractionLog"):
90168
"""Transforms the InteractionLog into OpenTelemetry spans."""
@@ -139,20 +217,3 @@ def _create_span(
139217
span.set_attribute("duration", span_data.duration)
140218

141219
spans[span_data.span_id] = span
142-
143-
@staticmethod
144-
def get_exporter(exporter: str, **kwargs) -> SpanExporter:
145-
if exporter == "zipkin":
146-
try:
147-
from opentelemetry.exporter.zipkin.json import ZipkinExporter
148-
149-
_exporter_name_cls_map["zipkin"] = ZipkinExporter
150-
except ImportError:
151-
raise ImportError(
152-
"The opentelemetry-exporter-zipkin package is not installed. Please install it using 'pip install opentelemetry-exporter-zipkin'."
153-
)
154-
155-
exporter_cls = _exporter_name_cls_map.get(exporter)
156-
if not exporter_cls:
157-
raise ValueError(f"Unknown exporter: {exporter}")
158-
return exporter_cls(**kwargs)

0 commit comments

Comments
 (0)