1 /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
4 This file is part of systemd.
6 Copyright 2013 Steven Hiscocks, Zbigniew Jędrzejewski-Szmek
8 systemd is free software; you can redistribute it and/or modify it
9 under the terms of the GNU Lesser General Public License as published by
10 the Free Software Foundation; either version 2.1 of the License, or
11 (at your option) any later version.
13 systemd is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
18 You should have received a copy of the GNU Lesser General Public License
19 along with systemd; If not, see <http://www.gnu.org/licenses/>.
21 #include <systemd/sd-journal.h>
24 #include <structmember.h>
27 #if PY_MAJOR_VERSION >=3
28 # define unicode_FromStringAndSize PyUnicode_FromStringAndSize
29 # define unicode_FromString PyUnicode_FromString
30 # define long_FromLong PyLong_FromLong
31 # define long_FromSize_t PyLong_FromSize_t
32 # define long_Check PyLong_Check
33 # define long_AsLong PyLong_AsLong
35 /* Python 3 type naming convention is used */
36 # define unicode_FromStringAndSize PyString_FromStringAndSize
37 # define unicode_FromString PyString_FromString
38 # define long_FromLong PyInt_FromLong
39 # define long_FromSize_t PyInt_FromSize_t
40 # define long_Check PyInt_Check
41 # define long_AsLong PyInt_AsLong
48 static PyTypeObject JournalType;
50 static int set_error(int r, const char* path, const char* invalid_message) {
53 if (r == -EINVAL && invalid_message)
54 PyErr_SetString(PyExc_ValueError, invalid_message);
55 else if (r == -ENOMEM)
56 PyErr_SetString(PyExc_MemoryError, "Not enough memory");
59 PyErr_SetFromErrnoWithFilename(PyExc_OSError, path);
65 Journal_dealloc(Journal* self)
67 sd_journal_close(self->j);
68 Py_TYPE(self)->tp_free((PyObject*)self);
71 PyDoc_STRVAR(Journal__doc__,
72 "Journal([flags][,path]) -> ...\n"
73 "Journal instance\n\n"
74 "Returns instance of Journal, which allows filtering and return\n"
75 "of journal entries.\n"
76 "Argument `flags` sets open flags of the journal, which can be one\n"
77 "of, or ORed combination of constants: LOCAL_ONLY (default) opens\n"
78 "journal on local machine only; RUNTIME_ONLY opens only\n"
79 "volatile journal files; and SYSTEM_ONLY opens only\n"
80 "journal files of system services and the kernel.\n"
81 "Argument `path` is the directory of journal files. Note that\n"
82 "currently flags are ignored when `path` is present as they are\n"
85 Journal_init(Journal *self, PyObject *args, PyObject *keywds)
87 int flags = SD_JOURNAL_LOCAL_ONLY;
90 static const char* const kwlist[] = {"flags", "path", NULL};
91 if (!PyArg_ParseTupleAndKeywords(args, keywds, "|iz", (char**) kwlist,
96 Py_BEGIN_ALLOW_THREADS
98 r = sd_journal_open_directory(&self->j, path, 0);
100 r = sd_journal_open(&self->j, flags);
104 return set_error(r, path, "Invalid flags or path");
107 PyDoc_STRVAR(Journal_get_next__doc__,
108 "get_next([skip]) -> dict\n\n"
109 "Return dictionary of the next log entry. Optional skip value will\n"
110 "return the `skip`th log entry.");
112 Journal_get_next(Journal *self, PyObject *args)
115 if (! PyArg_ParseTuple(args, "|L", &skip))
119 PyErr_SetString(PyExc_ValueError, "Skip number must positive/negative integer");
124 Py_BEGIN_ALLOW_THREADS
126 r = sd_journal_next(self->j);
127 }else if (skip == -1LL) {
128 r = sd_journal_previous(self->j);
129 }else if (skip > 1LL) {
130 r = sd_journal_next_skip(self->j, skip);
131 }else if (skip < -1LL) {
132 r = sd_journal_previous_skip(self->j, -skip);
136 set_error(r, NULL, NULL);
139 else if (r == 0) /* EOF */
147 const char *delim_ptr;
148 PyObject *key, *value, *cur_value, *tmp_list;
150 SD_JOURNAL_FOREACH_DATA(self->j, msg, msg_len) {
151 delim_ptr = memchr(msg, '=', msg_len);
152 key = unicode_FromStringAndSize(msg, delim_ptr - (const char*) msg);
153 value = PyBytes_FromStringAndSize(delim_ptr + 1, (const char*) msg + msg_len - (delim_ptr + 1) );
154 if (PyDict_Contains(dict, key)) {
155 cur_value = PyDict_GetItem(dict, key);
156 if (PyList_CheckExact(cur_value)) {
157 PyList_Append(cur_value, value);
159 tmp_list = PyList_New(0);
160 PyList_Append(tmp_list, cur_value);
161 PyList_Append(tmp_list, value);
162 PyDict_SetItem(dict, key, tmp_list);
166 PyDict_SetItem(dict, key, value);
173 if (sd_journal_get_realtime_usec(self->j, &realtime) == 0) {
174 char realtime_str[20];
175 sprintf(realtime_str, "%llu", (long long unsigned) realtime);
176 key = unicode_FromString("__REALTIME_TIMESTAMP");
177 value = PyBytes_FromString(realtime_str);
178 PyDict_SetItem(dict, key, value);
185 if (sd_journal_get_monotonic_usec(self->j, &monotonic, &sd_id) == 0) {
186 char monotonic_str[20];
187 sprintf(monotonic_str, "%llu", (long long unsigned) monotonic);
188 key = unicode_FromString("__MONOTONIC_TIMESTAMP");
189 value = PyBytes_FromString(monotonic_str);
191 PyDict_SetItem(dict, key, value);
197 if (sd_journal_get_cursor(self->j, &cursor) > 0) { //Should return 0...
198 key = unicode_FromString("__CURSOR");
199 value = PyBytes_FromString(cursor);
200 PyDict_SetItem(dict, key, value);
209 PyDoc_STRVAR(Journal_get_previous__doc__,
210 "get_previous([skip]) -> dict\n\n"
211 "Return dictionary of the previous log entry. Optional skip value\n"
212 "will return the -`skip`th log entry. Equivalent to get_next(-skip).");
214 Journal_get_previous(Journal *self, PyObject *args)
217 if (! PyArg_ParseTuple(args, "|L", &skip))
220 return PyObject_CallMethod((PyObject *)self, (char*) "get_next",
224 PyDoc_STRVAR(Journal_add_match__doc__,
225 "add_match(match) -> None\n\n"
226 "Add a match to filter journal log entries. All matches of different\n"
227 "fields are combined in logical AND, and matches of the same field\n"
228 "are automatically combined in logical OR.\n"
229 "Match is string of form \"field=value\".");
231 Journal_add_match(Journal *self, PyObject *args, PyObject *keywds)
235 if (! PyArg_ParseTuple(args, "s#", &match, &match_len))
239 r = sd_journal_add_match(self->j, match, match_len);
240 set_error(r, NULL, "Invalid match");
247 PyDoc_STRVAR(Journal_add_disjunction__doc__,
248 "add_disjunction() -> None\n\n"
249 "Once called, all matches before and after are combined in logical\n"
252 Journal_add_disjunction(Journal *self, PyObject *args)
255 r = sd_journal_add_disjunction(self->j);
256 set_error(r, NULL, NULL);
262 PyDoc_STRVAR(Journal_flush_matches__doc__,
263 "flush_matches() -> None\n\n"
264 "Clears all current match filters.");
266 Journal_flush_matches(Journal *self, PyObject *args)
268 sd_journal_flush_matches(self->j);
272 PyDoc_STRVAR(Journal_seek__doc__,
273 "seek(offset[, whence]) -> None\n\n"
274 "Seek through journal by `offset` number of entries. Argument\n"
275 "`whence` defines what the offset is relative to:\n"
276 "os.SEEK_SET (default) from first match in journal;\n"
277 "os.SEEK_CUR from current position in journal;\n"
278 "and os.SEEK_END is from last match in journal.");
280 Journal_seek(Journal *self, PyObject *args, PyObject *keywds)
285 static const char* const kwlist[] = {"offset", "whence", NULL};
286 if (!PyArg_ParseTupleAndKeywords(args, keywds, "L|i", (char**) kwlist,
290 PyObject *result=NULL;
291 if (whence == SEEK_SET){
293 Py_BEGIN_ALLOW_THREADS
294 r = sd_journal_seek_head(self->j);
296 if (set_error(r, NULL, NULL))
300 result = PyObject_CallMethod((PyObject *)self, (char*) "get_next",
301 (char*) "L", offset);
303 }else if (whence == SEEK_CUR){
304 result = PyObject_CallMethod((PyObject *)self, (char*) "get_next",
305 (char*) "L", offset);
306 }else if (whence == SEEK_END){
308 Py_BEGIN_ALLOW_THREADS
309 r = sd_journal_seek_tail(self->j);
311 if (set_error(r, NULL, NULL))
315 result = PyObject_CallMethod((PyObject *)self, (char*) "get_next",
316 (char*) "L", offset);
318 result = PyObject_CallMethod((PyObject *)self, (char*) "get_next",
322 PyErr_SetString(PyExc_ValueError, "Invalid value for whence");
326 if (PyErr_Occurred())
331 PyDoc_STRVAR(Journal_seek_realtime__doc__,
332 "seek_realtime(realtime) -> None\n\n"
333 "Seek to nearest matching journal entry to `realtime`. Argument\n"
334 "`realtime` can must be an integer unix timestamp.");
336 Journal_seek_realtime(Journal *self, PyObject *args)
339 if (! PyArg_ParseTuple(args, "d", &timedouble))
343 timestamp = (uint64_t) (timedouble * 1.0E6);
345 if ((int64_t) timestamp < 0LL) {
346 PyErr_SetString(PyExc_ValueError, "Time must be positive integer");
351 Py_BEGIN_ALLOW_THREADS
352 r = sd_journal_seek_realtime_usec(self->j, timestamp);
354 if (set_error(r, NULL, NULL))
359 PyDoc_STRVAR(Journal_seek_monotonic__doc__,
360 "seek_monotonic(monotonic[, bootid]) -> None\n\n"
361 "Seek to nearest matching journal entry to `monotonic`. Argument\n"
362 "`monotonic` is an timestamp from boot in seconds.\n"
363 "Argument `bootid` is a string representing which boot the\n"
364 "monotonic time is reference to. Defaults to current bootid.");
366 Journal_seek_monotonic(Journal *self, PyObject *args)
370 if (! PyArg_ParseTuple(args, "d|z", &timedouble, &bootid))
374 timestamp = (uint64_t) (timedouble * 1.0E6);
376 if ((int64_t) timestamp < 0LL) {
377 PyErr_SetString(PyExc_ValueError, "Time must be positive number");
384 r = sd_id128_from_string(bootid, &sd_id);
385 if (set_error(r, NULL, "Invalid bootid"))
388 Py_BEGIN_ALLOW_THREADS
389 r = sd_id128_get_boot(&sd_id);
391 if (set_error(r, NULL, NULL))
395 Py_BEGIN_ALLOW_THREADS
396 r = sd_journal_seek_monotonic_usec(self->j, sd_id, timestamp);
398 if (set_error(r, NULL, NULL))
403 PyDoc_STRVAR(Journal_wait__doc__,
404 "wait([timeout]) -> Change state (integer)\n\n"
405 "Waits until there is a change in the journal. Argument `timeout`\n"
406 "is the maximum number of seconds to wait before returning\n"
407 "regardless if journal has changed. If `timeout` is not given or is\n"
408 "0, then it will block forever.\n"
409 "Will return constants: NOP if no change; APPEND if new\n"
410 "entries have been added to the end of the journal; and\n"
411 "INVALIDATE if journal files have been added or removed.");
413 Journal_wait(Journal *self, PyObject *args, PyObject *keywds)
416 int64_t timeout = 0LL;
418 if (!PyArg_ParseTuple(args, "|L", &timeout))
421 Py_BEGIN_ALLOW_THREADS
422 r = sd_journal_wait(self->j, timeout ==0 ? (uint64_t) -1 : timeout * 1E6);
424 if (set_error(r, NULL, NULL))
427 return long_FromLong(r);
430 PyDoc_STRVAR(Journal_seek_cursor__doc__,
431 "seek_cursor(cursor) -> None\n\n"
432 "Seeks to journal entry by given unique reference `cursor`.");
434 Journal_seek_cursor(Journal *self, PyObject *args)
437 if (! PyArg_ParseTuple(args, "s", &cursor))
441 Py_BEGIN_ALLOW_THREADS
442 r = sd_journal_seek_cursor(self->j, cursor);
444 if (set_error(r, NULL, "Invalid cursor"))
450 Journal_iter(PyObject *self)
457 Journal_iternext(PyObject *self)
460 Py_ssize_t dict_size;
462 dict = PyObject_CallMethod(self, (char*) "get_next", (char*) "");
463 if (PyErr_Occurred())
465 dict_size = PyDict_Size(dict);
466 if ((int64_t) dict_size > 0LL) {
470 PyErr_SetNone(PyExc_StopIteration);
475 PyDoc_STRVAR(Journal_query_unique__doc__,
476 "query_unique(field) -> a set of values\n\n"
477 "Returns a set of unique values in journal for given `field`.\n"
478 "Note this does not respect any journal matches.");
480 Journal_query_unique(Journal *self, PyObject *args)
483 if (! PyArg_ParseTuple(args, "s", &query))
487 Py_BEGIN_ALLOW_THREADS
488 r = sd_journal_query_unique(self->j, query);
490 if (set_error(r, NULL, "Invalid field name"))
495 const char *delim_ptr;
496 PyObject *value_set, *key, *value;
497 value_set = PySet_New(0);
498 key = unicode_FromString(query);
500 SD_JOURNAL_FOREACH_UNIQUE(self->j, uniq, uniq_len) {
501 delim_ptr = memchr(uniq, '=', uniq_len);
502 value = PyBytes_FromStringAndSize(delim_ptr + 1, (const char*) uniq + uniq_len - (delim_ptr + 1));
503 PySet_Add(value_set, value);
511 Journal_get_data_threshold(Journal *self, void *closure)
516 r = sd_journal_get_data_threshold(self->j, &cvalue);
517 if (set_error(r, NULL, NULL))
520 return long_FromSize_t(cvalue);
524 Journal_set_data_threshold(Journal *self, PyObject *value, void *closure)
527 PyErr_SetString(PyExc_TypeError, "Cannot delete data threshold");
530 if (!long_Check(value)){
531 PyErr_SetString(PyExc_TypeError, "Data threshold must be int");
535 r = sd_journal_set_data_threshold(self->j, (size_t) long_AsLong(value));
536 return set_error(r, NULL, NULL);
539 static PyGetSetDef Journal_getseters[] = {
540 {(char*) "data_threshold",
541 (getter)Journal_get_data_threshold,
542 (setter)Journal_set_data_threshold,
543 (char*) "data threshold",
548 static PyMethodDef Journal_methods[] = {
549 {"get_next", (PyCFunction)Journal_get_next, METH_VARARGS,
550 Journal_get_next__doc__},
551 {"get_previous", (PyCFunction)Journal_get_previous, METH_VARARGS,
552 Journal_get_previous__doc__},
553 {"add_match", (PyCFunction)Journal_add_match, METH_VARARGS|METH_KEYWORDS,
554 Journal_add_match__doc__},
555 {"add_disjunction", (PyCFunction)Journal_add_disjunction, METH_NOARGS,
556 Journal_add_disjunction__doc__},
557 {"flush_matches", (PyCFunction)Journal_flush_matches, METH_NOARGS,
558 Journal_flush_matches__doc__},
559 {"seek", (PyCFunction)Journal_seek, METH_VARARGS | METH_KEYWORDS,
560 Journal_seek__doc__},
561 {"seek_realtime", (PyCFunction)Journal_seek_realtime, METH_VARARGS,
562 Journal_seek_realtime__doc__},
563 {"seek_monotonic", (PyCFunction)Journal_seek_monotonic, METH_VARARGS,
564 Journal_seek_monotonic__doc__},
565 {"wait", (PyCFunction)Journal_wait, METH_VARARGS,
566 Journal_wait__doc__},
567 {"seek_cursor", (PyCFunction)Journal_seek_cursor, METH_VARARGS,
568 Journal_seek_cursor__doc__},
569 {"query_unique", (PyCFunction)Journal_query_unique, METH_VARARGS,
570 Journal_query_unique__doc__},
571 {NULL} /* Sentinel */
574 static PyTypeObject JournalType = {
575 PyVarObject_HEAD_INIT(NULL, 0)
576 "_reader._Journal", /*tp_name*/
577 sizeof(Journal), /*tp_basicsize*/
579 (destructor)Journal_dealloc, /*tp_dealloc*/
586 0, /*tp_as_sequence*/
594 Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,/*tp_flags*/
595 Journal__doc__, /* tp_doc */
598 0, /* tp_richcompare */
599 0, /* tp_weaklistoffset */
600 Journal_iter, /* tp_iter */
601 Journal_iternext, /* tp_iternext */
602 Journal_methods, /* tp_methods */
604 Journal_getseters, /* tp_getset */
607 0, /* tp_descr_get */
608 0, /* tp_descr_set */
609 0, /* tp_dictoffset */
610 (initproc)Journal_init, /* tp_init */
612 PyType_GenericNew, /* tp_new */
615 #if PY_MAJOR_VERSION >= 3
616 static PyModuleDef _reader_module = {
617 PyModuleDef_HEAD_INIT,
619 "Module that reads systemd journal similar to journalctl.",
621 NULL, NULL, NULL, NULL, NULL
625 #pragma GCC diagnostic push
626 #pragma GCC diagnostic ignored "-Wmissing-prototypes"
629 #if PY_MAJOR_VERSION >= 3
639 if (PyType_Ready(&JournalType) < 0)
640 #if PY_MAJOR_VERSION >= 3
646 #if PY_MAJOR_VERSION >= 3
647 m = PyModule_Create(&_reader_module);
651 m = Py_InitModule3("_reader", NULL,
652 "Module that reads systemd journal similar to journalctl.");
657 Py_INCREF(&JournalType);
658 PyModule_AddObject(m, "_Journal", (PyObject *)&JournalType);
659 PyModule_AddIntConstant(m, "NOP", SD_JOURNAL_NOP);
660 PyModule_AddIntConstant(m, "APPEND", SD_JOURNAL_APPEND);
661 PyModule_AddIntConstant(m, "INVALIDATE", SD_JOURNAL_INVALIDATE);
662 PyModule_AddIntConstant(m, "LOCAL_ONLY", SD_JOURNAL_LOCAL_ONLY);
663 PyModule_AddIntConstant(m, "RUNTIME_ONLY", SD_JOURNAL_RUNTIME_ONLY);
664 PyModule_AddIntConstant(m, "SYSTEM_ONLY", SD_JOURNAL_SYSTEM_ONLY);
666 #if PY_MAJOR_VERSION >= 3
671 #pragma GCC diagnostic pop