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", "path", NULL};
58 if (! PyArg_ParseTupleAndKeywords(args, keywds, "|is", kwlist,
63 Py_BEGIN_ALLOW_THREADS
65 r = sd_journal_open_directory(&self->j, path, 0);
67 r = sd_journal_open(&self->j, flags);
72 PyObject *errtype = r == -EINVAL ? PyExc_ValueError :
73 r == -ENOMEM ? PyExc_MemoryError :
75 PyErr_SetFromErrnoWithFilename(errtype, path);
82 PyDoc_STRVAR(Journal_get_next__doc__,
83 "get_next([skip]) -> dict\n\n"
84 "Return dictionary of the next log entry. Optional skip value will\n"
85 "return the `skip`th log entry.");
87 Journal_get_next(Journal *self, PyObject *args)
90 if (! PyArg_ParseTuple(args, "|L", &skip))
94 PyErr_SetString(PyExc_ValueError, "Skip number must positive/negative integer");
99 Py_BEGIN_ALLOW_THREADS
101 r = sd_journal_next(self->j);
102 }else if (skip == -1LL) {
103 r = sd_journal_previous(self->j);
104 }else if (skip > 1LL) {
105 r = sd_journal_next_skip(self->j, skip);
106 }else if (skip < -1LL) {
107 r = sd_journal_previous_skip(self->j, -skip);
113 PyErr_SetFromErrno(PyExc_OSError);
115 }else if ( r == 0) { //EOF
124 const char *delim_ptr;
125 PyObject *key, *value, *cur_value, *tmp_list;
127 SD_JOURNAL_FOREACH_DATA(self->j, msg, msg_len) {
128 delim_ptr = memchr(msg, '=', msg_len);
129 #if PY_MAJOR_VERSION >=3
130 key = PyUnicode_FromStringAndSize(msg, delim_ptr - (const char*) msg);
132 key = PyString_FromStringAndSize(msg, delim_ptr - (const char*) msg);
134 value = PyBytes_FromStringAndSize(delim_ptr + 1, (const char*) msg + msg_len - (delim_ptr + 1) );
135 if (PyDict_Contains(dict, key)) {
136 cur_value = PyDict_GetItem(dict, key);
137 if (PyList_CheckExact(cur_value)) {
138 PyList_Append(cur_value, value);
140 tmp_list = PyList_New(0);
141 PyList_Append(tmp_list, cur_value);
142 PyList_Append(tmp_list, value);
143 PyDict_SetItem(dict, key, tmp_list);
147 PyDict_SetItem(dict, key, value);
154 if (sd_journal_get_realtime_usec(self->j, &realtime) == 0) {
155 char realtime_str[20];
156 sprintf(realtime_str, "%llu", (long long unsigned) realtime);
158 #if PY_MAJOR_VERSION >=3
159 key = PyUnicode_FromString("__REALTIME_TIMESTAMP");
161 key = PyString_FromString("__REALTIME_TIMESTAMP");
163 value = PyBytes_FromString(realtime_str);
164 PyDict_SetItem(dict, key, value);
171 if (sd_journal_get_monotonic_usec(self->j, &monotonic, &sd_id) == 0) {
172 char monotonic_str[20];
173 sprintf(monotonic_str, "%llu", (long long unsigned) monotonic);
174 #if PY_MAJOR_VERSION >=3
175 key = PyUnicode_FromString("__MONOTONIC_TIMESTAMP");
177 key = PyString_FromString("__MONOTONIC_TIMESTAMP");
179 value = PyBytes_FromString(monotonic_str);
181 PyDict_SetItem(dict, key, value);
187 if (sd_journal_get_cursor(self->j, &cursor) > 0) { //Should return 0...
188 #if PY_MAJOR_VERSION >=3
189 key = PyUnicode_FromString("__CURSOR");
191 key = PyString_FromString("__CURSOR");
193 value = PyBytes_FromString(cursor);
194 PyDict_SetItem(dict, key, value);
203 PyDoc_STRVAR(Journal_get_previous__doc__,
204 "get_previous([skip]) -> dict\n\n"
205 "Return dictionary of the previous log entry. Optional skip value\n"
206 "will return the -`skip`th log entry. Equivalent to get_next(-skip).");
208 Journal_get_previous(Journal *self, PyObject *args)
211 if (! PyArg_ParseTuple(args, "|L", &skip))
214 return PyObject_CallMethod((PyObject *)self, "get_next", "L", -skip);
217 PyDoc_STRVAR(Journal_add_match__doc__,
218 "add_match(match, ..., field=value, ...) -> None\n\n"
219 "Add a match to filter journal log entries. All matches of different\n"
220 "field are combined in logical AND, and matches of the same field\n"
221 "are automatically combined in logical OR.\n"
222 "Matches can be passed as strings \"field=value\", or keyword\n"
223 "arguments field=\"value\".");
225 Journal_add_match(Journal *self, PyObject *args, PyObject *keywds)
227 Py_ssize_t arg_match_len;
230 for (i = 0; i < PySequence_Size(args); i++) {
231 #if PY_MAJOR_VERSION >=3
233 arg = PySequence_Fast_GET_ITEM(args, i);
234 if (PyUnicode_Check(arg)) {
235 #if PY_MINOR_VERSION >=3
236 arg_match = PyUnicode_AsUTF8AndSize(arg, &arg_match_len);
239 temp = PyUnicode_AsUTF8String(arg);
240 PyBytes_AsStringAndSize(temp, &arg_match, &arg_match_len);
243 }else if (PyBytes_Check(arg)) {
244 PyBytes_AsStringAndSize(arg, &arg_match, &arg_match_len);
246 PyErr_SetString(PyExc_TypeError, "expected bytes or string");
249 PyString_AsStringAndSize(PySequence_Fast_GET_ITEM(args, i), &arg_match, &arg_match_len);
251 if (PyErr_Occurred())
253 r = sd_journal_add_match(self->j, arg_match, arg_match_len);
256 PyObject *errtype = r == -EINVAL ? PyExc_ValueError :
257 r == -ENOMEM ? PyExc_MemoryError :
259 PyErr_SetFromErrno(errtype);
267 PyObject *key, *value;
268 Py_ssize_t pos=0, match_key_len, match_value_len;
270 char *match_key, *match_value;
272 while (PyDict_Next(keywds, &pos, &key, &value)) {
273 #if PY_MAJOR_VERSION >=3
274 if (PyUnicode_Check(key)) {
275 #if PY_MINOR_VERSION >=3
276 match_key = PyUnicode_AsUTF8AndSize(key, &match_key_len);
279 temp2 = PyUnicode_AsUTF8String(key);
280 PyBytes_AsStringAndSize(temp2, &match_key, &match_key_len);
283 }else if (PyBytes_Check(key)) {
284 PyBytes_AsStringAndSize(key, &match_key, &match_key_len);
286 PyErr_SetString(PyExc_TypeError, "expected bytes or string");
288 if (PyUnicode_Check(value)) {
289 #if PY_MINOR_VERSION >=3
290 match_value = PyUnicode_AsUTF8AndSize(value, &match_value_len);
293 temp3 = PyUnicode_AsUTF8String(value);
294 PyBytes_AsStringAndSize(temp3, &match_value, &match_value_len);
297 }else if (PyBytes_Check(value)) {
298 PyBytes_AsStringAndSize(value, &match_value, &match_value_len);
300 PyErr_SetString(PyExc_TypeError, "expected bytes or string");
303 PyString_AsStringAndSize(key, &match_key, &match_key_len);
304 PyString_AsStringAndSize(value, &match_value, &match_value_len);
306 if (PyErr_Occurred())
309 match_len = match_key_len + 1 + match_value_len;
310 match = malloc(match_len);
311 memcpy(match, match_key, match_key_len);
312 memcpy(match + match_key_len, "=", 1);
313 memcpy(match + match_key_len + 1, match_value, match_value_len);
315 r = sd_journal_add_match(self->j, match, match_len);
318 PyErr_SetString(PyExc_ValueError, "Invalid match");
320 }else if (r == -ENOMEM) {
321 PyErr_SetString(PyExc_MemoryError, "Not enough memory");
324 PyErr_SetString(PyExc_RuntimeError, "Error adding match");
332 PyDoc_STRVAR(Journal_add_disjunction__doc__,
333 "add_disjunction() -> None\n\n"
334 "Once called, all matches before and after are combined in logical\n"
337 Journal_add_disjunction(Journal *self, PyObject *args)
340 r = sd_journal_add_disjunction(self->j);
343 PyObject *errtype = r == -ENOMEM ? PyExc_MemoryError :
345 PyErr_SetFromErrno(errtype);
351 PyDoc_STRVAR(Journal_flush_matches__doc__,
352 "flush_matches() -> None\n\n"
353 "Clears all current match filters.");
355 Journal_flush_matches(Journal *self, PyObject *args)
357 sd_journal_flush_matches(self->j);
361 PyDoc_STRVAR(Journal_seek__doc__,
362 "seek(offset[, whence]) -> None\n\n"
363 "Seek through journal by `offset` number of entries. Argument\n"
364 "`whence` defines what the offset is relative to:\n"
365 "os.SEEK_SET (default) from first match in journal;\n"
366 "os.SEEK_CUR from current position in journal;\n"
367 "and os.SEEK_END is from last match in journal.");
369 Journal_seek(Journal *self, PyObject *args, PyObject *keywds)
373 static char *kwlist[] = {"offset", "whence", NULL};
375 if (! PyArg_ParseTupleAndKeywords(args, keywds, "L|i", kwlist,
379 PyObject *result=NULL;
380 if (whence == SEEK_SET){
382 Py_BEGIN_ALLOW_THREADS
383 r = sd_journal_seek_head(self->j);
387 PyErr_SetFromErrno(PyExc_OSError);
391 result = PyObject_CallMethod((PyObject *)self, "get_next", "L", offset);
393 }else if (whence == SEEK_CUR){
394 result = PyObject_CallMethod((PyObject *)self, "get_next", "L", offset);
395 }else if (whence == SEEK_END){
397 Py_BEGIN_ALLOW_THREADS
398 r = sd_journal_seek_tail(self->j);
402 PyErr_SetFromErrno(PyExc_OSError);
406 result = PyObject_CallMethod((PyObject *)self, "get_next", "L", offset);
408 result = PyObject_CallMethod((PyObject *)self, "get_next", "L", -1LL);
411 PyErr_SetString(PyExc_ValueError, "Invalid value for whence");
416 if (PyErr_Occurred())
421 PyDoc_STRVAR(Journal_seek_realtime__doc__,
422 "seek_realtime(realtime) -> None\n\n"
423 "Seek to nearest matching journal entry to `realtime`. Argument\n"
424 "`realtime` can be an integer unix timestamp in usecs or a "
425 "datetime instance.");
427 Journal_seek_realtime(Journal *self, PyObject *args)
430 if (! PyArg_ParseTuple(args, "O", &arg))
433 uint64_t timestamp=-1LL;
434 if (PyDateTime_Check(arg)) {
437 temp = PyObject_CallMethod(arg, "strftime", "s", "%s%f");
438 #if PY_MAJOR_VERSION >=3
440 temp2 = PyUnicode_AsUTF8String(temp);
441 timestamp_str = PyBytes_AsString(temp2);
444 timestamp_str = PyString_AsString(temp);
447 timestamp = strtoull(timestamp_str, NULL, 10);
448 }else if (PyLong_Check(arg)) {
449 timestamp = PyLong_AsUnsignedLongLong(arg);
450 #if PY_MAJOR_VERSION <3
451 }else if (PyInt_Check(arg)) {
452 timestamp = PyInt_AsUnsignedLongLongMask(arg);
455 if ((int64_t) timestamp < 0LL) {
456 PyErr_SetString(PyExc_ValueError, "Time must be positive integer or datetime instance");
461 Py_BEGIN_ALLOW_THREADS
462 r = sd_journal_seek_realtime_usec(self->j, timestamp);
466 PyErr_SetFromErrno(PyExc_OSError);
472 PyDoc_STRVAR(Journal_seek_monotonic__doc__,
473 "seek_monotonic(monotonic[, bootid]) -> None\n\n"
474 "Seek to nearest matching journal entry to `monotonic`. Argument\n"
475 "`monotonic` is an timestamp from boot in secs, or a\n"
476 "timedelta instance.\n"
477 "Argument `bootid` is a string representing which boot the\n"
478 "monotonic time is reference to. Defaults to current bootid.");
480 Journal_seek_monotonic(Journal *self, PyObject *args)
484 if (! PyArg_ParseTuple(args, "O|s", &arg, &bootid))
487 uint64_t timestamp=-1LL;
488 if PyDelta_Check(arg) {
490 temp = PyObject_CallMethod(arg, "total_seconds", NULL);
491 timestamp = (uint64_t) (PyFloat_AsDouble(temp) * 1E6);
493 }else if (PyFloat_Check(arg)) {
494 timestamp = (uint64_t) (PyFloat_AsDouble(arg) * 1E6);
495 }else if (PyLong_Check(arg)) {
496 timestamp = PyLong_AsUnsignedLongLong(arg) * (uint64_t) 1E6;
497 #if PY_MAJOR_VERSION <3
498 }else if (PyInt_Check(arg)) {
499 timestamp = PyInt_AsUnsignedLongLongMask(arg) * (uint64_t) 1E6;
504 if ((int64_t) timestamp < 0LL) {
505 PyErr_SetString(PyExc_ValueError, "Time must be positive number or timedelta instance");
512 r = sd_id128_from_string(bootid, &sd_id);
514 PyErr_SetString(PyExc_ValueError, "Invalid bootid");
518 PyErr_SetFromErrno(PyExc_OSError);
522 r = sd_id128_get_boot(&sd_id);
524 PyErr_SetString(PyExc_IOError, "Error getting current boot ID");
528 PyErr_SetFromErrno(PyExc_OSError);
533 Py_BEGIN_ALLOW_THREADS
534 r = sd_journal_seek_monotonic_usec(self->j, sd_id, timestamp);
538 PyErr_SetFromErrno(PyExc_OSError);
544 PyDoc_STRVAR(Journal_wait__doc__,
545 "wait([timeout]) -> Change state (integer)\n\n"
546 "Waits until there is a change in the journal. Argument `timeout`\n"
547 "is the maximum number of seconds to wait before returning\n"
548 "regardless if journal has changed. If `timeout` is not given or is\n"
549 "0, then it will block forever.\n"
550 "Will return constants: NOP if no change; APPEND if new\n"
551 "entries have been added to the end of the journal; and\n"
552 "INVALIDATE if journal files have been added or removed.");
554 Journal_wait(Journal *self, PyObject *args, PyObject *keywds)
557 if (! PyArg_ParseTuple(args, "|L", &timeout))
561 Py_BEGIN_ALLOW_THREADS
562 if ( timeout == 0LL) {
563 r = sd_journal_wait(self->j, (uint64_t) -1);
565 r = sd_journal_wait(self->j, timeout * 1E6);
570 PyObject *errtype = r == -ENOMEM ? PyExc_MemoryError :
572 PyErr_SetFromErrno(errtype);
575 #if PY_MAJOR_VERSION >=3
576 return PyLong_FromLong(r);
578 return PyInt_FromLong(r);
582 PyDoc_STRVAR(Journal_seek_cursor__doc__,
583 "seek_cursor(cursor) -> None\n\n"
584 "Seeks to journal entry by given unique reference `cursor`.");
586 Journal_seek_cursor(Journal *self, PyObject *args)
589 if (! PyArg_ParseTuple(args, "s", &cursor))
593 Py_BEGIN_ALLOW_THREADS
594 r = sd_journal_seek_cursor(self->j, cursor);
598 PyObject *errtype = r == -EINVAL ? PyExc_ValueError :
599 r == -ENOMEM ? PyExc_MemoryError :
601 PyErr_SetFromErrno(errtype);
608 Journal_iter(PyObject *self)
615 Journal_iternext(PyObject *self)
618 Py_ssize_t dict_size;
620 dict = PyObject_CallMethod(self, "get_next", "");
621 dict_size = PyDict_Size(dict);
622 if ((int64_t) dict_size > 0LL) {
626 PyErr_SetNone(PyExc_StopIteration);
631 #ifdef SD_JOURNAL_FOREACH_UNIQUE
632 PyDoc_STRVAR(Journal_query_unique__doc__,
633 "query_unique(field) -> a set of values\n\n"
634 "Returns a set of unique values in journal for given `field`.\n"
635 "Note this does not respect any journal matches.");
637 Journal_query_unique(Journal *self, PyObject *args)
640 if (! PyArg_ParseTuple(args, "s", &query))
644 Py_BEGIN_ALLOW_THREADS
645 r = sd_journal_query_unique(self->j, query);
649 PyObject *errtype = r == -EINVAL ? PyExc_ValueError :
650 r == -ENOMEM ? PyExc_MemoryError :
652 PyErr_SetFromErrno(errtype);
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);
689 PyErr_SetFromErrno(PyExc_OSError);
693 #if PY_MAJOR_VERSION >=3
694 value = PyLong_FromSize_t(cvalue);
696 value = PyInt_FromSize_t(cvalue);
702 Journal_set_data_threshold(Journal *self, PyObject *value, void *closure)
705 PyErr_SetString(PyExc_TypeError, "Cannot delete data threshold");
708 #if PY_MAJOR_VERSION >=3
709 if (! PyLong_Check(value)){
711 if (! PyInt_Check(value)){
713 PyErr_SetString(PyExc_TypeError, "Data threshold must be int");
717 #if PY_MAJOR_VERSION >=3
718 r = sd_journal_set_data_threshold(self->j, (size_t) PyLong_AsLong(value));
720 r = sd_journal_set_data_threshold(self->j, (size_t) PyInt_AsLong(value));
724 PyErr_SetFromErrno(PyExc_OSError);
730 static PyGetSetDef Journal_getseters[] = {
732 (getter)Journal_get_data_threshold,
733 (setter)Journal_set_data_threshold,
739 static PyMethodDef Journal_methods[] = {
740 {"get_next", (PyCFunction)Journal_get_next, METH_VARARGS,
741 Journal_get_next__doc__},
742 {"get_previous", (PyCFunction)Journal_get_previous, METH_VARARGS,
743 Journal_get_previous__doc__},
744 {"add_match", (PyCFunction)Journal_add_match, METH_VARARGS|METH_KEYWORDS,
745 Journal_add_match__doc__},
746 {"add_disjunction", (PyCFunction)Journal_add_disjunction, METH_NOARGS,
747 Journal_add_disjunction__doc__},
748 {"flush_matches", (PyCFunction)Journal_flush_matches, METH_NOARGS,
749 Journal_flush_matches__doc__},
750 {"seek", (PyCFunction)Journal_seek, METH_VARARGS | METH_KEYWORDS,
751 Journal_seek__doc__},
752 {"seek_realtime", (PyCFunction)Journal_seek_realtime, METH_VARARGS,
753 Journal_seek_realtime__doc__},
754 {"seek_monotonic", (PyCFunction)Journal_seek_monotonic, METH_VARARGS,
755 Journal_seek_monotonic__doc__},
756 {"wait", (PyCFunction)Journal_wait, METH_VARARGS,
757 Journal_wait__doc__},
758 {"seek_cursor", (PyCFunction)Journal_seek_cursor, METH_VARARGS,
759 Journal_seek_cursor__doc__},
760 #ifdef SD_JOURNAL_FOREACH_UNIQUE
761 {"query_unique", (PyCFunction)Journal_query_unique, METH_VARARGS,
762 Journal_query_unique__doc__},
764 {NULL} /* Sentinel */
767 static PyTypeObject JournalType = {
768 PyVarObject_HEAD_INIT(NULL, 0)
769 "_reader.Journal", /*tp_name*/
770 sizeof(Journal), /*tp_basicsize*/
772 (destructor)Journal_dealloc, /*tp_dealloc*/
779 0, /*tp_as_sequence*/
787 Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,/*tp_flags*/
788 Journal__doc__, /* tp_doc */
791 0, /* tp_richcompare */
792 0, /* tp_weaklistoffset */
793 Journal_iter, /* tp_iter */
794 Journal_iternext, /* tp_iternext */
795 Journal_methods, /* tp_methods */
797 Journal_getseters, /* tp_getset */
800 0, /* tp_descr_get */
801 0, /* tp_descr_set */
802 0, /* tp_dictoffset */
803 (initproc)Journal_init, /* tp_init */
805 PyType_GenericNew, /* tp_new */
808 #if PY_MAJOR_VERSION >= 3
809 static PyModuleDef _reader_module = {
810 PyModuleDef_HEAD_INIT,
812 "Module that reads systemd journal similar to journalctl.",
814 NULL, NULL, NULL, NULL, NULL
819 #if PY_MAJOR_VERSION >= 3
829 if (PyType_Ready(&JournalType) < 0)
830 #if PY_MAJOR_VERSION >= 3
836 #if PY_MAJOR_VERSION >= 3
837 m = PyModule_Create(&_reader_module);
841 m = Py_InitModule3("_reader", NULL,
842 "Module that reads systemd journal similar to journalctl.");
847 Py_INCREF(&JournalType);
848 PyModule_AddObject(m, "_Journal", (PyObject *)&JournalType);
849 PyModule_AddIntConstant(m, "NOP", SD_JOURNAL_NOP);
850 PyModule_AddIntConstant(m, "APPEND", SD_JOURNAL_APPEND);
851 PyModule_AddIntConstant(m, "INVALIDATE", SD_JOURNAL_INVALIDATE);
852 PyModule_AddIntConstant(m, "LOCAL_ONLY", SD_JOURNAL_LOCAL_ONLY);
853 PyModule_AddIntConstant(m, "RUNTIME_ONLY", SD_JOURNAL_RUNTIME_ONLY);
854 PyModule_AddIntConstant(m, "SYSTEM_ONLY", SD_JOURNAL_SYSTEM_ONLY);
856 #if PY_MAJOR_VERSION >= 3