Skip to content

Add set_default_logger_callback Python API for custom ORT logging#28457

Open
Copilot wants to merge 8 commits into
mainfrom
copilot/add-custom-logging-callback
Open

Add set_default_logger_callback Python API for custom ORT logging#28457
Copilot wants to merge 8 commits into
mainfrom
copilot/add-custom-logging-callback

Conversation

Copilot AI commented May 11, 2026

Copy link
Copy Markdown
Contributor

Description

Exposes a new onnxruntime.set_default_logger_callback(callback, severity=2) API that lets Python users route ORT log messages to any Python callable.

New function signature:

def set_default_logger_callback(
    callback: Callable[[int, str, str, str, str], None] | None,
    severity: int = 2,  # 0=Verbose, 1=Info, 2=Warning, 3=Error, 4=Fatal
) -> None: ...

Example:

import logging
import onnxruntime as ort

def ort_log_to_python(severity: int, category: str, logid: str, code_location: str, message: str) -> None:
    level = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL][severity]
    logging.log(level, "[ORT:%s] %s", category, message)

ort.set_default_logger_callback(ort_log_to_python, severity=0)  # capture all
sess = ort.InferenceSession("model.onnx")

ort.set_default_logger_callback(None)  # restore platform default (stderr/OutputDebugString)

Implementation:

  • onnxruntime_pybind_state.cc: Adds PythonCallbackSink — a thread-safe ISink subclass that wraps a swappable Python callable and falls back to the platform sink when none is set. Callback exceptions are caught and swallowed (falling back to platform sink) to prevent crashes propagating into ORT. Adds set_default_logger_callback() in addGlobalMethods which updates the sink's callable in-place; no LoggingManager reconstruction needed.
  • onnxruntime_pybind_module.cc: Modifies CreateOrtEnv() to replace the initial LoggingManager with one backed by PythonCallbackSink at module import time via a safe two-phase replace (destroy → recreate), valid because no sessions or threads exist yet.
  • onnxruntime_pybind_module_functions.h: Declares CreateAndRegisterPythonCallbackSink() factory so the module init can call it.
  • onnxruntime/__init__.py: Exports set_default_logger_callback in the public namespace.
  • onnxruntime_test_python.py: Adds test_set_default_logger_callback covering normal use, None reset, inference stability, boundary severities, invalid-input rejection, and raising-callback safety.

Motivation and Context

The Python binding hard-codes nullptr as the ORT logging function at environment creation time, making it impossible to redirect ORT log output from Python. The C++ API already supports this via OrtApi::CreateEnvWithCustomLogger. This change brings equivalent capability to the Python API so users can integrate ORT logging with their application logger without resorting to file-descriptor manipulation.

Copilot AI and others added 3 commits May 11, 2026 18:09
This implements the feature requested in the issue by:
1. Adding PythonCallbackSink class that wraps a Python callable and can
   be dynamically updated without recreating the LoggingManager singleton
2. Modifying CreateOrtEnv to install the PythonCallbackSink at init time
3. Exposing set_default_logger_callback() in the Python module
4. Exporting it in onnxruntime/__init__.py
5. Adding test_set_default_logger_callback to the test file

Agent-Logs-Url: https://github.com/microsoft/onnxruntime/sessions/6698b9af-fdda-49b0-a406-f7fb340b39e2

Co-authored-by: tianleiwu <30328909+tianleiwu@users.noreply.github.com>
…backSink and singleton guard

Agent-Logs-Url: https://github.com/microsoft/onnxruntime/sessions/6698b9af-fdda-49b0-a406-f7fb340b39e2

Co-authored-by: tianleiwu <30328909+tianleiwu@users.noreply.github.com>
Copilot AI changed the title [WIP] Add Python API support for custom ONNX Runtime logging callback Add set_default_logger_callback Python API for custom ORT logging May 11, 2026
Copilot AI requested a review from tianleiwu May 11, 2026 18:14

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can commit the suggested changes from lintrunner.

Comment thread onnxruntime/__init__.py
Comment thread onnxruntime/__init__.py Outdated
@JanSellner

Copy link
Copy Markdown
Contributor

@tianleiwu Any news on the topic? :-)
The proposed API would be very useful if this would work!

@tianleiwu tianleiwu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

The approach is sound — installing a PythonCallbackSink at module import and swapping the callable in-place (rather than rebuilding the LoggingManager singleton) is the right design, and the comments explaining the destroy → recreate sequence are excellent. Test coverage is good too.

There are two high-priority correctness/thread-safety issues around the lifecycle of the stored py::object that can affect all Python users (not just callers of the new API), plus a few suggestions. Details inline.

High-Priority

  1. g_user_logging_callback defaults to a null handle, not py::none(). is_none() is ptr() == Py_None, which is false for a default-constructed (null) py::object. Since the sink is installed at import time, every ORT log at/above WARNING emitted before the first set_default_logger_callback() call takes the callback branch and invokes a null callable → null-pointer deref. Initialize the stored callback to py::none() (under the GIL) at sink creation.
  2. Refcount manipulation without the GIL in SendImpl. ORT logs from internal worker threads that don't hold the GIL. py::object cb = g_user_logging_callback; does Py_INCREF before the GIL is acquired, and cb's destructor does Py_DECREF after the gil_scoped_acquire scope ends — both unsynchronized → data race. Hold the GIL around the copy → invoke → destroy sequence.

Suggestions

  1. set_default_logger_callback(None) always rewrites the logger severity (default WARNING), silently undoing a prior set_default_logger_severity(0). Only update severity when a callback is provided, or document it.
  2. err_msg in the exception handler is built but unused.
  3. The callback is stored in a file-scope global rather than a sink member; consider a member for clarity.

Nitpicks

  • onnxruntime_pybind_module_functions.h is missing a trailing newline.
  • set_default_logger_callback import in __init__.py is out of alphabetical order (lint bot already flagged this block).

Comment thread onnxruntime/python/onnxruntime_pybind_state.cc Outdated
Comment thread onnxruntime/python/onnxruntime_pybind_state.cc Outdated
Comment thread onnxruntime/python/onnxruntime_pybind_state.cc Outdated
Comment thread onnxruntime/python/onnxruntime_pybind_state.cc Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new Python API (onnxruntime.set_default_logger_callback) to allow Python callers to intercept ONNX Runtime log messages via a Python callable, integrating ORT logging with application logging without needing to rebuild sessions or manipulate file descriptors.

Changes:

  • Add a PythonCallbackSink (ISink) implementation in the Python bindings and expose set_default_logger_callback(callback, severity=2).
  • Replace the default LoggingManager during Python module initialization to route logs through the Python-aware sink.
  • Export the new API from onnxruntime/__init__.py and add a Python test covering basic usage and invalid inputs.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
onnxruntime/python/onnxruntime_pybind_state.cc Adds PythonCallbackSink and the set_default_logger_callback pybind entry point.
onnxruntime/python/onnxruntime_pybind_module.cc Replaces the initial LoggingManager at import time to install the Python-aware sink.
onnxruntime/python/onnxruntime_pybind_module_functions.h Declares the factory for creating/registering the Python callback sink.
onnxruntime/init.py Re-exports set_default_logger_callback in the public Python namespace.
onnxruntime/test/python/onnxruntime_test_python.py Adds a new unit test for the callback API surface and basic behavior.

Comment thread onnxruntime/python/onnxruntime_pybind_state.cc Outdated
Comment thread onnxruntime/python/onnxruntime_pybind_state.cc Outdated
Comment thread onnxruntime/python/onnxruntime_pybind_state.cc Outdated
Comment thread onnxruntime/python/onnxruntime_pybind_module.cc
Comment thread onnxruntime/test/python/onnxruntime_test_python.py
- Initialize the global callback to py::none() in the sink factory so the
  no-callback fast path is taken before any callback is set (a null handle is
  not None, so the previous code could invoke an empty callable).
- Restructure PythonCallbackSink::SendImpl to acquire the GIL before copying or
  destroying the py::object, avoiding Python refcount mutation on non-Python
  worker threads. Lock order is GIL -> mutex consistently.
- Only update default logger severity when installing a callback; resetting
  with None no longer silently overwrites a user-set severity.
- Drop the unused err_msg in the exception handler.
- Skip replacing the LoggingManager when a process-wide OrtEnv already existed
  (created outside Python) to avoid invalidating  by existing
  sessions.
- Assert the callback is actually invoked and validate record types in the test.
- Reorder __init__.py import alphabetically; add trailing newline to header.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Comment thread onnxruntime/python/onnxruntime_pybind_state.cc Outdated
Comment thread onnxruntime/python/onnxruntime_pybind_state.cc Outdated
Comment thread onnxruntime/python/onnxruntime_pybind_state.cc Outdated
Comment thread onnxruntime/test/python/onnxruntime_test_python.py
Comment thread onnxruntime/test/python/onnxruntime_test_python.py Outdated
- Store the user logging callback as a raw PyObject* (intentionally leaked at
  shutdown) instead of a global py::object, so its reference is never DECREF'd
  during static destruction / module unload after interpreter finalization.
- Do all INCREF/DECREF under the GIL; keep the refcount-free pre-check under the
  mutex alone and clarify the lock-order comment.
- Restore the default (Warning) logger severity in the Python test so the
  Verbose level it sets does not leak into the rest of the suite.
- Add trailing newline in the set_default_logger_callback docstring.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@tianleiwu tianleiwu marked this pull request as ready for review June 20, 2026 23:17
…est model I/O names

- Extract InstallPythonCallbackLoggingSink() into the shared
  onnxruntime_pybind_state.cc so both the inference and training pybind
  entry points install the PythonCallbackSink. The training module's
  CreateOrtEnv() previously registered set_default_logger_callback (via the
  shared addGlobalMethods) but never installed the sink, so the API threw
  'g_python_callback_sink != nullptr was false' on training builds.
- Fix test_set_default_logger_callback to use mul_1.onnx's actual single
  input 'X' and output 'Y' instead of the nonexistent inputs X/Y and
  output Z, which caused 'Invalid input name: Y' on macOS/CPU builds.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Python API support for custom ONNX Runtime logging callback

4 participants