LSST Applications g0b6bd0c080+a72a5dd7e6,g1182afd7b4+2a019aa3bb,g17e5ecfddb+2b8207f7de,g1d67935e3f+06cf436103,g38293774b4+ac198e9f13,g396055baef+6a2097e274,g3b44f30a73+6611e0205b,g480783c3b1+98f8679e14,g48ccf36440+89c08d0516,g4b93dc025c+98f8679e14,g5c4744a4d9+a302e8c7f0,g613e996a0d+e1c447f2e0,g6c8d09e9e7+25247a063c,g7271f0639c+98f8679e14,g7a9cd813b8+124095ede6,g9d27549199+a302e8c7f0,ga1cf026fa3+ac198e9f13,ga32aa97882+7403ac30ac,ga786bb30fb+7a139211af,gaa63f70f4e+9994eb9896,gabf319e997+ade567573c,gba47b54d5d+94dc90c3ea,gbec6a3398f+06cf436103,gc6308e37c7+07dd123edb,gc655b1545f+ade567573c,gcc9029db3c+ab229f5caf,gd01420fc67+06cf436103,gd877ba84e5+06cf436103,gdb4cecd868+6f279b5b48,ge2d134c3d5+cc4dbb2e3f,ge448b5faa6+86d1ceac1d,gecc7e12556+98f8679e14,gf3ee170dca+25247a063c,gf4ac96e456+ade567573c,gf9f5ea5b4d+ac198e9f13,gff490e6085+8c2580be5c,w.2022.27
LSST Data Management Base Package
PyLogAppender.cc
Go to the documentation of this file.
1/*
2 * This file is part of log.
3 *
4 * Developed for the LSST Data Management System.
5 * This product includes software developed by the LSST Project
6 * (https://www.lsst.org).
7 * See the COPYRIGHT file at the top-level directory of this distribution
8 * for details of code ownership.
9 *
10 * This program is free software: you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation, either version 3 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 */
23
24#include <algorithm>
25#include <memory>
26#include <stdexcept>
27
28#include "./PyLogAppender.h"
29#include "log4cxx/patternlayout.h"
30#include "log4cxx/helpers/stringhelper.h"
31
32// macro below dows not work without this using directive
33// (and it breaks if placed inside namespaces)
35IMPLEMENT_LOG4CXX_OBJECT(PyLogAppender)
36
37using namespace log4cxx::helpers;
38
39namespace {
40
41// GIL wrapper class
42class GilGuard {
43public:
44
45 GilGuard() : gil_state(PyGILState_Ensure()) {}
46
47 ~GilGuard() { PyGILState_Release(gil_state); }
48
49private:
50 PyGILState_STATE gil_state;
51};
52
56std::string reraise(std::string const& message) {
57 lsst::log::detail::PyObjectPtr ptype, pvalue, ptraceback;
58 PyErr_Fetch(&ptype.get(), &pvalue.get(), &ptraceback.get());
59
60 std::string exc_msg = message;
61 if (pvalue != nullptr or ptype != nullptr) {
62 lsst::log::detail::PyObjectPtr exc_str(PyObject_Str(pvalue != nullptr ? pvalue : ptype));
63 exc_msg += ": ";
64 exc_msg += PyUnicode_AsUTF8(exc_str);
65 }
66
67 throw std::runtime_error(exc_msg);
68}
69
70// Maximum size of the logger name cache
71unsigned const MAX_LRU_CACHE_SIZE = 32;
72
73}
74
76
78
79 GilGuard gil_guard;
80
81 PyObjectPtr logging(PyImport_ImportModule("logging"));
82 if (logging == nullptr) {
83 ::reraise("ImportError: Failed to import Python logging module");
84 }
85 _getLogger = PyObject_GetAttrString(logging, "getLogger");
86 if (_getLogger == nullptr) {
87 ::reraise("AttributeError: logging.getLogger method does not exist");
88 }
89
90 PyObjectPtr lsstlog_module(PyImport_ImportModule("lsst.log"));
91 if (lsstlog_module == nullptr) {
92 ::reraise("ImportError: Failed to import lsst.log module");
93 }
94 _mdc_class = PyObject_GetAttrString(lsstlog_module, "MDCDict");
95 if (_mdc_class == nullptr) {
96 ::reraise("AttributeError: lsst.log.MDCDict class does not exist");
97 }
98}
99
100void PyLogAppender::append(const spi::LoggingEventPtr& event, log4cxx::helpers::Pool& p) {
101
102 // logger name, UTF-8 encoded
103 std::string logger_name;
104 log4cxx::helpers::Transcoder::encodeUTF8(event->getLoggerName(), logger_name);
105 int const level = event->getLevel()->toInt();
106 int const pyLevel = level / 1000;
107
108 // Check logger name in cache first, this does not need GIL but needs
109 // synchronization
111 {
112 std::lock_guard<std::mutex> lock(_cache_mutex);
113 auto cache_iter = _cache.find(logger_name);
114 if (cache_iter != _cache.end()) {
115 // use it, age is updated later
116 logger = cache_iter->second.logger;
117 }
118 }
119
120 // Need Python at this point
121 GilGuard gil_guard;
122
123 if (logger == nullptr) {
124 // was not found in cache get one from Python
125 if (logger_name == "root") {
126 logger = PyObject_CallFunction(_getLogger, nullptr);
127 } else {
128 logger = PyObject_CallFunction(_getLogger, "s", logger_name.c_str());
129 }
130 }
131 if (logger == nullptr) {
132 ::reraise("Failed to retrieve Python logger \"" + logger_name + "\"");
133 } else {
134 // remember it in cache
135 std::lock_guard<std::mutex> lock(_cache_mutex);
136 _cache.emplace(logger_name, LRUEntry({logger, _lru_age ++}));
137 while (_cache.size() > ::MAX_LRU_CACHE_SIZE) {
138 // find oldest element and remove
139 auto iter = std::min_element(
140 _cache.begin(), _cache.end(),
141 [](const LRUCache::value_type& lhs, const LRUCache::value_type& rhs) {
142 return lhs.second.age < rhs.second.age;
143 }
144 );
145 _cache.erase(iter);
146 }
147 // Age counter could potentially overflow (wrap to 0), reset age for
148 // all cache entries to avoid issues.
149 if (_lru_age == 0) {
150 for (auto& cache_value: _cache) {
151 // give them different but "random" ages
152 cache_value.second.age = _lru_age ++;
153 }
154 }
155 }
156
157 // before doing anything check logging level
158 PyObjectPtr py_is_enabled(PyObject_CallMethod(logger, "isEnabledFor", "i", pyLevel));
159 if (py_is_enabled == nullptr) {
160 ::reraise("Failure when calling logger.isEnabledFor() method");
161 }
162 if (not PyObject_IsTrue(py_is_enabled)) {
163 return;
164 }
165
166 // collect all necessary info
167 auto& loc = event->getLocationInformation();
168 std::string file_name;
169 if (loc.getFileName() != nullptr) {
170 file_name = loc.getFileName();
171 }
172 int const lineno = loc.getLineNumber();
173
174 // if layout is defined then use formatted message
175 std::string message;
176 if (this->layout) {
177 LogString msg;
178 this->layout->format(msg, event, p);
179 // get rid of trailing new-line, just in case someone uses %n in format
180 if (not msg.empty() and msg.back() == '\n') {
181 msg.pop_back();
182 }
183 log4cxx::helpers::Transcoder::encodeUTF8(msg, message);
184 } else {
185 log4cxx::helpers::Transcoder::encodeUTF8(event->getMessage(), message);
186 }
187
188 // record = logger.makeRecord(name, level, fn, lno, msg, args, exc_info,
189 // func=None, extra=None, sinfo=None)
190 // I would like to pass MDC as an `extra` argument but that does not
191 // work reliably in case we want to override record factory.
192 PyObjectPtr record(PyObject_CallMethod(logger, "makeRecord", "sisisOO",
193 logger_name.c_str(),
194 pyLevel,
195 file_name.c_str(),
196 lineno,
197 message.c_str(),
198 Py_None,
199 Py_None));
200 if (record == nullptr) {
201 ::reraise("Failed to create LogRecord instance");
202 }
203
204 // Record should already have an `MDC` attribute added by a record factory,
205 // and it may be pre-filled with some info by the same factory. Here we
206 // assume that if it already exists then it is dict-like, if it does not
207 // we add this attribute as lsst.log.MDCDict instance (which is a dict with
208 // some extra niceties).
209
210 // mdc = getattr(record, "MDC")
211 PyObjectPtr mdc(PyObject_GetAttrString(record, "MDC"));
212 if (mdc == nullptr) {
213 PyErr_Clear();
214 // mdc = lsst.log.MDCDict()
215 mdc = PyObject_CallObject(_mdc_class, nullptr);
216 if (mdc == nullptr) {
217 ::reraise("Failed to make MDCDict instance");
218 }
219 // record.MDC = mdc
220 if (PyObject_SetAttrString(record, "MDC", mdc) == -1) {
221 ::reraise("Failed to set LogRecord MDC attribute");
222 }
223 }
224
225 // Copy MDC to dict, for key in getMDCKeySet(): mdc[key] = event.getMDC(key)
226 for (auto& mdc_key: event->getMDCKeySet()) {
227 std::string key, value;
228 log4cxx::helpers::Transcoder::encodeUTF8(mdc_key, key);
229 log4cxx::LogString mdc_value;
230 event->getMDC(mdc_key, mdc_value);
231 log4cxx::helpers::Transcoder::encodeUTF8(mdc_value, value);
232 PyObjectPtr py_value(PyUnicode_FromStringAndSize(value.data(), value.size()));
233 PyObjectPtr py_key(PyUnicode_FromStringAndSize(key.data(), key.size()));
234 if (PyObject_SetItem(mdc, py_key, py_value) == -1) {
235 // it is probably not a dictionary
236 ::reraise("Failed to update MDC dictionary");
237 }
238 }
239
240 // logger.handle(record)
241 PyObjectPtr res(PyObject_CallMethod(logger, "handle", "O", record.get()));
242 if (res == nullptr) {
243 ::reraise("Logger failed to handle LogRecord");
244 }
245}
246
248}
249
251 return false;
252}
253
254void PyLogAppender::setOption(const LogString &option, const LogString &value) {
255
256 if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("MESSAGEPATTERN"),
257 LOG4CXX_STR("messagepattern"))) {
258 setLayout(LayoutPtr(new PatternLayout(value)));
259 } else {
260 AppenderSkeleton::setOption(option, value);
261 }
262}
263
264} // namespace lsst::log::detail
T begin(T... args)
T c_str(T... args)
This class defines special log4cxx appender which "appends" log messages to Python logging.
Definition: PyLogAppender.h:58
void close() override
Close this appender instance, this is no-op.
void append(const spi::LoggingEventPtr &event, log4cxx::helpers::Pool &p) override
Forward the event to Python logging.
void setOption(const LogString &option, const LogString &value) override
Handle configuration options.
bool requiresLayout() const override
Returns true if appender "requires" layout to be defined for it.
Smart pointer class for PyObject instances.
Definition: PyObjectPtr.h:37
T data(T... args)
T emplace(T... args)
T end(T... args)
T erase(T... args)
T find(T... args)
T min_element(T... args)
T pop_back(T... args)
T size(T... args)