Exporting Guardrails Logs to OpenTelemetry

View as Markdown

The NVIDIA 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.

API and SDK Responsibilities

The library follows the OpenTelemetry library-instrumentation pattern.

  • The library depends on the OpenTelemetry API only. It creates spans, emits log records, and participates in whatever OTEL pipeline the host application provides.
  • The host application owns the SDK. Configuring a TracerProvider, a LoggerProvider, exporters, and attaching handlers to Python’s logging tree are all the application’s responsibility.

This split is deliberate. It lets the library stay decoupled from SDK version churn, avoids injecting the library 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 OpenTelemetry: 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.

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.

1import logging
2from opentelemetry.sdk._logs import LoggingHandler
3
4logging.getLogger("nemoguardrails").addHandler(LoggingHandler())

Each line does the following:

  • logging.getLogger("nemoguardrails") selects the logger namespace that catches most records emitted by the library. Submodules that use logging.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-provided logging.Handler subclass that converts each Python LogRecord into an OTEL log record. On first emit it resolves the active LoggerProvider through get_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 a LoggerProvider was 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.

1import logging
2
3from opentelemetry import trace
4from opentelemetry._logs import set_logger_provider
5from opentelemetry.sdk.resources import Resource
6from opentelemetry.sdk.trace import TracerProvider
7from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
8from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
9from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogRecordExporter # ConsoleLogExporter on versions earlier than 1.39.0
10
11from nemoguardrails import LLMRails, RailsConfig
12
13# Application-owned SDK setup
14resource = Resource.create({"service.name": "guardrails-log-demo"})
15
16# 1. Traces → console
17tracer_provider = TracerProvider(resource=resource)
18tracer_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
19trace.set_tracer_provider(tracer_provider)
20
21# 2. Logs → console
22logger_provider = LoggerProvider(resource=resource)
23logger_provider.add_log_record_processor(BatchLogRecordProcessor(ConsoleLogRecordExporter()))
24set_logger_provider(logger_provider)
25
26# 3. Forward nemoguardrails log records into the OTEL pipeline
27logging.getLogger("nemoguardrails").addHandler(LoggingHandler())
28
29# Guardrails configuration
30config_yaml = """
31models:
32 - type: main
33 engine: openai
34 model: gpt-4o-mini
35
36tracing:
37 enabled: true
38 adapters:
39 - name: OpenTelemetry
40"""
41
42config = RailsConfig.from_content(yaml_content=config_yaml)
43rails = LLMRails(config)
44
45response = rails.generate(messages=[{"role": "user", "content": "Hello!"}])
46print(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.

1# Private module. Refer to "Experimental SDK surface" under Considerations.
2from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
3
4otlp_log_exporter = OTLPLogExporter(endpoint="http://localhost:4317", insecure=True)
5logger_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.

FieldDescription
BodyContains the formatted log message.
SeverityRecords severity_text (INFO, DEBUG, ERROR, and so on) and severity_number.
TimestampRecords the record’s emit time.
Trace contextCarries the trace_id and span_id of the active span when the record was emitted. The values are zero when no span is active.
Code attributesInclude code.file.path, code.function.name, and code.line.number derived from the Python LogRecord.

Log records emitted outside any guardrails request, such as startup, engine registration, or teardown, still flow through. Their trace_id and span_id are zero because there is no active span.

Considerations

  • Experimental SDK surface : Both the opentelemetry.sdk._logs module and the OTLP log exporter at opentelemetry.exporter.otlp.proto.grpc._log_exporter are still under active development in the OpenTelemetry Python ecosystem. The underscore prefix on both paths denotes a non-stable API. Pin your opentelemetry-sdk and opentelemetry-exporter-otlp versions 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 synchronous SimpleLogRecordProcessor in 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 sets propagate=False on the nemoguardrails.guardrails logger to prevent duplicate console output. The flag is only set on the first call, when no handlers exist yet. Records from submodules under nemoguardrails.guardrails.* will then not reach the handler attached to nemoguardrails. To capture them, attach the handler to nemoguardrails.guardrails instead of (or in addition to) nemoguardrails.
  • OpenTelemetry covers SDK installation and trace export setup.
  • Quick Start provides a minimal tracing setup with the OpenTelemetry SDK.
  • Logging covers Python logging, verbose mode, and the log generation option for in-process debugging.