2 _reader - Python module that reads systemd journal similar to journalctl
3 Copyright (C) 2012 Steven Hiscocks
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Lesser General Public
7 License as published by the Free Software Foundation; either
8 version 2.1 of the License, or (at your option) any later version.
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Lesser General Public License for more details.
15 You should have received a copy of the GNU Lesser General Public
16 License along with this library; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 #include <systemd/sd-journal.h>
22 #include <structmember.h>
29 static PyTypeObject JournalType;
32 Journal_dealloc(Journal* self)
34 sd_journal_close(self->j);
35 Py_TYPE(self)->tp_free((PyObject*)self);
38 PyDoc_STRVAR(Journal__doc__,
39 "Journal([flags][,path]) -> ...\n"
40 "Journal instance\n\n"
41 "Returns instance of Journal, which allows filtering and return\n"
42 "of journal entries.\n"
43 "Argument `flags` sets open flags of the journal, which can be one\n"
44 "of, or ORed combination of constants: LOCAL_ONLY (default) opens\n"
45 "journal on local machine only; RUNTIME_ONLY opens only\n"
46 "volatile journal files; and SYSTEM_ONLY opens only\n"
47 "journal files of system services and the kernel.\n"
48 "Argument `path` is the directory of journal files. Note that\n"
49 "currently flags are ignored when `path` is present as they are\n"
52 Journal_init(Journal *self, PyObject *args, PyObject *keywds)
54 int flags=SD_JOURNAL_LOCAL_ONLY;
57 static char *kwlist[] = {"flags", NULL};
58 if (! PyArg_ParseTupleAndKeywords(args, keywds, "|is", kwlist,
64 r = sd_journal_open_directory(&self->j, path, 0);
66 Py_BEGIN_ALLOW_THREADS
67 r = sd_journal_open(&self->j, flags);
71 PyErr_SetString(PyExc_ValueError, "Invalid flags or path");
73 }else if (r == -ENOMEM) {
74 PyErr_SetString(PyExc_MemoryError, "Not enough memory");
77 PyErr_SetString(PyExc_RuntimeError, "Error opening journal");
84 PyDoc_STRVAR(Journal_get_next__doc__,
85 "get_next([skip]) -> dict\n\n"
86 "Return dictionary of the next log entry. Optional skip value will\n"
87 "return the `skip`th log entry.");
89 Journal_get_next(Journal *self, PyObject *args)
92 if (! PyArg_ParseTuple(args, "|L", &skip))
97 Py_BEGIN_ALLOW_THREADS
98 r = sd_journal_next(self->j);
100 }else if (skip == -1LL) {
101 Py_BEGIN_ALLOW_THREADS
102 r = sd_journal_previous(self->j);
104 }else if (skip > 1LL) {
105 Py_BEGIN_ALLOW_THREADS
106 r = sd_journal_next_skip(self->j, skip);
108 }else if (skip < -1LL) {
109 Py_BEGIN_ALLOW_THREADS
110 r = sd_journal_previous_skip(self->j, -skip);
113 PyErr_SetString(PyExc_ValueError, "Skip number must positive/negative integer");
118 PyErr_SetString(PyExc_RuntimeError, "Error getting next message");
120 }else if ( r == 0) { //EOF
129 const char *delim_ptr;
130 PyObject *key, *value, *cur_value, *tmp_list;
132 SD_JOURNAL_FOREACH_DATA(self->j, msg, msg_len) {
133 delim_ptr = memchr(msg, '=', msg_len);
134 #if PY_MAJOR_VERSION >=3
135 key = PyUnicode_FromStringAndSize(msg, delim_ptr - (const char*) msg);
137 key = PyString_FromStringAndSize(msg, delim_ptr - (const char*) msg);
139 value = PyBytes_FromStringAndSize(delim_ptr + 1, (const char*) msg + msg_len - (delim_ptr + 1) );
140 if (PyDict_Contains(dict, key)) {
141 cur_value = PyDict_GetItem(dict, key);
142 if (PyList_CheckExact(cur_value)) {
143 PyList_Append(cur_value, value);
145 tmp_list = PyList_New(0);
146 PyList_Append(tmp_list, cur_value);
147 PyList_Append(tmp_list, value);
148 PyDict_SetItem(dict, key, tmp_list);
152 PyDict_SetItem(dict, key, value);
159 if (sd_journal_get_realtime_usec(self->j, &realtime) == 0) {
160 char realtime_str[20];
161 sprintf(realtime_str, "%llu", (long long unsigned) realtime);
163 #if PY_MAJOR_VERSION >=3
164 key = PyUnicode_FromString("__REALTIME_TIMESTAMP");
166 key = PyString_FromString("__REALTIME_TIMESTAMP");
168 value = PyBytes_FromString(realtime_str);
169 PyDict_SetItem(dict, key, value);
176 if (sd_journal_get_monotonic_usec(self->j, &monotonic, &sd_id) == 0) {
177 char monotonic_str[20];
178 sprintf(monotonic_str, "%llu", (long long unsigned) monotonic);
179 #if PY_MAJOR_VERSION >=3
180 key = PyUnicode_FromString("__MONOTONIC_TIMESTAMP");
182 key = PyString_FromString("__MONOTONIC_TIMESTAMP");
184 value = PyBytes_FromString(monotonic_str);
186 PyDict_SetItem(dict, key, value);
192 if (sd_journal_get_cursor(self->j, &cursor) > 0) { //Should return 0...
193 #if PY_MAJOR_VERSION >=3
194 key = PyUnicode_FromString("__CURSOR");
196 key = PyString_FromString("__CURSOR");
198 value = PyBytes_FromString(cursor);
199 PyDict_SetItem(dict, key, value);
208 PyDoc_STRVAR(Journal_get_previous__doc__,
209 "get_previous([skip]) -> dict\n\n"
210 "Return dictionary of the previous log entry. Optional skip value\n"
211 "will return the -`skip`th log entry. Equivalent to get_next(-skip).");
213 Journal_get_previous(Journal *self, PyObject *args)
216 if (! PyArg_ParseTuple(args, "|L", &skip))
219 return PyObject_CallMethod((PyObject *)self, "get_next", "L", -skip);
222 PyDoc_STRVAR(Journal_add_match__doc__,
223 "add_match(match, ..., field=value, ...) -> None\n\n"
224 "Add a match to filter journal log entries. All matches of different\n"
225 "field are combined in logical AND, and matches of the same field\n"
226 "are automatically combined in logical OR.\n"
227 "Matches can be passed as strings \"field=value\", or keyword\n"
228 "arguments field=\"value\".");
230 Journal_add_match(Journal *self, PyObject *args, PyObject *keywds)
232 Py_ssize_t arg_match_len;
235 for (i = 0; i < PySequence_Size(args); i++) {
236 #if PY_MAJOR_VERSION >=3
238 arg = PySequence_Fast_GET_ITEM(args, i);
239 if (PyUnicode_Check(arg)) {
240 #if PY_MINOR_VERSION >=3
241 arg_match = PyUnicode_AsUTF8AndSize(arg, &arg_match_len);
244 temp = PyUnicode_AsUTF8String(arg);
245 PyBytes_AsStringAndSize(temp, &arg_match, &arg_match_len);
248 }else if (PyBytes_Check(arg)) {
249 PyBytes_AsStringAndSize(arg, &arg_match, &arg_match_len);
251 PyErr_SetString(PyExc_TypeError, "expected bytes or string");
254 PyString_AsStringAndSize(PySequence_Fast_GET_ITEM(args, i), &arg_match, &arg_match_len);
256 if (PyErr_Occurred())
258 r = sd_journal_add_match(self->j, arg_match, arg_match_len);
260 PyErr_SetString(PyExc_ValueError, "Invalid match");
262 }else if (r == -ENOMEM) {
263 PyErr_SetString(PyExc_MemoryError, "Not enough memory");
266 PyErr_SetString(PyExc_RuntimeError, "Error adding match");
274 PyObject *key, *value;
275 Py_ssize_t pos=0, match_key_len, match_value_len;
277 char *match_key, *match_value;
279 while (PyDict_Next(keywds, &pos, &key, &value)) {
280 #if PY_MAJOR_VERSION >=3
281 if (PyUnicode_Check(key)) {
282 #if PY_MINOR_VERSION >=3
283 match_key = PyUnicode_AsUTF8AndSize(key, &match_key_len);
286 temp2 = PyUnicode_AsUTF8String(key);
287 PyBytes_AsStringAndSize(temp2, &match_key, &match_key_len);
290 }else if (PyBytes_Check(key)) {
291 PyBytes_AsStringAndSize(key, &match_key, &match_key_len);
293 PyErr_SetString(PyExc_TypeError, "expected bytes or string");
295 if (PyUnicode_Check(value)) {
296 #if PY_MINOR_VERSION >=3
297 match_value = PyUnicode_AsUTF8AndSize(value, &match_value_len);
300 temp3 = PyUnicode_AsUTF8String(value);
301 PyBytes_AsStringAndSize(temp3, &match_value, &match_value_len);
304 }else if (PyBytes_Check(value)) {
305 PyBytes_AsStringAndSize(value, &match_value, &match_value_len);
307 PyErr_SetString(PyExc_TypeError, "expected bytes or string");
310 PyString_AsStringAndSize(key, &match_key, &match_key_len);
311 PyString_AsStringAndSize(value, &match_value, &match_value_len);
313 if (PyErr_Occurred())
316 match_len = match_key_len + 1 + match_value_len;
317 match = malloc(match_len);
318 memcpy(match, match_key, match_key_len);
319 memcpy(match + match_key_len, "=", 1);
320 memcpy(match + match_key_len + 1, match_value, match_value_len);
322 r = sd_journal_add_match(self->j, match, match_len);
325 PyErr_SetString(PyExc_ValueError, "Invalid match");
327 }else if (r == -ENOMEM) {
328 PyErr_SetString(PyExc_MemoryError, "Not enough memory");
331 PyErr_SetString(PyExc_RuntimeError, "Error adding match");
339 PyDoc_STRVAR(Journal_add_disjunction__doc__,
340 "add_disjunction() -> None\n\n"
341 "Once called, all matches before and after are combined in logical\n"
344 Journal_add_disjunction(Journal *self, PyObject *args)
347 r = sd_journal_add_disjunction(self->j);
349 PyErr_SetString(PyExc_MemoryError, "Not enough memory");
352 PyErr_SetString(PyExc_RuntimeError, "Error adding disjunction");
358 PyDoc_STRVAR(Journal_flush_matches__doc__,
359 "flush_matches() -> None\n\n"
360 "Clears all current match filters.");
362 Journal_flush_matches(Journal *self, PyObject *args)
364 sd_journal_flush_matches(self->j);
368 PyDoc_STRVAR(Journal_seek__doc__,
369 "seek(offset[, whence]) -> None\n\n"
370 "Seek through journal by `offset` number of entries. Argument\n"
371 "`whence` defines what the offset is relative to:\n"
372 "os.SEEK_SET (default) from first match in journal;\n"
373 "os.SEEK_CUR from current position in journal;\n"
374 "and os.SEEK_END is from last match in journal.");
376 Journal_seek(Journal *self, PyObject *args, PyObject *keywds)
380 static char *kwlist[] = {"offset", "whence", NULL};
382 if (! PyArg_ParseTupleAndKeywords(args, keywds, "L|i", kwlist,
386 PyObject *result=NULL;
387 if (whence == SEEK_SET){
389 Py_BEGIN_ALLOW_THREADS
390 r = sd_journal_seek_head(self->j);
393 PyErr_SetString(PyExc_RuntimeError, "Error seeking to head");
397 result = PyObject_CallMethod((PyObject *)self, "get_next", "L", offset);
399 }else if (whence == SEEK_CUR){
400 result = PyObject_CallMethod((PyObject *)self, "get_next", "L", offset);
401 }else if (whence == SEEK_END){
403 Py_BEGIN_ALLOW_THREADS
404 r = sd_journal_seek_tail(self->j);
407 PyErr_SetString(PyExc_RuntimeError, "Error seeking to tail");
411 result = PyObject_CallMethod((PyObject *)self, "get_next", "L", offset);
413 result = PyObject_CallMethod((PyObject *)self, "get_next", "L", -1LL);
416 PyErr_SetString(PyExc_ValueError, "Invalid value for whence");
421 if (PyErr_Occurred())
426 PyDoc_STRVAR(Journal_seek_realtime__doc__,
427 "seek_realtime(realtime) -> None\n\n"
428 "Seek to nearest matching journal entry to `realtime`. Argument\n"
429 "`realtime` can be an integer unix timestamp in usecs or a "
430 "datetime instance.");
432 Journal_seek_realtime(Journal *self, PyObject *args)
435 if (! PyArg_ParseTuple(args, "O", &arg))
438 uint64_t timestamp=-1LL;
439 if (PyDateTime_Check(arg)) {
442 temp = PyObject_CallMethod(arg, "strftime", "s", "%s%f");
443 #if PY_MAJOR_VERSION >=3
445 temp2 = PyUnicode_AsUTF8String(temp);
446 timestamp_str = PyBytes_AsString(temp2);
449 timestamp_str = PyString_AsString(temp);
452 timestamp = strtoull(timestamp_str, NULL, 10);
453 }else if (PyLong_Check(arg)) {
454 timestamp = PyLong_AsUnsignedLongLong(arg);
455 #if PY_MAJOR_VERSION <3
456 }else if (PyInt_Check(arg)) {
457 timestamp = PyInt_AsUnsignedLongLongMask(arg);
460 if ((int64_t) timestamp < 0LL) {
461 PyErr_SetString(PyExc_ValueError, "Time must be positive integer or datetime instance");
466 Py_BEGIN_ALLOW_THREADS
467 r = sd_journal_seek_realtime_usec(self->j, timestamp);
470 PyErr_SetString(PyExc_RuntimeError, "Error seek to time");
476 PyDoc_STRVAR(Journal_seek_monotonic__doc__,
477 "seek_monotonic(monotonic[, bootid]) -> None\n\n"
478 "Seek to nearest matching journal entry to `monotonic`. Argument\n"
479 "`monotonic` is an timestamp from boot in secs, or a\n"
480 "timedelta instance.\n"
481 "Argument `bootid` is a string representing which boot the\n"
482 "monotonic time is reference to. Defaults to current bootid.");
484 Journal_seek_monotonic(Journal *self, PyObject *args)
488 if (! PyArg_ParseTuple(args, "O|s", &arg, &bootid))
491 uint64_t timestamp=-1LL;
492 if PyDelta_Check(arg) {
494 temp = PyObject_CallMethod(arg, "total_seconds", NULL);
495 timestamp = (uint64_t) (PyFloat_AsDouble(temp) * 1E6);
497 }else if (PyFloat_Check(arg)) {
498 timestamp = (uint64_t) (PyFloat_AsDouble(arg) * 1E6);
499 }else if (PyLong_Check(arg)) {
500 timestamp = PyLong_AsUnsignedLongLong(arg) * (uint64_t) 1E6;
501 #if PY_MAJOR_VERSION <3
502 }else if (PyInt_Check(arg)) {
503 timestamp = PyInt_AsUnsignedLongLongMask(arg) * (uint64_t) 1E6;
508 if ((int64_t) timestamp < 0LL) {
509 PyErr_SetString(PyExc_ValueError, "Time must be positive number or timedelta instance");
516 r = sd_id128_from_string(bootid, &sd_id);
518 PyErr_SetString(PyExc_ValueError, "Invalid bootid");
521 PyErr_SetString(PyExc_RuntimeError, "Error processing bootid");
525 r = sd_id128_get_boot(&sd_id);
527 PyErr_SetString(PyExc_IOError, "Error getting current boot ID");
530 PyErr_SetString(PyExc_RuntimeError, "Error getting current boot ID");
535 Py_BEGIN_ALLOW_THREADS
536 r = sd_journal_seek_monotonic_usec(self->j, sd_id, timestamp);
539 PyErr_SetString(PyExc_RuntimeError, "Error seek to time");
545 PyDoc_STRVAR(Journal_wait__doc__,
546 "wait([timeout]) -> Change state (integer)\n\n"
547 "Waits until there is a change in the journal. Argument `timeout`\n"
548 "is the maximum number of seconds to wait before returning\n"
549 "regardless if journal has changed. If `timeout` is not given or is\n"
550 "0, then it will block forever.\n"
551 "Will return constants: NOP if no change; APPEND if new\n"
552 "entries have been added to the end of the journal; and\n"
553 "INVALIDATE if journal files have been added or removed.");
555 Journal_wait(Journal *self, PyObject *args, PyObject *keywds)
558 if (! PyArg_ParseTuple(args, "|L", &timeout))
562 if ( timeout == 0LL) {
563 Py_BEGIN_ALLOW_THREADS
564 r = sd_journal_wait(self->j, (uint64_t) -1);
567 Py_BEGIN_ALLOW_THREADS
568 r = sd_journal_wait(self->j, timeout * 1E6);
571 #if PY_MAJOR_VERSION >=3
572 return PyLong_FromLong(r);
574 return PyInt_FromLong(r);
578 PyDoc_STRVAR(Journal_seek_cursor__doc__,
579 "seek_cursor(cursor) -> None\n\n"
580 "Seeks to journal entry by given unique reference `cursor`.");
582 Journal_seek_cursor(Journal *self, PyObject *args)
585 if (! PyArg_ParseTuple(args, "s", &cursor))
589 Py_BEGIN_ALLOW_THREADS
590 r = sd_journal_seek_cursor(self->j, cursor);
593 PyErr_SetString(PyExc_ValueError, "Invalid cursor");
595 }else if (r == -ENOMEM) {
596 PyErr_SetString(PyExc_MemoryError, "Not enough memory");
599 PyErr_SetString(PyExc_RuntimeError, "Error seeking to cursor");
606 Journal_iter(PyObject *self)
613 Journal_iternext(PyObject *self)
616 Py_ssize_t dict_size;
618 dict = PyObject_CallMethod(self, "get_next", "");
619 dict_size = PyDict_Size(dict);
620 if ((int64_t) dict_size > 0LL) {
624 PyErr_SetNone(PyExc_StopIteration);
629 #ifdef SD_JOURNAL_FOREACH_UNIQUE
630 PyDoc_STRVAR(Journal_query_unique__doc__,
631 "query_unique(field) -> a set of values\n\n"
632 "Returns a set of unique values in journal for given `field`.\n"
633 "Note this does not respect any journal matches.");
635 Journal_query_unique(Journal *self, PyObject *args)
638 if (! PyArg_ParseTuple(args, "s", &query))
642 Py_BEGIN_ALLOW_THREADS
643 r = sd_journal_query_unique(self->j, query);
646 PyErr_SetString(PyExc_ValueError, "Invalid field name");
648 } else if (r == -ENOMEM) {
649 PyErr_SetString(PyExc_MemoryError, "Not enough memory");
652 PyErr_SetString(PyExc_RuntimeError, "Error querying journal");
658 const char *delim_ptr;
659 PyObject *value_set, *key, *value;
660 value_set = PySet_New(0);
662 #if PY_MAJOR_VERSION >=3
663 key = PyUnicode_FromString(query);
665 key = PyString_FromString(query);
668 SD_JOURNAL_FOREACH_UNIQUE(self->j, uniq, uniq_len) {
669 delim_ptr = memchr(uniq, '=', uniq_len);
670 value = PyBytes_FromStringAndSize(delim_ptr + 1, (const char*) uniq + uniq_len - (delim_ptr + 1));
671 PySet_Add(value_set, value);
677 #endif //def SD_JOURNAL_FOREACH_UNIQUE
680 Journal_get_data_threshold(Journal *self, void *closure)
686 r = sd_journal_get_data_threshold(self->j, &cvalue);
688 PyErr_SetString(PyExc_RuntimeError, "Error getting data threshold");
692 #if PY_MAJOR_VERSION >=3
693 value = PyLong_FromSize_t(cvalue);
695 value = PyInt_FromSize_t(cvalue);
701 Journal_set_data_threshold(Journal *self, PyObject *value, void *closure)
704 PyErr_SetString(PyExc_TypeError, "Cannot delete data threshold");
707 #if PY_MAJOR_VERSION >=3
708 if (! PyLong_Check(value)){
710 if (! PyInt_Check(value)){
712 PyErr_SetString(PyExc_TypeError, "Data threshold must be int");
716 #if PY_MAJOR_VERSION >=3
717 r = sd_journal_set_data_threshold(self->j, (size_t) PyLong_AsLong(value));
719 r = sd_journal_set_data_threshold(self->j, (size_t) PyInt_AsLong(value));
722 PyErr_SetString(PyExc_RuntimeError, "Error setting data threshold");
728 static PyGetSetDef Journal_getseters[] = {
730 (getter)Journal_get_data_threshold,
731 (setter)Journal_set_data_threshold,
737 static PyMethodDef Journal_methods[] = {
738 {"get_next", (PyCFunction)Journal_get_next, METH_VARARGS,
739 Journal_get_next__doc__},
740 {"get_previous", (PyCFunction)Journal_get_previous, METH_VARARGS,
741 Journal_get_previous__doc__},
742 {"add_match", (PyCFunction)Journal_add_match, METH_VARARGS|METH_KEYWORDS,
743 Journal_add_match__doc__},
744 {"add_disjunction", (PyCFunction)Journal_add_disjunction, METH_NOARGS,
745 Journal_add_disjunction__doc__},
746 {"flush_matches", (PyCFunction)Journal_flush_matches, METH_NOARGS,
747 Journal_flush_matches__doc__},
748 {"seek", (PyCFunction)Journal_seek, METH_VARARGS | METH_KEYWORDS,
749 Journal_seek__doc__},
750 {"seek_realtime", (PyCFunction)Journal_seek_realtime, METH_VARARGS,
751 Journal_seek_realtime__doc__},
752 {"seek_monotonic", (PyCFunction)Journal_seek_monotonic, METH_VARARGS,
753 Journal_seek_monotonic__doc__},
754 {"wait", (PyCFunction)Journal_wait, METH_VARARGS,
755 Journal_wait__doc__},
756 {"seek_cursor", (PyCFunction)Journal_seek_cursor, METH_VARARGS,
757 Journal_seek_cursor__doc__},
758 #ifdef SD_JOURNAL_FOREACH_UNIQUE
759 {"query_unique", (PyCFunction)Journal_query_unique, METH_VARARGS,
760 Journal_query_unique__doc__},
762 {NULL} /* Sentinel */
765 static PyTypeObject JournalType = {
766 PyVarObject_HEAD_INIT(NULL, 0)
767 "_reader.Journal", /*tp_name*/
768 sizeof(Journal), /*tp_basicsize*/
770 (destructor)Journal_dealloc, /*tp_dealloc*/
777 0, /*tp_as_sequence*/
785 Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,/*tp_flags*/
786 Journal__doc__, /* tp_doc */
789 0, /* tp_richcompare */
790 0, /* tp_weaklistoffset */
791 Journal_iter, /* tp_iter */
792 Journal_iternext, /* tp_iternext */
793 Journal_methods, /* tp_methods */
795 Journal_getseters, /* tp_getset */
798 0, /* tp_descr_get */
799 0, /* tp_descr_set */
800 0, /* tp_dictoffset */
801 (initproc)Journal_init, /* tp_init */
803 PyType_GenericNew, /* tp_new */
806 #if PY_MAJOR_VERSION >= 3
807 static PyModuleDef _reader_module = {
808 PyModuleDef_HEAD_INIT,
810 "Module that reads systemd journal similar to journalctl.",
812 NULL, NULL, NULL, NULL, NULL
817 #if PY_MAJOR_VERSION >= 3
827 if (PyType_Ready(&JournalType) < 0)
828 #if PY_MAJOR_VERSION >= 3
834 #if PY_MAJOR_VERSION >= 3
835 m = PyModule_Create(&_reader_module);
839 m = Py_InitModule3("_reader", NULL,
840 "Module that reads systemd journal similar to journalctl.");
845 Py_INCREF(&JournalType);
846 PyModule_AddObject(m, "_Journal", (PyObject *)&JournalType);
847 PyModule_AddIntConstant(m, "NOP", SD_JOURNAL_NOP);
848 PyModule_AddIntConstant(m, "APPEND", SD_JOURNAL_APPEND);
849 PyModule_AddIntConstant(m, "INVALIDATE", SD_JOURNAL_INVALIDATE);
850 PyModule_AddIntConstant(m, "LOCAL_ONLY", SD_JOURNAL_LOCAL_ONLY);
851 PyModule_AddIntConstant(m, "RUNTIME_ONLY", SD_JOURNAL_RUNTIME_ONLY);
852 PyModule_AddIntConstant(m, "SYSTEM_ONLY", SD_JOURNAL_SYSTEM_ONLY);
854 #if PY_MAJOR_VERSION >= 3