diff --git a/include/triton/common/logging.h b/include/triton/common/logging.h index bb5a9b3..4d2ce4f 100644 --- a/include/triton/common/logging.h +++ b/include/triton/common/logging.h @@ -1,4 +1,4 @@ -// Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2018-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 @@ -27,8 +27,10 @@ #include #include +#include #include #include +#include #include #include #include @@ -140,6 +142,30 @@ class Logger { return std::string(); } + // Delivers each enabled log record as structured fields to an embedding host + // (e.g. Dynamo), bypassing Triton's default stderr/file sink entirely. + // + // The callback is invoked on the producing thread, outside the output mutex. + // It must be lightweight, thread-safe, and must never throw. + typedef std::function + LogCallbackFn; + + // Registers the log callback, or clears it when passed an empty function. + // While set, log records are delivered only to the callback and the default + // stdout/stderr/file sink is bypassed. + // + // Access to the callback is not thread-safe and this class enforces no + // set-once policy, so it must be set before any thread that logs is started. + // In the Triton server, TRITONSERVER_ServerNew sets it during server + // initialization. + void SetLogCallback(LogCallbackFn callback) + { + callback_ = std::move(callback); + } + const LogCallbackFn& LogCallback() const { return callback_; } + // Log a message. void Log(const std::string& msg, const Logger::Level level); @@ -156,6 +182,7 @@ class Logger { std::mutex mutex_; std::string filename_; std::ofstream file_stream_; + LogCallbackFn callback_; }; extern Logger gLogger_; @@ -179,6 +206,14 @@ class LogMessage { ~LogMessage(); + // Marks this record as verbose so a structured callback can distinguish + // LOG_VERBOSE from plain INFO. Has no effect on the default sink. + LogMessage& SetVerbose() + { + is_verbose_ = true; + return *this; + } + std::stringstream& stream() { return message_; } private: @@ -204,6 +239,7 @@ class LogMessage { std::stringstream message_; const char* heading_; bool escape_log_messages_; + bool is_verbose_ = false; }; #define LOG_ENABLE_INFO(E) \ @@ -219,6 +255,7 @@ class LogMessage { static_cast(std::max(0, (L)))) #define LOG_SET_OUT_FILE(FN) triton::common::gLogger_.SetLogFile((FN)) #define LOG_SET_FORMAT(F) triton::common::gLogger_.SetLogFormat((F)) +#define LOG_SET_CALLBACK(CB) triton::common::gLogger_.SetLogCallback((CB)) #define LOG_VERBOSE_LEVEL triton::common::gLogger_.VerboseLevel() #define LOG_FORMAT triton::common::gLogger_.LogFormat() @@ -265,6 +302,7 @@ class LogMessage { if (LOG_VERBOSE_IS_ON(L)) \ triton::common::LogMessage( \ (char*)(FN), LN, triton::common::Logger::Level::kINFO) \ + .SetVerbose() \ .stream() // Macros that use current filename and line number. @@ -286,6 +324,7 @@ class LogMessage { triton::common::LogMessage( \ __FILE__, __LINE__, triton::common::Logger::Level::kINFO, nullptr, \ false) \ + .SetVerbose() \ .stream() \ << TABLE.PrintTable(); \ } while (false) @@ -312,6 +351,7 @@ class LogMessage { triton::common::LogMessage( \ __FILE__, __LINE__, triton::common::Logger::Level::kINFO, HEADING, \ false) \ + .SetVerbose() \ .stream() \ << PB_MESSAGE.DebugString(); \ } while (false) diff --git a/src/logging.cc b/src/logging.cc index 89f162c..1be5e27 100644 --- a/src/logging.cc +++ b/src/logging.cc @@ -1,4 +1,4 @@ -// Copyright 2018-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// Copyright 2018-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 @@ -38,6 +38,10 @@ namespace triton { namespace common { +namespace { +constexpr uint64_t kMicrosecondsPerSecond = 1000000ULL; +} // namespace + Logger gLogger_; Logger::Logger() @@ -142,6 +146,34 @@ LogMessage::LogPreamble(std::stringstream& stream) LogMessage::~LogMessage() { + // If a structured callback is registered, send the raw Triton log record to + // it and skip the default sink. This allows the host to route Triton logs + // through its own logging pipeline as a single, consistent stream. + // The callback receives the unformatted message and structured metadata, and + // is responsible for any formatting or escaping. Invoke it outside the logger + // output mutex so a slow callback does not block all logging. It must never + // throw. + const Logger::LogCallbackFn& callback = gLogger_.LogCallback(); + if (callback) { + uint64_t timestamp_us = + static_cast(timestamp_.tv_sec) * kMicrosecondsPerSecond + + static_cast(timestamp_.tv_usec); + const std::string message = message_.str(); + const std::string raw_message = + (heading_ != nullptr) ? (std::string(heading_) + "\n" + message) + : message; + try { + callback( + level_, is_verbose_, path_.c_str(), line_, timestamp_us, + raw_message.c_str()); + } + catch (...) { + // Logging must not fail or terminate the server. + } + return; + } + + // Default sink: format the log record and write it to the configured output. std::stringstream log_record; LogPreamble(log_record); std::string escaped_message = diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 8a9390e..f7295f6 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-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 @@ -39,5 +39,6 @@ FetchContent_MakeAvailable(googletest) if (TRITON_COMMON_ENABLE_JSON) add_subdirectory(triton_json triton_json) + add_subdirectory(logging logging) endif() diff --git a/src/test/logging/CMakeLists.txt b/src/test/logging/CMakeLists.txt new file mode 100644 index 0000000..f723ad3 --- /dev/null +++ b/src/test/logging/CMakeLists.txt @@ -0,0 +1,48 @@ +# 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. + +cmake_minimum_required (VERSION 3.31.8) + +add_executable(triton-logging-test logging_test.cc) + +target_link_libraries( + triton-logging-test + PRIVATE + triton-common-logging + GTest::gtest + GTest::gtest_main +) + +set_target_properties( + triton-logging-test + PROPERTIES + OUTPUT_NAME logging_test +) + +install( + TARGETS triton-logging-test + RUNTIME DESTINATION bin + ) diff --git a/src/test/logging/logging_test.cc b/src/test/logging/logging_test.cc new file mode 100644 index 0000000..278fd96 --- /dev/null +++ b/src/test/logging/logging_test.cc @@ -0,0 +1,179 @@ +// 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. + +#include "triton/common/logging.h" + +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" + +namespace tc = triton::common; + +namespace { + +// A single log record as delivered to the callback. +struct CapturedRecord { + tc::Logger::Level level; + bool is_verbose; + std::string file; + int line; + uint64_t timestamp_us; + std::string message; +}; + +// Returns a callback that appends each record it receives to `records`. +tc::Logger::LogCallbackFn +MakeCapturingCallback(std::vector& records) +{ + return [&records]( + tc::Logger::Level level, bool is_verbose, const char* file, + int line, uint64_t ts, const char* msg) { + records.push_back( + {level, is_verbose, file ? file : "", line, ts, msg ? msg : ""}); + }; +} + +// Runs `fn` with std::cout redirected to `buf`, restores cout even if `fn` +// throws. +template +void +WithCapturedStdout(std::ostringstream& buf, Fn&& fn) +{ + std::streambuf* prev = std::cout.rdbuf(buf.rdbuf()); + try { + fn(); + } + catch (...) { + std::cout.rdbuf(prev); + throw; + } + std::cout.rdbuf(prev); +} + +// The logger is a process-global singleton, clear the callback after each test. +class LogCallbackTest : public ::testing::Test { + protected: + void TearDown() override + { + tc::gLogger_.SetLogCallback(tc::Logger::LogCallbackFn()); + } +}; + + +// Validate that a registered callback receives all structured log fields +// (level, verbose flag, file, line, raw message) intact. +TEST_F(LogCallbackTest, ReceivesCorrectRecordFields) +{ + std::vector records; + tc::gLogger_.SetLogCallback(MakeCapturingCallback(records)); + EXPECT_TRUE(tc::gLogger_.LogCallback() != nullptr); + + // ERROR, non-verbose: every structured field is forwarded correctly. + const int emit_line = __LINE__ + 1; + tc::LogMessage(__FILE__, emit_line, tc::Logger::Level::kERROR).stream() + << "structured-callback-test"; + + ASSERT_EQ(records.size(), 1u); + { + const auto& r = records[0]; + EXPECT_EQ(r.level, tc::Logger::Level::kERROR); + EXPECT_FALSE(r.is_verbose); + EXPECT_EQ(r.line, emit_line); + // LogMessage trims the path to its basename. + EXPECT_NE(r.file.find("logging_test"), std::string::npos); + // The message reaches the callback raw (unescaped). + EXPECT_EQ(r.message, "structured-callback-test"); + } + + // INFO with the verbose flag set is reported as verbose. + tc::LogMessage(__FILE__, __LINE__, tc::Logger::Level::kINFO) + .SetVerbose() + .stream() + << "verbose-callback-test"; + + ASSERT_EQ(records.size(), 2u); + { + const auto& r = records[1]; + EXPECT_EQ(r.level, tc::Logger::Level::kINFO); + EXPECT_TRUE(r.is_verbose); + } +} + +// Validate that when a callback is registered, log records are sent only +// to the callback (not to the default stdout/stderr sink), once the callback is +// cleared, records return to the default sink. +TEST_F(LogCallbackTest, CallbackAndDefaultSinkAreMutuallyExclusive) +{ + // Registered: the record goes only to the callback, never to std::cout. + int count = 0; + tc::gLogger_.SetLogCallback([&count]( + tc::Logger::Level, bool, const char*, int, + uint64_t, const char*) { ++count; }); + + std::ostringstream captured; + WithCapturedStdout(captured, [&] { + tc::LogMessage(__FILE__, __LINE__, tc::Logger::Level::kINFO).stream() + << "callback-only"; + }); + + EXPECT_EQ(count, 1); + EXPECT_TRUE(captured.str().empty()); // default sink skipped + + // Cleared: records return to the default sink and the callback stops firing. + tc::gLogger_.SetLogCallback(tc::Logger::LogCallbackFn()); + EXPECT_FALSE(tc::gLogger_.LogCallback() != nullptr); + + captured.str(""); + WithCapturedStdout(captured, [&] { + tc::LogMessage(__FILE__, __LINE__, tc::Logger::Level::kINFO).stream() + << "after-clear"; + }); + + EXPECT_EQ(count, 1); // callback not invoked after clear + EXPECT_NE(captured.str().find("after-clear"), std::string::npos); +} + +// Validate that any exception thrown by the callback does not escape +// LogMessage's destructor, ensuring logging can never crash the process. +TEST_F(LogCallbackTest, ThrowingCallbackDoesNotEscapeDestructor) +{ + // A callback that throws must never propagate out of LogMessage's destructor. + tc::gLogger_.SetLogCallback( + [](tc::Logger::Level, bool, const char*, int, uint64_t, const char*) { + throw std::runtime_error("callback failure"); + }); + + EXPECT_NO_THROW({ + tc::LogMessage(__FILE__, __LINE__, tc::Logger::Level::kINFO).stream() + << "boom"; + }); +} + +} // namespace