Exporting Guardrails Logs to OpenTelemetry#
The NeMo Guardrails library emits operational logs through Python’s standard logging module. When you have OpenTelemetry tracing configured, you can forward those log records into the same backend as your traces with a few lines of application code. Records emitted inside an active guardrails span automatically carry the span’s trace_id and span_id, so every log line correlates to the request that produced it.
This page covers the setup. For plain Python logging (verbose mode, explain, generation options), refer to Logging and Debugging Guardrails Generated Responses.
API and SDK Responsibilities#
The NeMo Guardrails library follows the OpenTelemetry library-instrumentation pattern.
The library depends on the OpenTelemetry API only. It creates spans, emits log records, and otherwise participates in whatever OTEL pipeline the host application provides.
The host application owns the SDK. Configuring a
TracerProvider, aLoggerProvider, exporters, and attaching handlers to Python’sloggingtree are all the application’s responsibility.
This split is deliberate. It lets the NeMo Guardrails library stay decoupled from SDK-version churn, avoids the library injecting itself into a host’s observability stack without opt-in, and gives applications full control over where their telemetry is exported.
The three-line recipe below is therefore a user-side setup, not something the library does for you.
Prerequisites#
Before enabling log export, install the OpenTelemetry SDK as described in Installation. The OpenTelemetry log components live in the same opentelemetry-sdk package that powers trace export. No additional installation is required for in-process log forwarding.
For exporting logs to an external backend over OTLP, install the OTLP exporter.
pip install opentelemetry-exporter-otlp
Attach the Logging Handler#
Configure a LoggerProvider first, then attach the handler. The surrounding SDK setup appears in the full example below. The core of the bridge is three lines.
Important
Configure the LoggerProvider through set_logger_provider(...) before you call addHandler(LoggingHandler()). The handler resolves its LoggerProvider on first emit and caches the result. If no provider is set by then, the SDK hands back a no-op logger and every forwarded record is silently discarded. The SDK raises no error, and calling set_logger_provider(...) later does not recover the handler.
import logging
from opentelemetry.sdk._logs import LoggingHandler
logging.getLogger("nemoguardrails").addHandler(LoggingHandler())
Each line does the following:
logging.getLogger("nemoguardrails")selects the logger namespace that catches most records emitted by the NeMo Guardrails library. Submodules that uselogging.getLogger(__name__)inherit this handler. Verbose mode (nemoguardrails.logging.verbose) is the known exception. It writes to the root logger, so attach the handler to the root logger as well if you need verbose output forwarded.LoggingHandler()is an OpenTelemetry-providedlogging.Handlersubclass that converts each PythonLogRecordinto an OTEL log record. On first emit it resolves the activeLoggerProviderthroughget_logger_provider(), caches the resulting logger, and attaches trace context automatically..addHandler(...)attaches the handler. From this point forward, every record the NeMo Guardrails library emits flows to both the host’s existing handlers (console, files, and so on) and the OpenTelemetry pipeline, provided aLoggerProviderwas configured before this call.
This is additive. Your existing Python logging configuration continues to work unchanged. OpenTelemetry export happens alongside, not instead.
Full Example with Traces and Logs#
This program configures a TracerProvider and a LoggerProvider, both exporting to the console, then runs a guardrails request so you can see correlated spans and log records.
import logging
from opentelemetry import trace
from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogRecordExporter # ConsoleLogExporter on versions earlier than 1.39.0
from nemoguardrails import LLMRails, RailsConfig
# Application-owned SDK setup
resource = Resource.create({"service.name": "guardrails-log-demo"})
# 1. Traces → console
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(tracer_provider)
# 2. Logs → console
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(ConsoleLogRecordExporter()))
set_logger_provider(logger_provider)
# 3. Forward nemoguardrails log records into the OTEL pipeline
logging.getLogger("nemoguardrails").addHandler(LoggingHandler())
# Guardrails configuration
config_yaml = """
models:
- type: main
engine: openai
model: gpt-4o-mini
tracing:
enabled: true
adapters:
- name: OpenTelemetry
"""
config = RailsConfig.from_content(yaml_content=config_yaml)
rails = LLMRails(config)
response = rails.generate(messages=[{"role": "user", "content": "Hello!"}])
print(f"Response: {response}")
Running this script prints both the span tree and the log records to your console. Records emitted while the guardrails request is in flight carry trace_id and span_id fields that match the enclosing span.
Exporting to a Backend#
The log-record processor in the example above can target any OpenTelemetry log exporter. The following example uses an OTLP collector.
# Private module. Refer to "Experimental SDK surface" under Considerations.
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
otlp_log_exporter = OTLPLogExporter(endpoint="http://localhost:4317", insecure=True)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(otlp_log_exporter))
The OpenTelemetry Collector then forwards the records to any compatible backend, such as Loki, Datadog, New Relic, or Elastic. Refer to the OpenTelemetry Registry for the list.
What the Exported Records Contain#
Each forwarded LogRecord becomes an OTEL log record with the following fields populated automatically.
Field |
Description |
|---|---|
Body |
Contains the formatted log message. |
Severity |
Records |
Timestamp |
Records the record’s emit time. |
Trace context |
Carries the |
Code attributes |
Include |
Log records emitted outside any guardrails request (startup, engine registration, teardown) still flow through, but their trace_id / span_id are zero because there is no active span.
Considerations#
- Experimental SDK surface
Both the
opentelemetry.sdk._logsmodule and the OTLP log exporter atopentelemetry.exporter.otlp.proto.grpc._log_exporterare still under active development in the OpenTelemetry Python ecosystem. The underscore prefix on both paths denotes a non-stable API. Pin youropentelemetry-sdkandopentelemetry-exporter-otlpversions in production and review release notes before upgrading.
- Privacy
Guardrails log messages include user inputs and rail decisions. Before exporting to a third-party backend, review whether the records may contain PII and whether your retention and redaction policies cover them.
- Performance
At high log volumes or DEBUG level, log export can add measurable overhead. Use
BatchLogRecordProcessor(as shown) rather than the synchronousSimpleLogRecordProcessorin production, and consider filtering at the logger level (logging.getLogger("nemoguardrails").setLevel(logging.INFO)) to limit what crosses the bridge.
- Interaction with
propagate=False If your application calls
nemoguardrails.guardrails.configure_logging()on a freshly initialized logger, that helper setspropagate=Falseon thenemoguardrails.guardrailslogger to prevent duplicate console output. The flag is only set on the first call, when no handlers exist yet. Records from submodules undernemoguardrails.guardrails.*will then not reach the handler attached tonemoguardrails. To capture them, attach the handler tonemoguardrails.guardrailsinstead of (or in addition to)nemoguardrails.
- Interaction with