From 84231d5148c47986a70219525e1cf1e20a2decdf Mon Sep 17 00:00:00 2001 From: Christina Xu Date: Tue, 28 Apr 2026 15:38:01 -0400 Subject: [PATCH] feat: Add OTel SDK setup --- Dockerfile.server | 18 +- poetry.lock | 265 ++++++++++---------- pyproject.toml | 4 +- requirements.txt | 29 ++- scripts/entrypoint.py | 131 ++++++++++ scripts/entrypoint.sh | 48 ---- scripts/observability/__init__.py | 14 ++ scripts/observability/configure_otel_sdk.py | 50 ++++ scripts/observability/otel.py | 143 +++++++++++ 9 files changed, 511 insertions(+), 191 deletions(-) create mode 100644 scripts/entrypoint.py delete mode 100644 scripts/entrypoint.sh create mode 100644 scripts/observability/__init__.py create mode 100644 scripts/observability/configure_otel_sdk.py create mode 100644 scripts/observability/otel.py diff --git a/Dockerfile.server b/Dockerfile.server index fc4bfb4921..12458158e7 100644 --- a/Dockerfile.server +++ b/Dockerfile.server @@ -7,11 +7,11 @@ ARG PYYAML_VERSION=6.0.3 ARG GUARDRAILS_PROFILE=opensource RUN dnf install -y --nodocs --setopt=install_weak_deps=False \ - gcc \ - gcc-c++ \ - git && \ + gcc \ + gcc-c++ \ + git && \ pip install --no-cache-dir \ - pyyaml==${PYYAML_VERSION} && \ + pyyaml==${PYYAML_VERSION} && \ dnf clean all && \ rm -rf /var/cache/dnf /tmp/* /var/tmp/* @@ -20,7 +20,7 @@ COPY nemoguardrails/ ./nemoguardrails/ COPY scripts/ ./scripts/ COPY examples/bots/ ./examples/bots/ -RUN chmod +x ./scripts/entrypoint.sh +RUN chmod +x ./scripts/entrypoint.py RUN python3 ./scripts/filter_guardrails.py ./scripts/provider-list.yaml $GUARDRAILS_PROFILE ENV HF_HOME=/app/.cache/huggingface \ @@ -32,8 +32,8 @@ ENV HF_HOME=/app/.cache/huggingface \ # Create virtual environment and install dependencies RUN python3 -m venv /app/.venv && \ /app/.venv/bin/pip install --no-cache-dir \ - --extra-index-url https://download.pytorch.org/whl/cpu \ - -r requirements.txt && \ + --extra-index-url https://download.pytorch.org/whl/cpu \ + -r requirements.txt && \ /app/.venv/bin/pip install --no-cache-dir --no-deps . && \ rm -rf /root/.cache/pip /tmp/* /var/tmp/* @@ -58,7 +58,7 @@ COPY --from=build /app /app RUN rm -f /etc/security/namespace.conf /usr/lib64/security/pam_namespace.so || true && \ chgrp -R 0 /app && \ chmod -R g=u /app && \ - chmod +x /app/scripts/entrypoint.sh + chmod +x /app/scripts/entrypoint.py USER 1001 @@ -73,4 +73,4 @@ ENV HF_HOME=/app/.cache/huggingface \ DO_NOT_TRACK=1 EXPOSE 8000 -ENTRYPOINT ["./scripts/entrypoint.sh"] +ENTRYPOINT ["./scripts/entrypoint.py"] diff --git a/poetry.lock b/poetry.lock index 4bbada8748..710260d2f3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "accessible-pygments" @@ -2022,29 +2022,6 @@ files = [ {file = "imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3"}, ] -[[package]] -name = "importlib-metadata" -version = "8.7.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -files = [ - {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, - {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -perf = ["ipython"] -test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["mypy (<1.19)", "pytest-mypy (>=1.0.1)"] - [[package]] name = "iniconfig" version = "2.3.0" @@ -3493,76 +3470,75 @@ voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] [[package]] name = "opentelemetry-api" -version = "1.40.0" +version = "1.42.1" description = "OpenTelemetry Python API" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9"}, - {file = "opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f"}, + {file = "opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714"}, + {file = "opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716"}, ] [package.dependencies] -importlib-metadata = ">=6.0,<8.8.0" typing-extensions = ">=4.5.0" [[package]] name = "opentelemetry-distro" -version = "0.61b0" +version = "0.63b1" description = "OpenTelemetry Python Distro" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "opentelemetry_distro-0.61b0-py3-none-any.whl", hash = "sha256:f21d1ac0627549795d75e332006dd068877f00e461b1b2e8fe4568d6eb7b9590"}, - {file = "opentelemetry_distro-0.61b0.tar.gz", hash = "sha256:975b845f50181ad53753becf4fd4b123b54fa04df5a9d78812264436d6518981"}, + {file = "opentelemetry_distro-0.63b1-py3-none-any.whl", hash = "sha256:b405b04ad70e430390265eb38e82e067a84ca1f49a21429eaadb930c13330d66"}, + {file = "opentelemetry_distro-0.63b1.tar.gz", hash = "sha256:f435098abc7953f58226e8bf79e4c90bc6b32e50aa75d6fa074201db8243b577"}, ] [package.dependencies] opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.61b0" +opentelemetry-instrumentation = "0.63b1" opentelemetry-sdk = ">=1.13,<2.0" [package.extras] -otlp = ["opentelemetry-exporter-otlp (==1.40.0)"] +otlp = ["opentelemetry-exporter-otlp (==1.42.1)"] [[package]] name = "opentelemetry-exporter-otlp" -version = "1.40.0" +version = "1.42.1" description = "OpenTelemetry Collector Exporters" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "opentelemetry_exporter_otlp-1.40.0-py3-none-any.whl", hash = "sha256:48c87e539ec9afb30dc443775a1334cc5487de2f72a770a4c00b1610bf6c697d"}, - {file = "opentelemetry_exporter_otlp-1.40.0.tar.gz", hash = "sha256:7caa0870b95e2fcb59d64e16e2b639ecffb07771b6cd0000b5d12e5e4fef765a"}, + {file = "opentelemetry_exporter_otlp-1.42.1-py3-none-any.whl", hash = "sha256:aedd54545bb0587cd45210abdc8be545af9c01413f3307786e276df1e3c83bee"}, + {file = "opentelemetry_exporter_otlp-1.42.1.tar.gz", hash = "sha256:2d9ebaed714377a67d224d46795ddcc11d2c877fa5de35fda70b6f3b010729a9"}, ] [package.dependencies] -opentelemetry-exporter-otlp-proto-grpc = "1.40.0" -opentelemetry-exporter-otlp-proto-http = "1.40.0" +opentelemetry-exporter-otlp-proto-grpc = "1.42.1" +opentelemetry-exporter-otlp-proto-http = "1.42.1" [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.40.0" +version = "1.42.1" description = "OpenTelemetry Protobuf encoding" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149"}, - {file = "opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee"}, ] [package.dependencies] -opentelemetry-proto = "1.40.0" +opentelemetry-proto = "1.42.1" [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.40.0" +version = "1.42.1" description = "OpenTelemetry Collector Protobuf over gRPC Exporter" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "opentelemetry_exporter_otlp_proto_grpc-1.40.0-py3-none-any.whl", hash = "sha256:2aa0ca53483fe0cf6405087a7491472b70335bc5c7944378a0a8e72e86995c52"}, - {file = "opentelemetry_exporter_otlp_proto_grpc-1.40.0.tar.gz", hash = "sha256:bd4015183e40b635b3dab8da528b27161ba83bf4ef545776b196f0fb4ec47740"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.42.1-py3-none-any.whl", hash = "sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.42.1.tar.gz", hash = "sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15"}, ] [package.dependencies] @@ -3572,9 +3548,9 @@ grpcio = [ {version = ">=1.66.2,<2.0.0", markers = "python_version == \"3.13\""}, ] opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.40.0" -opentelemetry-proto = "1.40.0" -opentelemetry-sdk = ">=1.40.0,<1.41.0" +opentelemetry-exporter-otlp-proto-common = "1.42.1" +opentelemetry-proto = "1.42.1" +opentelemetry-sdk = ">=1.42.1,<1.43.0" typing-extensions = ">=4.6.0" [package.extras] @@ -3582,116 +3558,132 @@ gcp-auth = ["opentelemetry-exporter-credential-provider-gcp (>=0.59b0)"] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.40.0" +version = "1.42.1" description = "OpenTelemetry Collector Protobuf over HTTP Exporter" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "opentelemetry_exporter_otlp_proto_http-1.40.0-py3-none-any.whl", hash = "sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069"}, - {file = "opentelemetry_exporter_otlp_proto_http-1.40.0.tar.gz", hash = "sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d"}, ] [package.dependencies] googleapis-common-protos = ">=1.52,<2.0" opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.40.0" -opentelemetry-proto = "1.40.0" -opentelemetry-sdk = ">=1.40.0,<1.41.0" +opentelemetry-exporter-otlp-proto-common = "1.42.1" +opentelemetry-proto = "1.42.1" +opentelemetry-sdk = ">=1.42.1,<1.43.0" requests = ">=2.7,<3.0" typing-extensions = ">=4.5.0" [package.extras] gcp-auth = ["opentelemetry-exporter-credential-provider-gcp (>=0.59b0)"] +[[package]] +name = "opentelemetry-exporter-prometheus" +version = "0.63b1" +description = "Prometheus Metric Exporter for OpenTelemetry" +optional = true +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_exporter_prometheus-0.63b1-py3-none-any.whl", hash = "sha256:0efd00aa6b1939345ddcc6de141b83ebffa2b4401a37a68f880e54217602701d"}, + {file = "opentelemetry_exporter_prometheus-0.63b1.tar.gz", hash = "sha256:31902e22c89431058a95b6dcdb644f9309f226aa4872cc755f0a780d2895e97f"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-sdk = ">=1.42.1,<1.43.0" +prometheus-client = ">=0.5.0,<1.0.0" + [[package]] name = "opentelemetry-instrumentation" -version = "0.61b0" +version = "0.63b1" description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "opentelemetry_instrumentation-0.61b0-py3-none-any.whl", hash = "sha256:92a93a280e69788e8f88391247cc530fd81f16f2b011979d4d6398f805cfbc63"}, - {file = "opentelemetry_instrumentation-0.61b0.tar.gz", hash = "sha256:cb21b48db738c9de196eba6b805b4ff9de3b7f187e4bbf9a466fa170514f1fc7"}, + {file = "opentelemetry_instrumentation-0.63b1-py3-none-any.whl", hash = "sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c"}, + {file = "opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541"}, ] [package.dependencies] opentelemetry-api = ">=1.4,<2.0" -opentelemetry-semantic-conventions = "0.61b0" +opentelemetry-semantic-conventions = "0.63b1" packaging = ">=18.0" -wrapt = ">=1.0.0,<2.0.0" +wrapt = ">=1.0.0,<3.0.0" [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.61b0" +version = "0.63b1" description = "ASGI instrumentation for OpenTelemetry" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "opentelemetry_instrumentation_asgi-0.61b0-py3-none-any.whl", hash = "sha256:e4b3ce6b66074e525e717efff20745434e5efd5d9df6557710856fba356da7a4"}, - {file = "opentelemetry_instrumentation_asgi-0.61b0.tar.gz", hash = "sha256:9d08e127244361dc33976d39dd4ca8f128b5aa5a7ae425208400a80a095019b5"}, + {file = "opentelemetry_instrumentation_asgi-0.63b1-py3-none-any.whl", hash = "sha256:1a22453dfa965f14799b10a674b8acbcb897a8a75c79136060af54214cc7886e"}, + {file = "opentelemetry_instrumentation_asgi-0.63b1.tar.gz", hash = "sha256:267b422416d768f3c7f4054883b41d9c3a7c943d86d20032b738c99a3dbb5862"}, ] [package.dependencies] asgiref = ">=3.0,<4.0" opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.61b0" -opentelemetry-semantic-conventions = "0.61b0" -opentelemetry-util-http = "0.61b0" +opentelemetry-instrumentation = "0.63b1" +opentelemetry-semantic-conventions = "0.63b1" +opentelemetry-util-http = "0.63b1" [package.extras] instruments = ["asgiref (>=3.0,<4.0)"] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.61b0" +version = "0.63b1" description = "OpenTelemetry FastAPI Instrumentation" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "opentelemetry_instrumentation_fastapi-0.61b0-py3-none-any.whl", hash = "sha256:a1a844d846540d687d377516b2ff698b51d87c781b59f47c214359c4a241047c"}, - {file = "opentelemetry_instrumentation_fastapi-0.61b0.tar.gz", hash = "sha256:3a24f35b07c557ae1bbc483bf8412221f25d79a405f8b047de8b670722e2fa9f"}, + {file = "opentelemetry_instrumentation_fastapi-0.63b1-py3-none-any.whl", hash = "sha256:52ee2cde9a2ac094bdd45d79f85860e03a972928a2553006071fe61d94cf7281"}, + {file = "opentelemetry_instrumentation_fastapi-0.63b1.tar.gz", hash = "sha256:cc42dff56c96d0a2921510c4abab2a4c2e27fe64b26dc1254727fb550df100ba"}, ] [package.dependencies] opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.61b0" -opentelemetry-instrumentation-asgi = "0.61b0" -opentelemetry-semantic-conventions = "0.61b0" -opentelemetry-util-http = "0.61b0" +opentelemetry-instrumentation = "0.63b1" +opentelemetry-instrumentation-asgi = "0.63b1" +opentelemetry-semantic-conventions = "0.63b1" +opentelemetry-util-http = "0.63b1" [package.extras] instruments = ["fastapi (>=0.92,<1.0)"] [[package]] name = "opentelemetry-instrumentation-httpx" -version = "0.61b0" +version = "0.63b1" description = "OpenTelemetry HTTPX Instrumentation" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "opentelemetry_instrumentation_httpx-0.61b0-py3-none-any.whl", hash = "sha256:dee05c93a6593a5dc3ae5d9d5c01df8b4e2c5d02e49275e5558534ee46343d5e"}, - {file = "opentelemetry_instrumentation_httpx-0.61b0.tar.gz", hash = "sha256:6569ec097946c5551c2a4252f74c98666addd1bf047c1dde6b4ef426719ff8dd"}, + {file = "opentelemetry_instrumentation_httpx-0.63b1-py3-none-any.whl", hash = "sha256:14df6e99d81be9a8cd238f6639b6fa52404c4d3ce219058fcb5dc8c0f2211f86"}, + {file = "opentelemetry_instrumentation_httpx-0.63b1.tar.gz", hash = "sha256:f41ec82f25c3abcdada621052db3e5fd648e3b43d55eec4b9c0c5d3ecb7b4ff4"}, ] [package.dependencies] opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.61b0" -opentelemetry-semantic-conventions = "0.61b0" -opentelemetry-util-http = "0.61b0" -wrapt = ">=1.0.0,<2.0.0" +opentelemetry-instrumentation = "0.63b1" +opentelemetry-semantic-conventions = "0.63b1" +opentelemetry-util-http = "0.63b1" +wrapt = ">=1.0.0,<3.0.0" [package.extras] instruments = ["httpx (>=0.18.0)"] [[package]] name = "opentelemetry-proto" -version = "1.40.0" +version = "1.42.1" description = "OpenTelemetry Python Proto" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f"}, - {file = "opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd"}, + {file = "opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c"}, + {file = "opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6"}, ] [package.dependencies] @@ -3699,44 +3691,47 @@ protobuf = ">=5.0,<7.0" [[package]] name = "opentelemetry-sdk" -version = "1.40.0" +version = "1.42.1" description = "OpenTelemetry Python SDK" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1"}, - {file = "opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2"}, + {file = "opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d"}, + {file = "opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7"}, ] [package.dependencies] -opentelemetry-api = "1.40.0" -opentelemetry-semantic-conventions = "0.61b0" +opentelemetry-api = "1.42.1" +opentelemetry-semantic-conventions = "0.63b1" typing-extensions = ">=4.5.0" +[package.extras] +file-configuration = ["jsonschema (>=4.0)", "pyyaml (>=6.0)"] + [[package]] name = "opentelemetry-semantic-conventions" -version = "0.61b0" +version = "0.63b1" description = "OpenTelemetry Semantic Conventions" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2"}, - {file = "opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a"}, + {file = "opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682"}, + {file = "opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9"}, ] [package.dependencies] -opentelemetry-api = "1.40.0" +opentelemetry-api = "1.42.1" typing-extensions = ">=4.5.0" [[package]] name = "opentelemetry-util-http" -version = "0.61b0" +version = "0.63b1" description = "Web util for OpenTelemetry" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "opentelemetry_util_http-0.61b0-py3-none-any.whl", hash = "sha256:8e715e848233e9527ea47e275659ea60a57a75edf5206a3b937e236a6da5fc33"}, - {file = "opentelemetry_util_http-0.61b0.tar.gz", hash = "sha256:1039cb891334ad2731affdf034d8fb8b48c239af9b6dd295e5fabd07f1c95572"}, + {file = "opentelemetry_util_http-0.63b1-py3-none-any.whl", hash = "sha256:6284194028c59cd439f8acfe388145069a6127f11dc077e1344a2094adacc3f8"}, + {file = "opentelemetry_util_http-0.63b1.tar.gz", hash = "sha256:ba1268f00922ee522dba2ae38458060f99486e7385a8056985901ca9685adfff"}, ] [[package]] @@ -4267,6 +4262,37 @@ cryptography = ">=46.0.4" ahds = ["azure-health-deidentification (>=1.1.0b1,<2.0.0)", "azure-identity (>=1.23.0,<2.0.0)"] server = ["flask (>=1.1)", "gunicorn", "waitress"] +[[package]] +name = "prometheus-client" +version = "0.25.0" +description = "Python client for the Prometheus monitoring system." +optional = true +python-versions = ">=3.9" +files = [ + {file = "prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1"}, + {file = "prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28"}, +] + +[package.extras] +aiohttp = ["aiohttp"] +django = ["django"] +twisted = ["twisted"] + +[[package]] +name = "prometheus-fastapi-instrumentator" +version = "8.0.0" +description = "Instrument your FastAPI app with Prometheus metrics" +optional = true +python-versions = ">=3.10" +files = [ + {file = "prometheus_fastapi_instrumentator-8.0.0-py3-none-any.whl", hash = "sha256:e3c967c402124dfe81cf5aad35208d9f96db5452c6cb752350d9358ca0a96198"}, + {file = "prometheus_fastapi_instrumentator-8.0.0.tar.gz", hash = "sha256:c31ed192db077e75467b28b2fe5055362517f53369b798cb8dac9ff85f3f1c38"}, +] + +[package.dependencies] +prometheus-client = ">=0.8.0,<1.0.0" +starlette = ">=1.0.0,<2.0.0" + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -7838,25 +7864,6 @@ idna = ">=2.0" multidict = ">=4.0" propcache = ">=0.2.1" -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [[package]] name = "zstandard" version = "0.25.0" @@ -7977,7 +7984,7 @@ models = ["einops", "nltk", "sentence-transformers", "spacy", "torch"] multilingual = ["fast-langdetect"] nvidia = ["langchain-nvidia-ai-endpoints"] openai = ["langchain-openai"] -otel = ["opentelemetry-distro", "opentelemetry-exporter-otlp", "opentelemetry-instrumentation-fastapi", "opentelemetry-instrumentation-httpx"] +otel = ["opentelemetry-distro", "opentelemetry-exporter-otlp", "opentelemetry-exporter-prometheus", "opentelemetry-instrumentation-fastapi", "opentelemetry-instrumentation-httpx", "prometheus-fastapi-instrumentator"] sdd = ["presidio-analyzer", "presidio-anonymizer"] server = ["aiofiles", "openai"] tracing = ["aiofiles", "opentelemetry-api"] @@ -7985,4 +7992,4 @@ tracing = ["aiofiles", "opentelemetry-api"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.14" -content-hash = "5e342f2a7f3bde068daf56672812c8ed9d2d4605d88e6f6bafdce85a321e86c5" +content-hash = "023f56336dcdbe00bd65285a49afa6dc818c54c3e461614953c064f89433f74a" diff --git a/pyproject.toml b/pyproject.toml index 36a3e43ad6..ffe5d881c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,8 +86,10 @@ aiofiles = { version = ">=24.1.0", optional = true } # otel auto-instrumentation (fork-specific) opentelemetry-distro = { version = ">=0.48b0", optional = true } opentelemetry-exporter-otlp = { version = ">=1.27.0,<2.0.0", optional = true } +opentelemetry-exporter-prometheus = {version = ">=0.48b0", optional = true} opentelemetry-instrumentation-fastapi = { version = ">=0.48b0", optional = true } opentelemetry-instrumentation-httpx = { version = ">=0.48b0", optional = true } +prometheus-fastapi-instrumentator = { version = ">=7.0.0", optional = true } # openai langchain-openai = { version = ">=0.1.0", optional = true } @@ -139,7 +141,7 @@ jailbreak = ["yara-python"] models = ["torch", "spacy", "nltk", "sentence-transformers", "einops"] multilingual = ["fast-langdetect"] server = ["aiofiles", "openai"] -otel = ["opentelemetry-distro", "opentelemetry-exporter-otlp", "opentelemetry-instrumentation-fastapi", "opentelemetry-instrumentation-httpx"] +otel = ["opentelemetry-distro", "opentelemetry-exporter-otlp", "opentelemetry-exporter-prometheus", "opentelemetry-instrumentation-fastapi", "opentelemetry-instrumentation-httpx", "prometheus-fastapi-instrumentator"] # Poetry does not support recursive dependencies, so we need to add all the dependencies here. # I also support their decision. There is no PEP for recursive dependencies, but it has been supported in pip since version 21.2. # It is here for backward compatibility. diff --git a/requirements.txt b/requirements.txt index 426f5fd4dd..3148a323f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,6 +27,10 @@ anyio==4.11.0 # starlette asgiref==3.11.1 # via opentelemetry-instrumentation-asgi +async-timeout==4.0.3 + # via + # aiohttp + # langchain attrs==25.3.0 # via aiohttp blis==1.3.0 @@ -73,6 +77,8 @@ distro==1.9.0 # via openai einops==0.8.1 # via nemoguardrails (pyproject.toml) +exceptiongroup==1.3.1 + # via anyio fast-langdetect==1.0.0 # via nemoguardrails (pyproject.toml) fastapi==0.122.0 @@ -212,11 +218,11 @@ mypy-extensions==1.1.0 # via typing-inspect nest-asyncio==1.6.0 # via nemoguardrails (pyproject.toml) -networkx==3.5 +networkx==3.4.2 # via torch nltk==3.9.2 # via nemoguardrails (pyproject.toml) -numpy==2.3.3 +numpy==2.2.6 # via # blis # fastembed @@ -242,6 +248,7 @@ opentelemetry-api==1.38.0 # opentelemetry-distro # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http + # opentelemetry-exporter-prometheus # opentelemetry-instrumentation # opentelemetry-instrumentation-asgi # opentelemetry-instrumentation-fastapi @@ -260,6 +267,8 @@ opentelemetry-exporter-otlp-proto-grpc==1.38.0 # via opentelemetry-exporter-otlp opentelemetry-exporter-otlp-proto-http==1.38.0 # via opentelemetry-exporter-otlp +opentelemetry-exporter-prometheus==0.59b0 + # via nemoguardrails (pyproject.toml) opentelemetry-instrumentation==0.59b0 # via # opentelemetry-distro @@ -282,6 +291,7 @@ opentelemetry-sdk==1.38.0 # opentelemetry-distro # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http + # opentelemetry-exporter-prometheus opentelemetry-semantic-conventions==0.59b0 # via # opentelemetry-instrumentation @@ -324,6 +334,12 @@ presidio-analyzer==2.2.359 # via nemoguardrails (pyproject.toml) presidio-anonymizer==2.2.359 # via nemoguardrails (pyproject.toml) +prometheus-client==0.25.0 + # via + # opentelemetry-exporter-prometheus + # prometheus-fastapi-instrumentator +prometheus-fastapi-instrumentator==7.1.0 + # via nemoguardrails (pyproject.toml) prompt-toolkit==3.0.52 # via nemoguardrails (pyproject.toml) propcache==0.3.2 @@ -411,7 +427,7 @@ safetensors==0.6.2 # via transformers scikit-learn==1.7.2 # via sentence-transformers -scipy==1.16.3 +scipy==1.15.3 # via # scikit-learn # sentence-transformers @@ -421,7 +437,6 @@ setuptools==80.9.0 # via # spacy # thinc - # torch shellingham==1.5.4 # via typer simpleeval==1.0.3 @@ -456,6 +471,7 @@ starlette==0.50.0 # via # nemoguardrails (pyproject.toml) # fastapi + # prometheus-fastapi-instrumentator sympy==1.14.0 # via # onnxruntime @@ -501,10 +517,14 @@ typing-extensions==4.15.0 # via # aiosignal # anyio + # asgiref + # cloudpathlib + # exceptiongroup # fastapi # grpcio # huggingface-hub # langchain-core + # multidict # openai # opentelemetry-api # opentelemetry-exporter-otlp-proto-grpc @@ -520,6 +540,7 @@ typing-extensions==4.15.0 # typer # typing-inspect # typing-inspection + # uvicorn typing-inspect==0.9.0 # via dataclasses-json typing-inspection==0.4.1 diff --git a/scripts/entrypoint.py b/scripts/entrypoint.py new file mode 100644 index 0000000000..27c55ad08d --- /dev/null +++ b/scripts/entrypoint.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Entrypoint that optionally bootstraps the OTel SDK before starting the server.""" + +import argparse +import logging +import os +import re +import sys +from contextlib import asynccontextmanager + +import uvicorn +from observability.configure_otel_sdk import ( + initialize_otel, + otel_config, +) +from observability.otel import make_metrics_app, shutdown_otel + +from nemoguardrails.server.api import app + +logging.basicConfig(level=logging.INFO) + +log = logging.getLogger(__name__) + + +try: + initialize_otel() + if otel_config.otel_enabled: + make_metrics_app(app) +except Exception: + log.exception("OTel initialisation failed; continuing without telemetry") + +_app_lifespan = app.router.lifespan_context + + +@asynccontextmanager +async def _lifespan_with_otel(application): + """Extend the app lifespan to flush and shut down OTel providers on exit.""" + async with _app_lifespan(application): + yield + + if otel_config.otel_enabled: + shutdown_otel() + + +app.router.lifespan_context = _lifespan_with_otel + +_CONFIG_ID_RE = re.compile(r"^[a-zA-Z0-9._-]+$") + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="NeMo Guardrails server", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "--config", + default=os.environ.get("CONFIG_DIR", "config"), + help="Path to config directory ($CONFIG_DIR)", + ) + parser.add_argument( + "--host", + default=os.environ.get("HOST", "0.0.0.0"), + help="Bind address ($HOST)", + ) + parser.add_argument( + "--port", + type=int, + default=int(os.environ.get("PORT", "8000")), + help="Port to listen on ($PORT)", + ) + parser.add_argument( + "--default-config-id", + default=os.environ.get("CONFIG_ID"), + help="Default guardrail config ID ($CONFIG_ID)", + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Enable debug logging", + ) + parser.add_argument( + "--enable-chat-ui", + action="store_true", + default=False, + help="Enable the built-in chat UI (disabled by default)", + ) + return parser.parse_args() + + +def main() -> None: + """Main entrypoint function for the NeMo Guardrails server.""" + args = _parse_args() + + config_path = os.path.abspath(args.config) + if not os.path.isdir(config_path): + log.error("Config directory not found: %s", config_path) + sys.exit(1) + + if args.default_config_id and not _CONFIG_ID_RE.match(args.default_config_id): + log.error( + "Invalid config ID %r — must match [a-zA-Z0-9._-]+", + args.default_config_id, + ) + sys.exit(1) + + app.rails_config_path = config_path + if args.default_config_id: + app.default_config_id = args.default_config_id + app.disable_chat_ui = not args.enable_chat_ui + + log_level = "debug" if args.verbose else "info" + uvicorn.run(app, host=args.host, port=args.port, log_level=log_level) + + +if __name__ == "__main__": + main() diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh deleted file mode 100644 index c58e7114d0..0000000000 --- a/scripts/entrypoint.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash - -# Allow runtime overrides via env vars or args -CONFIG_ID="${CONFIG_ID:-${1:-nemo}}" -PORT="${PORT:-${2:-8000}}" - -# Validate inputs to prevent command injection -if [[ ! "$CONFIG_ID" =~ ^[a-zA-Z0-9_-]+$ ]]; then - echo "ERROR: CONFIG_ID contains invalid characters: $CONFIG_ID" - exit 1 -fi -if [[ ! "$PORT" =~ ^[0-9]+$ ]]; then - echo "ERROR: PORT is not a valid number: $PORT" - exit 1 -fi - -CONFIG_DIR="/app/config/${CONFIG_ID}" - -echo "🚀 Starting NeMo Guardrails with config from: $CONFIG_DIR (port: $PORT)" - -# Validate config exists -if [[ ! -f "$CONFIG_DIR/config.yaml" ]]; then - echo "❌ ERROR: config.yaml not found in $CONFIG_DIR" - exit 1 -fi - -if [[ ! -f "$CONFIG_DIR/rails.co" ]]; then - echo "❌ ERROR: rails.co not found in $CONFIG_DIR (ConfigMap is read-only, please provide it)" - exit 1 -fi - -echo "✅ Configuration validated. Starting server..." - -CMD=(/app/.venv/bin/nemoguardrails server \ - --config "/app/config" \ - --port "$PORT" \ - --default-config-id "$CONFIG_ID" \ - --disable-chat-ui) - -# If OTEL_EXPORTER_OTLP_ENDPOINT is set and opentelemetry-instrument is available, -# wrap the server with auto-instrumentation -if [[ -n "$OTEL_EXPORTER_OTLP_ENDPOINT" ]] && command -v opentelemetry-instrument &>/dev/null; then - echo "OpenTelemetry enabled: endpoint=$OTEL_EXPORTER_OTLP_ENDPOINT service=${OTEL_SERVICE_NAME:-nemo-guardrails}" - export OTEL_SERVICE_NAME="${OTEL_SERVICE_NAME:-nemo-guardrails}" - exec opentelemetry-instrument "${CMD[@]}" -else - exec "${CMD[@]}" -fi diff --git a/scripts/observability/__init__.py b/scripts/observability/__init__.py new file mode 100644 index 0000000000..467079831e --- /dev/null +++ b/scripts/observability/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/scripts/observability/configure_otel_sdk.py b/scripts/observability/configure_otel_sdk.py new file mode 100644 index 0000000000..5eb38290ae --- /dev/null +++ b/scripts/observability/configure_otel_sdk.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from typing import Literal + +from pydantic_settings import BaseSettings + + +class OTELConfig(BaseSettings): + otel_enabled: bool = False + service_name: str = "nemo-guardrails" + otel_exporter_otlp_insecure: bool = False + otel_exporter_otlp_endpoint: str | None = None + otel_traces_exporter: str | None = "otlp" + otel_metrics_exporter: Literal["prometheus", "otlp"] = "prometheus" + otel_logs_exporter: str | None = "otlp" + otel_exporter_otlp_metrics_endpoint: str | None = None + otel_exporter_otlp_traces_endpoint: str | None = None + otel_exporter_otlp_logs_endpoint: str | None = None + otel_exporter_metrics_endpoint: str | None = None + + +log = logging.getLogger(__name__) + +otel_config = OTELConfig() + + +def initialize_otel(): + if not otel_config.otel_enabled: + return + + from .otel import create_otel_resource, initialize_logs, initialize_metrics, initialize_traces + + resource = create_otel_resource(otel_config.service_name) + initialize_traces(resource) + initialize_metrics(resource) + initialize_logs(resource) diff --git a/scripts/observability/otel.py b/scripts/observability/otel.py new file mode 100644 index 0000000000..215d898a51 --- /dev/null +++ b/scripts/observability/otel.py @@ -0,0 +1,143 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from fastapi import FastAPI +from opentelemetry import metrics, trace +from opentelemetry._logs import get_logger_provider +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.trace import TracerProvider + +from .configure_otel_sdk import otel_config + +log = logging.getLogger(__name__) + + +def create_otel_resource(service_name: str) -> Resource: + return Resource.create({SERVICE_NAME: service_name}) + + +def initialize_traces(resource: Resource | None = None): + if resource is None: + resource = create_otel_resource(otel_config.service_name) + tracer_provider = TracerProvider(resource=resource) + + if otel_config.otel_traces_exporter == "otlp": + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, + ) + from opentelemetry.sdk.trace.export import BatchSpanProcessor + + span_processor = BatchSpanProcessor( + OTLPSpanExporter( + endpoint=otel_config.otel_exporter_otlp_traces_endpoint or otel_config.otel_exporter_otlp_endpoint + ) + ) + tracer_provider.add_span_processor(span_processor) + trace.set_tracer_provider(tracer_provider) + + +def initialize_metrics(resource: Resource | None = None): + if resource is None: + resource = create_otel_resource(otel_config.service_name) + + if otel_config.otel_metrics_exporter == "prometheus": + from opentelemetry.exporter.prometheus import PrometheusMetricReader + + metrics_reader = PrometheusMetricReader() + elif otel_config.otel_metrics_exporter == "otlp": + from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( + OTLPMetricExporter, + ) + from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader + + metrics_reader = PeriodicExportingMetricReader( + OTLPMetricExporter( + endpoint=otel_config.otel_exporter_otlp_metrics_endpoint or otel_config.otel_exporter_otlp_endpoint + ) + ) + else: + raise ValueError(f"Invalid metrics exporter: {otel_config.otel_metrics_exporter}") + + meter_provider = MeterProvider(resource=resource, metric_readers=[metrics_reader]) + metrics.set_meter_provider(meter_provider) + + +def make_metrics_app(app: FastAPI): + from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor + + FastAPIInstrumentor.instrument_app( + app, + excluded_urls="/metrics", + ) + + if otel_config.otel_metrics_exporter == "prometheus": + from prometheus_fastapi_instrumentator import ( + Instrumentator as PrometheusInstrumentator, + ) + + PrometheusInstrumentator().instrument(app).expose(app) + + +def _otel_internal_filter(record: logging.LogRecord) -> bool: + _SUPPRESSED = ("opentelemetry", "grpc", "urllib3") + return not any(record.name.startswith(ns) for ns in _SUPPRESSED) + + +def initialize_logs(resource: Resource | None = None): + if resource is None: + resource = create_otel_resource(otel_config.service_name) + logger_provider = LoggerProvider(resource=resource) + if otel_config.otel_logs_exporter == "otlp": + from opentelemetry._logs import set_logger_provider + from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( + OTLPLogExporter as OTLPLogsExporter, + ) + from opentelemetry.sdk._logs import LoggingHandler + from opentelemetry.sdk._logs.export import BatchLogRecordProcessor + + log_record_processor = BatchLogRecordProcessor( + OTLPLogsExporter( + endpoint=otel_config.otel_exporter_otlp_logs_endpoint or otel_config.otel_exporter_otlp_endpoint + ) + ) + logger_provider.add_log_record_processor(log_record_processor) + set_logger_provider(logger_provider) + + handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider) + handler.addFilter(_otel_internal_filter) + logging.getLogger().addHandler(handler) + + +def shutdown_otel() -> None: + """Flush and shut down all globally-registered OTel providers.""" + providers = { + "tracer": trace.get_tracer_provider(), + "meter": metrics.get_meter_provider(), + "logger": get_logger_provider(), + } + failed = False + for name, provider in providers.items(): + if hasattr(provider, "shutdown"): + try: + provider.shutdown() + except Exception: + log.exception("Failed to shut down OTel %s provider", name) + failed = True + if not failed: + log.info("OTel providers shut down")