├── .gitignore ├── CMakeLists.txt ├── CMakePresets.json ├── LICENSE ├── README.md ├── images └── log_test.png ├── include └── pybind11_log.h └── tests ├── log_test.py └── pybind11_log_test.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | cmake_policy(SET CMP0057 NEW) 3 | 4 | project(PyBind11Log) 5 | 6 | include_directories(include) 7 | 8 | find_package(Python3 COMPONENTS Interpreter Development) 9 | find_package(pybind11 CONFIG REQUIRED) 10 | find_package(spdlog CONFIG REQUIRED) 11 | 12 | pybind11_add_module(pybind11_log_test tests/pybind11_log_test.cpp) 13 | target_link_libraries(pybind11_log_test PRIVATE spdlog::spdlog_header_only) 14 | target_link_libraries(pybind11_log_test PRIVATE fmt::fmt-header-only) 15 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "configurePresets": [ 4 | { 5 | "name": "vcpkg", 6 | "generator": "Ninja", 7 | "binaryDir": "${sourceDir}/build", 8 | "cacheVariables": { 9 | "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Haibao Tang 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pybind11_log 2 | 3 | A bridge from C++ to Python logging. Inspired by 4 | [pyo3_log](https://docs.rs/pyo3-log/latest/pyo3_log/). 5 | 6 | This works by routing the logging calls, e.g. `spdlog::info`, to the Python 7 | `logging` module. This is done by setting the `spdlog::logger`'s `sink` to a 8 | custom router sink. The router sink then calls the Python `logging` module. 9 | 10 | For thread-safe logging, please use `pybind11_log::init_mt()` to initialize 11 | the logger. For single-threaded applications, use `pybind11_log::init_st()`. 12 | 13 | ## Usage 14 | 15 | The library is header-only, so you can just include the header file 16 | `include/pybind11_log.h` in your project. 17 | 18 | ```cpp 19 | #include "pybind11_log.h" 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | int add(int i, int j) { 27 | spdlog::info("add({0}, {1})", i, j); 28 | return i + j; 29 | } 30 | 31 | int divide(int i, int j) { 32 | if (j == 0) { 33 | spdlog::error("Division by zero!"); 34 | throw std::invalid_argument("Division by zero!"); 35 | } else { 36 | spdlog::info("divide({0}, {1})", i, j); 37 | } 38 | return i / j; 39 | } 40 | 41 | void sleep_and_log() { 42 | spdlog::info("thread: will sleep for 1s"); 43 | sleep(1); 44 | spdlog::info("thread: sleep completed"); 45 | } 46 | 47 | void threaded_log() { 48 | py::gil_scoped_release release; // Release the GIL when working with threads 49 | spdlog::info("threaded_log() starts"); 50 | std::thread t1(sleep_and_log); 51 | t1.join(); 52 | spdlog::info("threaded_log() ends"); 53 | } 54 | 55 | void init(const std::string& logger_name) { 56 | pybind11_log::init_mt(logger_name); 57 | } 58 | 59 | PYBIND11_MODULE(pybind11_log_test, m) { 60 | m.doc() = "pybind11_log example plugin"; 61 | m.def("init", &init, "Initialize the logger"); 62 | m.def("add", &add, "A function which adds two numbers with logging"); 63 | m.def("divide", ÷, "A function which divides two numbers with logging"); 64 | m.def("threaded_log", &threaded_log, 65 | "A function which sleeps and log in a thread"); 66 | } 67 | ``` 68 | 69 | You can then test that the logging works by running the following Python code: 70 | 71 | ```python 72 | import logging 73 | import os 74 | import sys 75 | 76 | sys.path.append(os.getcwd()) 77 | 78 | from rich.logging import RichHandler 79 | 80 | logging.basicConfig( 81 | level="NOTSET", format="%(name)s %(message)s", datefmt="[%X]", handlers=[RichHandler()] 82 | ) 83 | 84 | import pybind11_log_test 85 | 86 | if __name__ == "__main__": 87 | pybind11_log_test.init("my_fancy_logger") 88 | pybind11_log_test.add(3, 4) 89 | pybind11_log_test.threaded_log() 90 | pybind11_log_test.divide(3, 0) 91 | ``` 92 | 93 | As shown in the terminal output below, the logging calls are routed to Python 94 | `logging` from the C++ code. 95 | 96 | ![log_test](images/log_test.png) -------------------------------------------------------------------------------- /images/log_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanghaibao/pybind11_log/974d58a890b368aafb6094bee670ecd5f9e63aaf/images/log_test.png -------------------------------------------------------------------------------- /include/pybind11_log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | namespace py = pybind11; 12 | 13 | namespace pybind11_log { 14 | 15 | enum LevelFilter : int { 16 | Off = 0, 17 | Trace = 5, 18 | Debug = 10, 19 | Info = 20, 20 | Warn = 30, 21 | Error = 40, 22 | Critical = 50, 23 | }; 24 | 25 | /// Map from spdlog logging level to Python logging level 26 | LevelFilter map_level(spdlog::level::level_enum level) { 27 | switch (level) { 28 | case spdlog::level::trace: 29 | return LevelFilter::Trace; 30 | case spdlog::level::debug: 31 | return LevelFilter::Debug; 32 | case spdlog::level::info: 33 | return LevelFilter::Info; 34 | case spdlog::level::warn: 35 | return LevelFilter::Warn; 36 | case spdlog::level::err: 37 | return LevelFilter::Error; 38 | case spdlog::level::critical: 39 | return LevelFilter::Critical; 40 | case spdlog::level::off: 41 | default: 42 | return LevelFilter::Off; 43 | } 44 | } 45 | 46 | bool is_enabled(py::object py_logger, int level) { 47 | return py_logger.attr("isEnabledFor")(level).cast(); 48 | } 49 | 50 | template 51 | class pybind11_sink : public spdlog::sinks::base_sink { 52 | public: 53 | void sink_it_(const spdlog::details::log_msg& msg) override { 54 | // Acquire GIL to interact with Python interpreter 55 | py::gil_scoped_acquire acquire; 56 | if (py_logger_.is_none()) { 57 | auto py_logging = py::module::import("logging"); 58 | py_logger_ = py_logging.attr("getLogger")(name_); 59 | } 60 | std::string filename = msg.source.filename ? msg.source.filename : ""; 61 | std::string msg_payload = 62 | std::string(msg.payload.begin(), msg.payload.end()); 63 | 64 | int level = static_cast(map_level(msg.level)); 65 | 66 | if (is_enabled(py_logger_, level)) { 67 | auto record = py_logger_.attr("makeRecord")( 68 | name_, level, filename, 69 | msg.source.line, msg_payload, py::none(), py::none()); 70 | py_logger_.attr("handle")(record); 71 | } 72 | } 73 | 74 | void flush_() override {} 75 | 76 | void set_name(const std::string& logger_name) { name_ = logger_name; } 77 | 78 | private: 79 | py::object py_logger_ = py::none(); 80 | std::string name_; 81 | }; 82 | 83 | using pybind11_sink_mt = pybind11_sink; 84 | using pybind11_sink_st = pybind11_sink; 85 | 86 | template 88 | SPDLOG_INLINE std::shared_ptr pybind11_helper( 89 | const std::string& logger_name) { 90 | auto ptr = Factory::template create(logger_name); 91 | auto sink_ptr = std::dynamic_pointer_cast(ptr->sinks().back()); 92 | sink_ptr->set_name(logger_name); 93 | return ptr; 94 | } 95 | 96 | std::shared_ptr pybind11_mt(const std::string& logger_name) { 97 | return pybind11_helper<>(logger_name); 98 | } 99 | 100 | std::shared_ptr pybind11_st(const std::string& logger_name) { 101 | return pybind11_helper( 102 | logger_name); 103 | } 104 | 105 | /// Initialize a multi-threaded logger 106 | void init_mt(const std::string& logger_name) { 107 | auto logger = pybind11_mt(logger_name); 108 | spdlog::set_default_logger(logger); 109 | } 110 | 111 | /// Initialize a single-threaded logger 112 | void init_st(const std::string& logger_name) { 113 | auto logger = pybind11_st(logger_name); 114 | spdlog::set_default_logger(logger); 115 | } 116 | } // namespace pybind11_log 117 | -------------------------------------------------------------------------------- /tests/log_test.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | sys.path.append(os.getcwd()) 6 | 7 | from rich.logging import RichHandler 8 | 9 | logging.basicConfig( 10 | level="NOTSET", format="%(name)s %(message)s", datefmt="[%X]", handlers=[RichHandler()] 11 | ) 12 | 13 | import pybind11_log_test 14 | 15 | if __name__ == "__main__": 16 | pybind11_log_test.init("my_fancy_logger") 17 | pybind11_log_test.add(3, 4) 18 | pybind11_log_test.threaded_log() 19 | pybind11_log_test.divide(3, 0) 20 | -------------------------------------------------------------------------------- /tests/pybind11_log_test.cpp: -------------------------------------------------------------------------------- 1 | #include "pybind11_log.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | int add(int i, int j) { 9 | spdlog::info("add({0}, {1})", i, j); 10 | return i + j; 11 | } 12 | 13 | int divide(int i, int j) { 14 | if (j == 0) { 15 | spdlog::error("Division by zero!"); 16 | throw std::invalid_argument("Division by zero!"); 17 | } else { 18 | spdlog::info("divide({0}, {1})", i, j); 19 | } 20 | return i / j; 21 | } 22 | 23 | void sleep_and_log() { 24 | spdlog::info("thread: will sleep for 1s"); 25 | sleep(1); 26 | spdlog::info("thread: sleep completed"); 27 | } 28 | 29 | void threaded_log() { 30 | py::gil_scoped_release release; // Release the GIL when working with threads 31 | spdlog::info("threaded_log() starts"); 32 | std::thread t1(sleep_and_log); 33 | t1.join(); 34 | spdlog::info("threaded_log() ends"); 35 | } 36 | 37 | void init(const std::string& logger_name) { 38 | pybind11_log::init_mt(logger_name); 39 | } 40 | 41 | PYBIND11_MODULE(pybind11_log_test, m) { 42 | m.doc() = "pybind11_log example plugin"; 43 | m.def("init", &init, "Initialize the logger"); 44 | m.def("add", &add, "A function which adds two numbers with logging"); 45 | m.def("divide", ÷, "A function which divides two numbers with logging"); 46 | m.def("threaded_log", &threaded_log, 47 | "A function which sleeps and log in a thread"); 48 | } 49 | --------------------------------------------------------------------------------