Skip to content

Commit d65ea80

Browse files
feat: Add OTel SDK setup
1 parent a07460b commit d65ea80

7 files changed

Lines changed: 376 additions & 93 deletions

File tree

Dockerfile.server

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ ARG PYYAML_VERSION=6.0.3
77
ARG GUARDRAILS_PROFILE=opensource
88

99
RUN dnf install -y --nodocs --setopt=install_weak_deps=False \
10-
gcc \
11-
gcc-c++ \
12-
git && \
10+
gcc \
11+
gcc-c++ \
12+
git && \
1313
pip install --no-cache-dir \
14-
pyyaml==${PYYAML_VERSION} && \
14+
pyyaml==${PYYAML_VERSION} && \
1515
dnf clean all && \
1616
rm -rf /var/cache/dnf /tmp/* /var/tmp/*
1717

@@ -20,7 +20,7 @@ COPY nemoguardrails/ ./nemoguardrails/
2020
COPY scripts/ ./scripts/
2121
COPY examples/bots/ ./examples/bots/
2222

23-
RUN chmod +x ./scripts/entrypoint.sh
23+
RUN chmod +x ./scripts/otel/entrypoint.py
2424
RUN python3 ./scripts/filter_guardrails.py ./scripts/provider-list.yaml $GUARDRAILS_PROFILE
2525

2626
ENV HF_HOME=/app/.cache/huggingface \
@@ -32,8 +32,8 @@ ENV HF_HOME=/app/.cache/huggingface \
3232
# Create virtual environment and install dependencies
3333
RUN python3 -m venv /app/.venv && \
3434
/app/.venv/bin/pip install --no-cache-dir \
35-
--extra-index-url https://download.pytorch.org/whl/cpu \
36-
-r requirements.txt && \
35+
--extra-index-url https://download.pytorch.org/whl/cpu \
36+
-r requirements.txt && \
3737
/app/.venv/bin/pip install --no-cache-dir --no-deps . && \
3838
rm -rf /root/.cache/pip /tmp/* /var/tmp/*
3939

@@ -58,7 +58,7 @@ COPY --from=build /app /app
5858
RUN rm -f /etc/security/namespace.conf /usr/lib64/security/pam_namespace.so || true && \
5959
chgrp -R 0 /app && \
6060
chmod -R g=u /app && \
61-
chmod +x /app/scripts/entrypoint.sh
61+
chmod +x /app/scripts/otel/entrypoint.py
6262

6363
USER 1001
6464

@@ -73,4 +73,4 @@ ENV HF_HOME=/app/.cache/huggingface \
7373
DO_NOT_TRACK=1
7474

7575
EXPOSE 8000
76-
ENTRYPOINT ["./scripts/entrypoint.sh"]
76+
ENTRYPOINT ["./scripts/otel/entrypoint.py"]

pyproject.toml

Lines changed: 27 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description = "NeMo Guardrails is an open-source toolkit for easily adding progr
44
authors = ["NVIDIA <nemoguardrails@nvidia.com>"]
55
license = "LICENSE.md"
66
readme = "README.md"
7-
version = "0.21.0"
7+
version = "0.18.0"
88
packages = [{ include = "nemoguardrails" }]
99

1010

@@ -34,11 +34,11 @@ classifiers = [
3434
]
3535

3636
[tool.poetry.urls]
37-
homepage = "https://github.com/NVIDIA-NeMo/Guardrails"
38-
issues = "https://github.com/NVIDIA-NeMo/Guardrails/issues"
39-
changelog = "https://github.com/NVIDIA-NeMo/Guardrails/blob/develop/CHANGELOG.md"
37+
homepage = "https://github.com/NVIDIA/nemo-guardrails"
38+
issues = "https://github.com/NVIDIA/nemo-guardrails/issues"
39+
changelog = "https://github.com/NVIDIA/NeMo-Guardrails/blob/develop/CHANGELOG.md"
4040
documentation = "https://docs.nvidia.com/nemo/guardrails/"
41-
repository = "https://github.com/NVIDIA-NeMo/Guardrails"
41+
repository = "https://github.com/NVIDIA/NeMo-Guardrails"
4242

4343

4444
[tool.poetry.scripts]
@@ -47,15 +47,9 @@ nemoguardrails = "nemoguardrails.__main__:app"
4747
[tool.poetry.dependencies]
4848
python = ">=3.10,<3.14"
4949
aiohttp = ">=3.10.11"
50-
aiohttp-retry = ">=2.9.0"
5150
annoy = ">=1.17.3"
5251
fastapi = ">=0.103.0,"
53-
fastembed = [{ version = ">=0.2.2", python = ">=3.10,<3.14" }]
54-
# onnxruntime 1.24+ dropped Python 3.10 wheels; cap it for that version
55-
onnxruntime = [
56-
{ version = ">=1.17.0,<1.24", python = ">=3.10,<3.11" },
57-
{ version = ">=1.17.0", python = ">=3.11" },
58-
]
52+
fastembed = [{ version = ">=0.8.0", python = ">=3.10,<3.14" }]
5953
httpx = ">=0.24.1"
6054
jinja2 = ">=3.1.6"
6155
langchain = ">=0.2.14,<2.0.0"
@@ -77,25 +71,26 @@ starlette = ">=0.49.1"
7771
typer = ">=0.8"
7872
uvicorn = ">=0.23"
7973
watchdog = ">=3.0.0,"
80-
openai = { version = ">=1.0.0,<3.0.0", optional = true }
8174

8275
# tracing
8376
opentelemetry-api = { version = ">=1.27.0,<2.0.0", optional = true }
8477
aiofiles = { version = ">=24.1.0", optional = true }
8578

86-
# otel auto-instrumentation (fork-specific)
87-
opentelemetry-distro = { version = ">=0.48b0", optional = true }
79+
# otel (server observability entrypoint)
80+
opentelemetry-sdk = { version = ">=1.27.0,<2.0.0", optional = true }
8881
opentelemetry-exporter-otlp = { version = ">=1.27.0,<2.0.0", optional = true }
8982
opentelemetry-instrumentation-fastapi = { version = ">=0.48b0", optional = true }
90-
opentelemetry-instrumentation-httpx = { version = ">=0.48b0", optional = true }
83+
opentelemetry-exporter-prometheus = { version = ">=0.48b0", optional = true }
84+
prometheus-fastapi-instrumentator = { version = ">=0.9.0", optional = true }
85+
pydantic-settings = { version = ">=2.0.0", optional = true }
9186

9287
# openai
9388
langchain-openai = { version = ">=0.1.0", optional = true }
9489

9590
# eval
9691
tqdm = { version = ">=4.65,<5.0", optional = true }
9792
streamlit = { version = "^1.37.0", optional = true, python = ">=3.10,<3.14" }
98-
tornado = { version = ">=6.5.0", optional = true, python = ">=3.10,<3.14" }
93+
tornado = { version = "6.5.5", optional = true, python = ">=3.10,<3.14" }
9994
pandas = { version = ">=1.4.0,<3", optional = true }
10095
numpy = [
10196
{ version = ">=1.21", python = ">=3.10,<3.12" },
@@ -121,25 +116,30 @@ yara-python = { version = "^4.5.1", optional = true }
121116
# models - optional dependencies for various embedding and NLP models
122117
torch = { version = ">=2.0.0", optional = true, source = "pytorch-cpu" }
123118
spacy = { version = ">=3.4.4,<4.0.0,!=3.7.0", python = "<3.14", optional = true }
124-
nltk = { version = ">=3.8", optional = true }
119+
nltk = { version = ">=3.9.4", optional = true }
125120
sentence-transformers = { version = ">=2.2.0", optional = true }
126121
einops = { version = ">=0.6.0", optional = true }
127-
128-
# multilingual content safety - language detection
129-
fast-langdetect = { version = ">=1.0.0", optional = true }
122+
python-dotenv = "^1.2.2"
123+
pillow = "^12.2.0"
130124

131125
[tool.poetry.extras]
132126
sdd = ["presidio-analyzer", "presidio-anonymizer"]
133127
eval = ["tqdm", "numpy", "streamlit", "tornado"]
134128
openai = ["langchain-openai"]
135129
gcp = ["google-cloud-language"]
136130
tracing = ["opentelemetry-api", "aiofiles"]
131+
otel = [
132+
"opentelemetry-api",
133+
"opentelemetry-sdk",
134+
"opentelemetry-exporter-otlp",
135+
"opentelemetry-instrumentation-fastapi",
136+
"opentelemetry-exporter-prometheus",
137+
"prometheus-fastapi-instrumentator",
138+
"pydantic-settings",
139+
]
137140
nvidia = ["langchain-nvidia-ai-endpoints"]
138141
jailbreak = ["yara-python"]
139142
models = ["torch", "spacy", "nltk", "sentence-transformers", "einops"]
140-
multilingual = ["fast-langdetect"]
141-
server = ["aiofiles", "openai"]
142-
otel = ["opentelemetry-distro", "opentelemetry-exporter-otlp", "opentelemetry-instrumentation-fastapi", "opentelemetry-instrumentation-httpx"]
143143
# Poetry does not support recursive dependencies, so we need to add all the dependencies here.
144144
# I also support their decision. There is no PEP for recursive dependencies, but it has been supported in pip since version 21.2.
145145
# It is here for backward compatibility.
@@ -150,7 +150,6 @@ all = [
150150
"numpy",
151151
"streamlit",
152152
"langchain-openai",
153-
"openai",
154153
"google-cloud-language",
155154
"opentelemetry-api",
156155
"aiofiles",
@@ -161,7 +160,6 @@ all = [
161160
"nltk",
162161
"sentence-transformers",
163162
"einops",
164-
"fast-langdetect",
165163
]
166164

167165
[tool.poetry.group.dev]
@@ -171,7 +169,7 @@ optional = true
171169
[tool.poetry.group.dev.dependencies]
172170
aioresponses = ">=0.7.6"
173171
pre-commit = ">=3.1.1"
174-
pytest = ">=7.2.2,<9.0.0"
172+
pytest = ">=7.2.2"
175173
pytest-asyncio = ">=0.21.0, <1.0.0"
176174
pytest-cov = ">=4.1.0"
177175
pytest-httpx = ">=0.22.0"
@@ -181,8 +179,6 @@ pytest-profiling = "^1.7.0"
181179
yara-python = "^4.5.1"
182180
opentelemetry-api = "^1.34.1"
183181
opentelemetry-sdk = "^1.34.1"
184-
fast-langdetect = ">=1.0.0"
185-
openai = ">=1.0.0,<3.0.0"
186182
pyright = "^1.1.405"
187183
ruff = "0.14.6"
188184

@@ -199,11 +195,11 @@ include = [
199195
"nemoguardrails/tracing/**",
200196
"nemoguardrails/server/**",
201197
"tests/test_callbacks.py",
202-
"nemoguardrails/benchmark/**",
203-
"nemoguardrails/guardrails/**"
198+
"nemoguardrails/benchmark/**"
204199
]
205200
exclude = [
206201
"nemoguardrails/llm/providers/trtllm/**",
202+
"nemoguardrails/llm/providers/_langchain_nvidia_ai_endpoints_patch.py",
207203
]
208204

209205
[tool.poetry.group.docs]
@@ -215,12 +211,7 @@ sphinx-reredirects = "<0.2"
215211
sphinx = "<=7.5"
216212
myst-parser = "<=5"
217213
sphinx-copybutton = "<=0.6"
218-
sphinx-design = "*"
219-
sphinx-autobuild = "*"
220-
sphinx-autodoc-typehints = "*"
221-
sphinxcontrib-mermaid = "*"
222214
nvidia-sphinx-theme = { version = ">=0.0.8", python = ">=3.10" }
223-
watchdog = "^6.0.0"
224215

225216

226217
[tool.pytest.ini_options]

scripts/entrypoint.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env python
2+
# SPDX-FileCopyrightText: Copyright (c) 2026 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+
"""Entrypoint that optionally bootstraps the OTel SDK before starting the server."""
18+
19+
import argparse
20+
import logging
21+
import os
22+
import re
23+
import sys
24+
from contextlib import asynccontextmanager
25+
26+
logging.basicConfig(level=logging.INFO)
27+
28+
log = logging.getLogger(__name__)
29+
30+
import uvicorn # noqa: E402
31+
from observability.configure_otel_sdk_v2 import (
32+
initialize_otel,
33+
otel_config,
34+
) # noqa: E402
35+
from observability.otel import make_metrics_app, shutdown_otel # noqa: E402
36+
37+
from nemoguardrails.server.api import app
38+
39+
_app_lifespan = app.router.lifespan_context
40+
41+
42+
@asynccontextmanager
43+
async def _lifespan_with_otel(application):
44+
_otel_initialized = False
45+
try:
46+
initialize_otel()
47+
_otel_initialized = otel_config.otel_enabled
48+
if _otel_initialized:
49+
make_metrics_app(application)
50+
except Exception:
51+
log.exception("OTel initialisation failed; continuing without telemetry")
52+
53+
try:
54+
async with _app_lifespan(application):
55+
yield
56+
finally:
57+
if _otel_initialized:
58+
shutdown_otel()
59+
60+
61+
app.router.lifespan_context = _lifespan_with_otel
62+
63+
_CONFIG_ID_RE = re.compile(r"^[a-zA-Z0-9._-]+$")
64+
65+
66+
def _parse_args() -> argparse.Namespace:
67+
parser = argparse.ArgumentParser(
68+
description="NeMo Guardrails server",
69+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
70+
)
71+
parser.add_argument(
72+
"--config",
73+
default=os.environ.get("CONFIG_DIR", "config"),
74+
help="Path to config directory ($CONFIG_DIR)",
75+
)
76+
parser.add_argument(
77+
"--host",
78+
default=os.environ.get("HOST", "0.0.0.0"),
79+
help="Bind address ($HOST)",
80+
)
81+
parser.add_argument(
82+
"--port",
83+
type=int,
84+
default=int(os.environ.get("PORT", "8000")),
85+
help="Port to listen on ($PORT)",
86+
)
87+
parser.add_argument(
88+
"--default-config-id",
89+
default=os.environ.get("CONFIG_ID"),
90+
help="Default guardrail config ID ($CONFIG_ID)",
91+
)
92+
parser.add_argument(
93+
"--verbose",
94+
action="store_true",
95+
help="Enable debug logging",
96+
)
97+
parser.add_argument(
98+
"--enable-chat-ui",
99+
action="store_true",
100+
default=False,
101+
help="Enable the built-in chat UI (disabled by default)",
102+
)
103+
return parser.parse_args()
104+
105+
106+
def main() -> None:
107+
"""Main entrypoint function for the NeMo Guardrails server."""
108+
args = _parse_args()
109+
110+
config_path = os.path.abspath(args.config)
111+
if not os.path.isdir(config_path):
112+
log.error("Config directory not found: %s", config_path)
113+
sys.exit(1)
114+
115+
if args.default_config_id and not _CONFIG_ID_RE.match(args.default_config_id):
116+
log.error(
117+
"Invalid config ID %r — must match [a-zA-Z0-9._-]+",
118+
args.default_config_id,
119+
)
120+
sys.exit(1)
121+
122+
app.rails_config_path = config_path
123+
if args.default_config_id:
124+
app.default_config_id = args.default_config_id
125+
app.disable_chat_ui = not args.enable_chat_ui
126+
127+
log_level = "debug" if args.verbose else "info"
128+
uvicorn.run(app, host=args.host, port=args.port, log_level=log_level)
129+
130+
131+
if __name__ == "__main__":
132+
main()

0 commit comments

Comments
 (0)