|
| 1 | +#include "duckdb_python/python_log_storage.hpp" |
| 2 | +#include "duckdb_python/pybind11/pybind_wrapper.hpp" |
| 3 | +#include "duckdb/common/types/data_chunk.hpp" |
| 4 | +#include "duckdb/common/types/vector.hpp" |
| 5 | +#include "duckdb/logging/logging.hpp" |
| 6 | + |
| 7 | +namespace duckdb { |
| 8 | + |
| 9 | +static int LogLevelToPython(LogLevel level) { |
| 10 | + switch (level) { |
| 11 | + case LogLevel::LOG_TRACE: |
| 12 | + case LogLevel::LOG_DEBUG: |
| 13 | + return 10; // logging.DEBUG |
| 14 | + case LogLevel::LOG_INFO: |
| 15 | + return 20; // logging.INFO |
| 16 | + case LogLevel::LOG_WARNING: |
| 17 | + return 30; // logging.WARNING |
| 18 | + case LogLevel::LOG_ERROR: |
| 19 | + return 40; // logging.ERROR |
| 20 | + case LogLevel::LOG_FATAL: |
| 21 | + return 50; // logging.CRITICAL |
| 22 | + default: |
| 23 | + return 30; |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +static int LevelStringToPython(const string &level_str) { |
| 28 | + if (level_str == "TRACE" || level_str == "DEBUG") { |
| 29 | + return 10; |
| 30 | + } |
| 31 | + if (level_str == "INFO") { |
| 32 | + return 20; |
| 33 | + } |
| 34 | + if (level_str == "WARNING") { |
| 35 | + return 30; |
| 36 | + } |
| 37 | + if (level_str == "ERROR") { |
| 38 | + return 40; |
| 39 | + } |
| 40 | + if (level_str == "FATAL") { |
| 41 | + return 50; |
| 42 | + } |
| 43 | + return 30; |
| 44 | +} |
| 45 | + |
| 46 | +// Both write methods run on engine worker threads and invoke arbitrary user Python (the |
| 47 | +// handlers installed on the "duckdb" logger). The engine calls these directly from query |
| 48 | +// binding/execution with NO surrounding try/catch (see LogManager::WriteLogEntry), so an |
| 49 | +// exception escaping here would fail the user's query. Logging is a side effect — it must |
| 50 | +// never do that. Hence every body swallows all exceptions. |
| 51 | +// |
| 52 | +// Note also that the engine holds LogManager::lock (a non-recursive mutex) across this call. |
| 53 | +// A handler that re-enters DuckDB on the same thread and emits another log entry would |
| 54 | +// self-deadlock on that lock — outside our control, but worth knowing. |
| 55 | + |
| 56 | +void PythonLogStorage::WriteLogEntry(timestamp_t, LogLevel level, const string &, const string &log_message, |
| 57 | + const RegisteredLoggingContext &) { |
| 58 | + if (!Py_IsInitialized()) { |
| 59 | + return; // interpreter is finalizing — acquiring the GIL would crash |
| 60 | + } |
| 61 | + try { |
| 62 | + py::gil_scoped_acquire gil; |
| 63 | + auto logging = py::module::import("logging"); |
| 64 | + auto logger = logging.attr("getLogger")("duckdb"); |
| 65 | + logger.attr("log")(LogLevelToPython(level), log_message); |
| 66 | + } catch (...) { |
| 67 | + // Logging must not disrupt query execution. |
| 68 | + } |
| 69 | +} |
| 70 | + |
| 71 | +void PythonLogStorage::WriteLogEntries(DataChunk &chunk, const RegisteredLoggingContext &) { |
| 72 | + if (!Py_IsInitialized()) { |
| 73 | + return; // interpreter is finalizing — acquiring the GIL would crash |
| 74 | + } |
| 75 | + try { |
| 76 | + py::gil_scoped_acquire gil; |
| 77 | + auto logging = py::module::import("logging"); |
| 78 | + auto logger = logging.attr("getLogger")("duckdb"); |
| 79 | + // DataChunk is in LOG_ENTRIES format: context_id, timestamp, type, log_level, message. |
| 80 | + // log_level (idx 3) and message (idx 4) are both VARCHAR; the chunk is freshly |
| 81 | + // allocated by the engine so the vectors are flat. |
| 82 | + auto level_data = FlatVector::GetData<string_t>(chunk.data[3]); |
| 83 | + auto message_data = FlatVector::GetData<string_t>(chunk.data[4]); |
| 84 | + for (idx_t i = 0; i < chunk.size(); i++) { |
| 85 | + logger.attr("log")(LevelStringToPython(level_data[i].GetString()), message_data[i].GetString()); |
| 86 | + } |
| 87 | + } catch (...) { |
| 88 | + // Logging must not disrupt query execution. |
| 89 | + } |
| 90 | +} |
| 91 | + |
| 92 | +} // namespace duckdb |
0 commit comments