Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions include/triton/core/tritonserver.h
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,23 @@ TRITONSERVER_DECLSPEC struct TRITONSERVER_Error* TRITONSERVER_LogMessage(
TRITONSERVER_LogLevel level, const char* filename, const int line,
const char* msg);

/// Log callback function type for receiving structured log records.
/// Allows an embedding application to route Triton's logs into its own
/// pipeline instead of parsing formatted stderr/file output.
///
/// \param level The log level.
/// \param filename The file name of the location of the log message.
/// \param line The line number of the log message.
/// \param timestamp_us The timestamp associated with the log record.
/// \param message The raw, unescaped log text. The host owns any formatting.
/// \param userp The user data pointer.
///
/// Invoked synchronously on the producing thread. Must be lightweight,
/// thread-safe, and must never throw.
typedef void (*TRITONSERVER_LogCallbackFn_t)(
TRITONSERVER_LogLevel level, const char* filename, int64_t line,
uint64_t timestamp_us, const char* message, void* userp);

/// TRITONSERVER_Error
///
/// Errors are reported by a TRITONSERVER_Error object. A NULL
Expand Down Expand Up @@ -2139,6 +2156,20 @@ TRITONSERVER_DECLSPEC struct TRITONSERVER_Error*
TRITONSERVER_ServerOptionsSetLogVerbose(
struct TRITONSERVER_ServerOptions* options, int level);

/// Registers a callback to receive Triton log records as structured fields,
/// bypassing the default stderr/file sink. Allows an embedding application
/// to route Triton's logs into its own logging pipeline.
/// Pass nullptr to clear a previously registered callback.
///
/// \param options The server options object.
/// \param callback The callback to invoke per log record, or nullptr to clear.
/// \param userp The user data pointer.
/// \return a TRITONSERVER_Error indicating success or failure.
TRITONSERVER_DECLSPEC struct TRITONSERVER_Error*
TRITONSERVER_ServerOptionsSetLogCallback(
struct TRITONSERVER_ServerOptions* options,
TRITONSERVER_LogCallbackFn_t callback, void* userp);

/// Enable or disable metrics collection in a server options.
///
/// \param options The server options object.
Expand Down
14 changes: 11 additions & 3 deletions python/test/test_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ def execute(self, requests):
'''


def _is_model_ready(server, name, version=-1):
# A never-loaded or unloaded model reports not-ready by raising TritonError.
try:
return server.model_is_ready(name, version)
except triton_bindings.TritonError:
return False


# ======================================= Test cases ===========================
class TestBindings:
@pytest.fixture(autouse=True, scope="function")
Expand Down Expand Up @@ -989,7 +997,7 @@ def test_server_explicit(self):
assert server.model_is_ready("wired_addsub", -1)

# Model Repository
assert not (server.model_is_ready(self._model_name, -1))
assert not _is_model_ready(server, self._model_name)
# unregister
server.unregister_model_repository(self._test_model_repo)
with pytest.raises(triton_bindings.TritonError):
Expand All @@ -1001,9 +1009,9 @@ def test_server_explicit(self):

# unload
server.unload_model("wired_addsub")
assert not (server.model_is_ready("wired_addsub", -1))
assert not _is_model_ready(server, "wired_addsub")
server.unload_model_and_dependents(self._model_name)
assert not (server.model_is_ready(self._model_name, -1))
assert not _is_model_ready(server, self._model_name)

def test_custom_metric(self):
options = triton_bindings.TRITONSERVER_ServerOptions()
Expand Down
113 changes: 113 additions & 0 deletions python/test/test_logging_callback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Copyright 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of NVIDIA CORPORATION nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


import pytest
import tritonserver
from tritonserver import _c as triton_bindings


@pytest.fixture(autouse=True)
def _clear_log_callback():
# Logging is process-global in Triton, clear the callback after each test so
# one test's callback cannot leak into the next.
yield
triton_bindings.TRITONSERVER_ServerOptions().set_log_callback(None)


@pytest.fixture
def options():
return triton_bindings.TRITONSERVER_ServerOptions()


def _create_callback():
"""Return (callback, records); the callback appends each record to records."""
records = []

def _callback(level, filename, line, timestamp_us, message):
records.append((level, filename, line, timestamp_us, message))

return _callback, records


def _emit(level, message, filename="logcb_test.py", line=1):
triton_bindings.TRITONSERVER_LogMessage(level, filename, line, message)


class TestLogCallback:
"""Tests for the structured log callback added via
TRITONSERVER_ServerOptionsSetLogCallback and exposed as Options.log_callback.
"""

def test_binding_receives_structured_record(self, options):
callback, records = _create_callback()
options.set_log_callback(callback)

_emit(
triton_bindings.TRITONSERVER_LogLevel.ERROR,
"callback-record",
filename="model.cc",
line=42,
)

assert records, "log callback was not invoked"
level, filename, line, _ts, message = records[-1]
assert level == triton_bindings.TRITONSERVER_LogLevel.ERROR
assert filename == "model.cc"
assert line == 42
assert message == "callback-record"

def test_binding_clear_stops_delivery(self, options):
callback, records = _create_callback()
options.set_log_callback(callback)
options.set_log_callback(None) # clear

_emit(triton_bindings.TRITONSERVER_LogLevel.ERROR, "should-be-dropped")
assert not records

def test_binding_callback_exceptions_do_not_propagate(self, options):
# A throwing callback must not crash logging or raise to the caller.
def _raise(*args):
raise RuntimeError("error in callback")

options.set_log_callback(_raise)
_emit(triton_bindings.TRITONSERVER_LogLevel.ERROR, "trigger-throwing-callback")

def test_option_applies_callback(self):
callback, records = _create_callback()
options = tritonserver.Options(
# Not started, so the repository path is only stored, never read.
model_repository="/tmp/triton-log-callback-test",
# log_info keeps all levels enabled (the logger default), so this
# does not disable any level process-wide for other tests.
log_info=True,
log_callback=callback,
)
# Apply the dataclass options to the global logger without starting a server.
options._create_tritonserver_server_options()

_emit(triton_bindings.TRITONSERVER_LogLevel.INFO, "via-options")
assert any(message == "via-options" for *_, message in records)
14 changes: 13 additions & 1 deletion python/tritonserver/_api/_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import json
import time
from dataclasses import dataclass, field
from typing import Annotated, Any, Optional
from typing import Annotated, Any, Callable, Optional

from tritonserver._api._model import Model
from tritonserver._c.triton_bindings import InvalidArgumentError
Expand Down Expand Up @@ -229,6 +229,15 @@ class Options:
Verbose logging level. Level zero disables logging.
See :c:func:`TRITONSERVER_ServerOptionsSetLogVerbose`

log_callback : Optional[Callable], default None
Callable invoked for each enabled log record as
``callback(level, filename, line, timestamp_us, message)`` where
``level`` is a :class:`LogLevel`, ``timestamp_us`` is a timestamp
associated with the log record, and ``message`` is the raw log text.
When set, records are routed only to the callback and Triton's
default stderr/file output is bypassed (the host owns the single stream).
See :c:func:`TRITONSERVER_ServerOptionsSetLogCallback`

metrics : bool, default True
Enable or disable metric collection.
See :c:func:`TRITONSERVER_ServerOptionsSetMetrics`
Expand Down Expand Up @@ -312,6 +321,7 @@ class Options:
log_error: bool = False
log_format: LogFormat = LogFormat.DEFAULT
log_verbose: uint = 0
log_callback: Optional[Callable] = None

metrics: bool = True
gpu_metrics: bool = True
Expand Down Expand Up @@ -398,6 +408,8 @@ def _create_tritonserver_server_options(
options.set_log_error(self.log_error)
options.set_log_format(self.log_format)
options.set_log_verbose(self.log_verbose)
if self.log_callback is not None:
options.set_log_callback(self.log_callback)
options.set_metrics(self.metrics)
options.set_cpu_metrics(self.cpu_metrics)
options.set_gpu_metrics(self.gpu_metrics)
Expand Down
55 changes: 55 additions & 0 deletions python/tritonserver/_c/tritonserver_pybind.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1447,6 +1447,55 @@ class PyServerOptions : public PyWrapper<struct TRITONSERVER_ServerOptions> {
ThrowIfError(
TRITONSERVER_ServerOptionsSetLogVerbose(triton_object_, level));
}

// Registers a Python callable to receive structured log records as
// callback(level, filename, line, timestamp_us, message).
// While registered, bypasses Triton's default stderr/file sink entirely.
void SetLogCallback(py::object callback)
{
// The logger is process-global and outlives any Options/Server instance.
// Intentionally leak the holder to avoid interpreter-finalization ordering
// issues. The most recent registration wins, consistent with other log
// options.
static py::object* log_callback_holder = new py::object();

if (callback.is_none()) {
ThrowIfError(TRITONSERVER_ServerOptionsSetLogCallback(
triton_object_, nullptr, nullptr));
*log_callback_holder = py::none();
return;
}

*log_callback_holder = std::move(callback);

// Acquires the GIL before entering Python since the logging thread does
// not hold it. Exceptions must not propagate into Triton's C++ logging
// path.
TRITONSERVER_LogCallbackFn_t trampoline =
[](TRITONSERVER_LogLevel level, const char* filename, int64_t line,
uint64_t timestamp_us, const char* message, void* userp) {
if (!Py_IsInitialized()) {
return;
}
py::gil_scoped_acquire gil;
try {
auto* fn = reinterpret_cast<py::object*>(userp);
(*fn)(level, filename, line, timestamp_us, message);
}
catch (py::error_already_set& e) {
// Report via Python's unraisable hook and clear the error
// indicator.
e.discard_as_unraisable("Triton log callback");
}
catch (...) {
}
};

ThrowIfError(TRITONSERVER_ServerOptionsSetLogCallback(
triton_object_, trampoline,
reinterpret_cast<void*>(log_callback_holder)));
}

void SetMetrics(bool metrics)
{
ThrowIfError(TRITONSERVER_ServerOptionsSetMetrics(triton_object_, metrics));
Expand Down Expand Up @@ -1520,6 +1569,11 @@ class PyServer : public PyWrapper<struct TRITONSERVER_Server> {

PyServer(PyServerOptions& options)
{
// TRITONSERVER_ServerNew blocks while loading the model repository.
// Internal threads may invoke a registered log callback that acquires
// the GIL, holding it here would deadlock. Release it for the duration,
// consistent with LoadModel() and Stop().
py::gil_scoped_release release;
ThrowIfError(TRITONSERVER_ServerNew(&triton_object_, options.Ptr()));
owned_ = true;
}
Expand Down Expand Up @@ -2134,6 +2188,7 @@ PYBIND11_MODULE(triton_bindings, m)
.def("set_log_error", &PyServerOptions::SetLogError)
.def("set_log_format", &PyServerOptions::SetLogFormat)
.def("set_log_verbose", &PyServerOptions::SetLogVerbose)
.def("set_log_callback", &PyServerOptions::SetLogCallback)
.def("set_metrics", &PyServerOptions::SetMetrics)
.def("set_gpu_metrics", &PyServerOptions::SetGpuMetrics)
.def("set_cpu_metrics", &PyServerOptions::SetCpuMetrics)
Expand Down
46 changes: 46 additions & 0 deletions src/tritonserver.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1566,6 +1566,52 @@ TRITONSERVER_ServerOptionsSetLogFormat(
return nullptr; // Success
}

TRITONAPI_DECLSPEC TRITONSERVER_Error*
TRITONSERVER_ServerOptionsSetLogCallback(
TRITONSERVER_ServerOptions* options, TRITONSERVER_LogCallbackFn_t callback,
void* userp)
{
#ifdef TRITON_ENABLE_LOGGING
if (callback == nullptr) {
// Clear any previously registered callback.
LOG_SET_CALLBACK(triton::common::Logger::LogCallbackFn());
return nullptr; // Success
}

triton::common::Logger::LogCallbackFn fn =
[callback, userp](
triton::common::Logger::Level level, bool is_verbose,
const char* file, int line, uint64_t timestamp_us,
const char* message) {
TRITONSERVER_LogLevel c_level;
if (is_verbose) {
c_level = TRITONSERVER_LOG_VERBOSE;
} else {
switch (level) {
case triton::common::Logger::Level::kERROR:
c_level = TRITONSERVER_LOG_ERROR;
break;
case triton::common::Logger::Level::kWARNING:
c_level = TRITONSERVER_LOG_WARN;
break;
case triton::common::Logger::Level::kINFO:
default:
c_level = TRITONSERVER_LOG_INFO;
break;
}
}
callback(
c_level, file, static_cast<int64_t>(line), timestamp_us, message,
userp);
};
LOG_SET_CALLBACK(fn);
return nullptr; // Success
#else
return TRITONSERVER_ErrorNew(
TRITONSERVER_ERROR_UNSUPPORTED, "logging not supported");
#endif // TRITON_ENABLE_LOGGING
}

TRITONAPI_DECLSPEC TRITONSERVER_Error*
TRITONSERVER_ServerOptionsSetMetrics(
TRITONSERVER_ServerOptions* options, bool metrics)
Expand Down
Loading