1 /*-*- Mode: C; c-basic-offset: 4; 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/>.
23 #include <structmember.h>
27 #include <systemd/sd-journal.h>
33 #if PY_MAJOR_VERSION >=3
34 # define unicode_FromStringAndSize PyUnicode_FromStringAndSize
35 # define unicode_FromString PyUnicode_FromString
36 # define long_FromLong PyLong_FromLong
37 # define long_FromSize_t PyLong_FromSize_t
38 # define long_Check PyLong_Check
39 # define long_AsLong PyLong_AsLong
41 /* Python 3 type naming convention is used */
42 # define unicode_FromStringAndSize PyString_FromStringAndSize
43 # define unicode_FromString PyString_FromString
44 # define long_FromLong PyInt_FromLong
45 # define long_FromSize_t PyInt_FromSize_t
46 # define long_Check PyInt_Check
47 # define long_AsLong PyInt_AsLong
54 static PyTypeObject JournalType;
56 static int set_error(int r, const char* path, const char* invalid_message) {
59 if (r == -EINVAL && invalid_message)
60 PyErr_SetString(PyExc_ValueError, invalid_message);
61 else if (r == -ENOMEM)
62 PyErr_SetString(PyExc_MemoryError, "Not enough memory");
65 PyErr_SetFromErrnoWithFilename(PyExc_OSError, path);
70 static void Journal_dealloc(Journal* self)
72 sd_journal_close(self->j);
73 Py_TYPE(self)->tp_free((PyObject*)self);
76 PyDoc_STRVAR(Journal__doc__,
77 "Journal([flags][,path]) -> ...\n\n"
78 "Journal allows filtering and retrieval of Journal entries.\n"
79 "Argument `flags` sets open flags of the journal, which can be one\n"
80 "of, or ORed combination of constants: LOCAL_ONLY (default) opens\n"
81 "journal on local machine only; RUNTIME_ONLY opens only\n"
82 "volatile journal files; and SYSTEM_ONLY opens only\n"
83 "journal files of system services and the kernel.\n"
84 "Argument `path` is the directory of journal files. Note that\n"
85 "currently flags are ignored when `path` is present as they are\n"
87 static int Journal_init(Journal *self, PyObject *args, PyObject *keywds)
89 int flags = SD_JOURNAL_LOCAL_ONLY, r;
92 static const char* const kwlist[] = {"flags", "path", NULL};
93 if (!PyArg_ParseTupleAndKeywords(args, keywds, "|iz", (char**) kwlist,
97 Py_BEGIN_ALLOW_THREADS
99 r = sd_journal_open_directory(&self->j, path, 0);
101 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.");
111 static PyObject* Journal_get_next(Journal *self, PyObject *args)
119 if (!PyArg_ParseTuple(args, "|L", &skip))
123 PyErr_SetString(PyExc_ValueError, "skip must be nonzero");
127 Py_BEGIN_ALLOW_THREADS
129 r = sd_journal_next(self->j);
130 else if (skip == -1LL)
131 r = sd_journal_previous(self->j);
133 r = sd_journal_next_skip(self->j, skip);
134 else if (skip < -1LL)
135 r = sd_journal_previous_skip(self->j, -skip);
137 assert_not_reached("should not be here");
140 set_error(r, NULL, NULL);
143 else if (r == 0) /* EOF */
150 SD_JOURNAL_FOREACH_DATA(self->j, msg, msg_len) {
151 PyObject _cleanup_Py_DECREF_ *key = NULL, *value = NULL;
152 const char *delim_ptr;
154 delim_ptr = memchr(msg, '=', msg_len);
156 PyErr_SetString(PyExc_OSError,
157 "journal gave us a field without '='");
161 key = unicode_FromStringAndSize(msg, delim_ptr - (const char*) msg);
165 value = PyBytes_FromStringAndSize(
167 (const char*) msg + msg_len - (delim_ptr + 1) );
171 if (PyDict_Contains(dict, key)) {
172 PyObject *cur_value = PyDict_GetItem(dict, key);
174 if (PyList_CheckExact(cur_value)) {
175 r = PyList_Append(cur_value, value);
179 PyObject _cleanup_Py_DECREF_ *tmp_list = PyList_New(0);
183 r = PyList_Append(tmp_list, cur_value);
187 r = PyList_Append(tmp_list, value);
191 PyDict_SetItem(dict, key, tmp_list);
196 r = PyDict_SetItem(dict, key, value);
203 PyObject _cleanup_Py_DECREF_ *key = NULL, *value = NULL;
206 r = sd_journal_get_realtime_usec(self->j, &realtime);
207 if (set_error(r, NULL, NULL))
210 key = unicode_FromString("__REALTIME_TIMESTAMP");
214 assert_cc(sizeof(unsigned long long) == sizeof(realtime));
215 value = PyLong_FromUnsignedLongLong(realtime);
219 if (PyDict_SetItem(dict, key, value))
224 PyObject _cleanup_Py_DECREF_ *key = NULL, *value = NULL;
228 r = sd_journal_get_monotonic_usec(self->j, &monotonic, &sd_id);
229 if (set_error(r, NULL, NULL))
232 key = unicode_FromString("__MONOTONIC_TIMESTAMP");
236 assert_cc(sizeof(unsigned long long) == sizeof(monotonic));
237 value = PyLong_FromUnsignedLongLong(monotonic);
241 if (PyDict_SetItem(dict, key, value))
246 PyObject _cleanup_Py_DECREF_ *key = NULL, *value = NULL;
247 char _cleanup_free_ *cursor = NULL;
249 r = sd_journal_get_cursor(self->j, &cursor);
250 if (set_error(r, NULL, NULL))
253 key = unicode_FromString("__CURSOR");
257 value = PyBytes_FromString(cursor);
261 if (PyDict_SetItem(dict, key, value))
271 PyDoc_STRVAR(Journal_get_previous__doc__,
272 "get_previous([skip]) -> dict\n\n"
273 "Return dictionary of the previous log entry. Optional skip value\n"
274 "will return the -`skip`\\-th log entry. Equivalent to get_next(-skip).");
275 static PyObject* Journal_get_previous(Journal *self, PyObject *args)
278 if (!PyArg_ParseTuple(args, "|L", &skip))
281 return PyObject_CallMethod((PyObject *)self, (char*) "get_next",
285 PyDoc_STRVAR(Journal_add_match__doc__,
286 "add_match(match) -> None\n\n"
287 "Add a match to filter journal log entries. All matches of different\n"
288 "fields are combined with logical AND, and matches of the same field\n"
289 "are automatically combined with logical OR.\n"
290 "Match is a string of the form \"FIELD=value\".");
291 static PyObject* Journal_add_match(Journal *self, PyObject *args, PyObject *keywds)
295 if (!PyArg_ParseTuple(args, "s#", &match, &match_len))
298 r = sd_journal_add_match(self->j, match, match_len);
299 set_error(r, NULL, "Invalid match");
306 PyDoc_STRVAR(Journal_add_disjunction__doc__,
307 "add_disjunction() -> None\n\n"
308 "Inserts a logical OR between matches added before and afterwards.");
309 static PyObject* Journal_add_disjunction(Journal *self, PyObject *args)
312 r = sd_journal_add_disjunction(self->j);
313 set_error(r, NULL, NULL);
319 PyDoc_STRVAR(Journal_flush_matches__doc__,
320 "flush_matches() -> None\n\n"
321 "Clear all current match filters.");
322 static PyObject* Journal_flush_matches(Journal *self, PyObject *args)
324 sd_journal_flush_matches(self->j);
328 PyDoc_STRVAR(Journal_seek__doc__,
329 "seek(offset[, whence]) -> None\n\n"
330 "Jump `offset` entries in the journal. Argument\n"
331 "`whence` defines what the offset is relative to:\n"
332 "os.SEEK_SET (default) from first match in journal;\n"
333 "os.SEEK_CUR from current position in journal;\n"
334 "and os.SEEK_END is from last match in journal.");
335 static PyObject* Journal_seek(Journal *self, PyObject *args, PyObject *keywds)
338 int whence = SEEK_SET;
339 PyObject *result = NULL;
341 static const char* const kwlist[] = {"offset", "whence", NULL};
342 if (!PyArg_ParseTupleAndKeywords(args, keywds, "L|i", (char**) kwlist,
349 Py_BEGIN_ALLOW_THREADS
350 r = sd_journal_seek_head(self->j);
352 if (set_error(r, NULL, NULL))
356 result = PyObject_CallMethod((PyObject *)self, (char*) "get_next",
357 (char*) "L", offset);
361 result = PyObject_CallMethod((PyObject *)self, (char*) "get_next",
362 (char*) "L", offset);
366 Py_BEGIN_ALLOW_THREADS
367 r = sd_journal_seek_tail(self->j);
369 if (set_error(r, NULL, NULL))
372 result = PyObject_CallMethod((PyObject *)self, (char*) "get_next",
373 (char*) "L", offset < 0LL ? offset : -1LL);
377 PyErr_SetString(PyExc_ValueError, "Invalid value for whence");
381 if (PyErr_Occurred())
386 PyDoc_STRVAR(Journal_seek_realtime__doc__,
387 "seek_realtime(realtime) -> None\n\n"
388 "Seek to nearest matching journal entry to `realtime`. Argument\n"
389 "`realtime` can must be an integer unix timestamp.");
390 static PyObject* Journal_seek_realtime(Journal *self, PyObject *args)
396 if (!PyArg_ParseTuple(args, "d", &timedouble))
399 timestamp = (uint64_t) (timedouble * 1.0E6);
400 if ((int64_t) timestamp < 0LL) {
401 PyErr_SetString(PyExc_ValueError, "Time must be a positive integer");
405 Py_BEGIN_ALLOW_THREADS
406 r = sd_journal_seek_realtime_usec(self->j, timestamp);
408 if (set_error(r, NULL, NULL))
413 PyDoc_STRVAR(Journal_seek_monotonic__doc__,
414 "seek_monotonic(monotonic[, bootid]) -> None\n\n"
415 "Seek to nearest matching journal entry to `monotonic`. Argument\n"
416 "`monotonic` is an timestamp from boot in seconds.\n"
417 "Argument `bootid` is a string representing which boot the\n"
418 "monotonic time is reference to. Defaults to current bootid.");
419 static PyObject* Journal_seek_monotonic(Journal *self, PyObject *args)
427 if (!PyArg_ParseTuple(args, "d|z", &timedouble, &bootid))
430 timestamp = (uint64_t) (timedouble * 1.0E6);
432 if ((int64_t) timestamp < 0LL) {
433 PyErr_SetString(PyExc_ValueError, "Time must be positive number");
438 r = sd_id128_from_string(bootid, &sd_id);
439 if (set_error(r, NULL, "Invalid bootid"))
442 Py_BEGIN_ALLOW_THREADS
443 r = sd_id128_get_boot(&sd_id);
445 if (set_error(r, NULL, NULL))
449 Py_BEGIN_ALLOW_THREADS
450 r = sd_journal_seek_monotonic_usec(self->j, sd_id, timestamp);
452 if (set_error(r, NULL, NULL))
457 PyDoc_STRVAR(Journal_wait__doc__,
458 "wait([timeout]) -> state change (integer)\n\n"
459 "Wait for a change in the journal. Argument `timeout` specifies\n"
460 "the maximum number of seconds to wait before returning\n"
461 "regardless of wheter the journal has changed. If `timeout` is not given\n"
462 "or is 0, then block forever.\n"
463 "Will return constants: NOP if no change; APPEND if new\n"
464 "entries have been added to the end of the journal; and\n"
465 "INVALIDATE if journal files have been added or removed.");
466 static PyObject* Journal_wait(Journal *self, PyObject *args, PyObject *keywds)
469 int64_t timeout = 0LL;
471 if (!PyArg_ParseTuple(args, "|L", &timeout))
474 Py_BEGIN_ALLOW_THREADS
475 r = sd_journal_wait(self->j, timeout ==0 ? (uint64_t) -1 : timeout * 1E6);
477 if (set_error(r, NULL, NULL))
480 return long_FromLong(r);
483 PyDoc_STRVAR(Journal_seek_cursor__doc__,
484 "seek_cursor(cursor) -> None\n\n"
485 "Seek to journal entry by given unique reference `cursor`.");
486 static PyObject* Journal_seek_cursor(Journal *self, PyObject *args)
491 if (!PyArg_ParseTuple(args, "s", &cursor))
494 Py_BEGIN_ALLOW_THREADS
495 r = sd_journal_seek_cursor(self->j, cursor);
497 if (set_error(r, NULL, "Invalid cursor"))
502 static PyObject* Journal_iter(PyObject *self)
508 static PyObject* Journal_iternext(PyObject *self)
511 Py_ssize_t dict_size;
513 dict = PyObject_CallMethod(self, (char*) "get_next", (char*) "");
514 if (PyErr_Occurred())
516 dict_size = PyDict_Size(dict);
517 if ((int64_t) dict_size > 0LL) {
521 PyErr_SetNone(PyExc_StopIteration);
526 PyDoc_STRVAR(Journal_query_unique__doc__,
527 "query_unique(field) -> a set of values\n\n"
528 "Return a set of unique values appearing in journal for the\n"
529 "given `field`. Note this does not respect any journal matches.");
530 static PyObject* Journal_query_unique(Journal *self, PyObject *args)
536 PyObject *value_set, *key, *value;
538 if (!PyArg_ParseTuple(args, "s", &query))
541 Py_BEGIN_ALLOW_THREADS
542 r = sd_journal_query_unique(self->j, query);
544 if (set_error(r, NULL, "Invalid field name"))
547 value_set = PySet_New(0);
548 key = unicode_FromString(query);
550 SD_JOURNAL_FOREACH_UNIQUE(self->j, uniq, uniq_len) {
551 const char *delim_ptr;
553 delim_ptr = memchr(uniq, '=', uniq_len);
554 value = PyBytes_FromStringAndSize(
556 (const char*) uniq + uniq_len - (delim_ptr + 1));
557 PySet_Add(value_set, value);
564 PyDoc_STRVAR(data_threshold__doc__,
565 "Threshold for field size truncation in bytes.\n\n"
566 "Fields longer than this will be truncated to the threshold size.\n"
567 "Defaults to 64Kb.");
569 static PyObject* Journal_get_data_threshold(Journal *self, void *closure)
574 r = sd_journal_get_data_threshold(self->j, &cvalue);
575 if (set_error(r, NULL, NULL))
578 return long_FromSize_t(cvalue);
581 static int Journal_set_data_threshold(Journal *self, PyObject *value, void *closure)
585 PyErr_SetString(PyExc_AttributeError, "Cannot delete data threshold");
588 if (!long_Check(value)){
589 PyErr_SetString(PyExc_TypeError, "Data threshold must be an int");
592 r = sd_journal_set_data_threshold(self->j, (size_t) long_AsLong(value));
593 return set_error(r, NULL, NULL);
596 static PyGetSetDef Journal_getseters[] = {
597 {(char*) "data_threshold",
598 (getter) Journal_get_data_threshold,
599 (setter) Journal_set_data_threshold,
600 (char*) data_threshold__doc__,
605 static PyMethodDef Journal_methods[] = {
606 {"get_next", (PyCFunction) Journal_get_next, METH_VARARGS, Journal_get_next__doc__},
607 {"get_previous", (PyCFunction) Journal_get_previous, METH_VARARGS, Journal_get_previous__doc__},
608 {"add_match", (PyCFunction) Journal_add_match, METH_VARARGS|METH_KEYWORDS, Journal_add_match__doc__},
609 {"add_disjunction", (PyCFunction) Journal_add_disjunction, METH_NOARGS, Journal_add_disjunction__doc__},
610 {"flush_matches", (PyCFunction) Journal_flush_matches, METH_NOARGS, Journal_flush_matches__doc__},
611 {"seek", (PyCFunction) Journal_seek, METH_VARARGS | METH_KEYWORDS, Journal_seek__doc__},
612 {"seek_realtime", (PyCFunction) Journal_seek_realtime, METH_VARARGS, Journal_seek_realtime__doc__},
613 {"seek_monotonic", (PyCFunction) Journal_seek_monotonic, METH_VARARGS, Journal_seek_monotonic__doc__},
614 {"wait", (PyCFunction) Journal_wait, METH_VARARGS, Journal_wait__doc__},
615 {"seek_cursor", (PyCFunction) Journal_seek_cursor, METH_VARARGS, Journal_seek_cursor__doc__},
616 {"query_unique", (PyCFunction) Journal_query_unique, METH_VARARGS, Journal_query_unique__doc__},
617 {NULL} /* Sentinel */
620 static PyTypeObject JournalType = {
621 PyVarObject_HEAD_INIT(NULL, 0)
622 "_reader._Journal", /*tp_name*/
623 sizeof(Journal), /*tp_basicsize*/
625 (destructor)Journal_dealloc, /*tp_dealloc*/
632 0, /*tp_as_sequence*/
640 Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
641 Journal__doc__, /* tp_doc */
644 0, /* tp_richcompare */
645 0, /* tp_weaklistoffset */
646 Journal_iter, /* tp_iter */
647 Journal_iternext, /* tp_iternext */
648 Journal_methods, /* tp_methods */
650 Journal_getseters, /* tp_getset */
653 0, /* tp_descr_get */
654 0, /* tp_descr_set */
655 0, /* tp_dictoffset */
656 (initproc) Journal_init, /* tp_init */
658 PyType_GenericNew, /* tp_new */
662 "Module that reads the systemd journal similar to journalctl."
664 #if PY_MAJOR_VERSION >= 3
665 static PyModuleDef _reader_module = {
666 PyModuleDef_HEAD_INIT,
670 NULL, NULL, NULL, NULL, NULL
674 #pragma GCC diagnostic push
675 #pragma GCC diagnostic ignored "-Wmissing-prototypes"
678 #if PY_MAJOR_VERSION >= 3
688 if (PyType_Ready(&JournalType) < 0)
689 #if PY_MAJOR_VERSION >= 3
695 #if PY_MAJOR_VERSION >= 3
696 m = PyModule_Create(&_reader_module);
700 m = Py_InitModule3("_reader", NULL, SUMMARY);
705 Py_INCREF(&JournalType);
706 if (PyModule_AddObject(m, "_Journal", (PyObject *) &JournalType) ||
707 PyModule_AddIntConstant(m, "NOP", SD_JOURNAL_NOP) ||
708 PyModule_AddIntConstant(m, "APPEND", SD_JOURNAL_APPEND) ||
709 PyModule_AddIntConstant(m, "INVALIDATE", SD_JOURNAL_INVALIDATE) ||
710 PyModule_AddIntConstant(m, "LOCAL_ONLY", SD_JOURNAL_LOCAL_ONLY) ||
711 PyModule_AddIntConstant(m, "RUNTIME_ONLY", SD_JOURNAL_RUNTIME_ONLY) ||
712 PyModule_AddIntConstant(m, "SYSTEM_ONLY", SD_JOURNAL_SYSTEM_ONLY)) {
713 #if PY_MAJOR_VERSION >= 3
719 #if PY_MAJOR_VERSION >= 3
724 #pragma GCC diagnostic pop