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/>.
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);
64 static void Journal_dealloc(Journal* self)
66 sd_journal_close(self->j);
67 Py_TYPE(self)->tp_free((PyObject*)self);
70 PyDoc_STRVAR(Journal__doc__,
71 "Journal([flags][,path]) -> ...\n\n"
72 "Journal allows filtering and retrieval of Journal entries.\n"
73 "Argument `flags` sets open flags of the journal, which can be one\n"
74 "of, or ORed combination of constants: LOCAL_ONLY (default) opens\n"
75 "journal on local machine only; RUNTIME_ONLY opens only\n"
76 "volatile journal files; and SYSTEM_ONLY opens only\n"
77 "journal files of system services and the kernel.\n"
78 "Argument `path` is the directory of journal files. Note that\n"
79 "currently flags are ignored when `path` is present as they are\n"
81 static int Journal_init(Journal *self, PyObject *args, PyObject *keywds)
83 int flags = SD_JOURNAL_LOCAL_ONLY, r;
86 static const char* const kwlist[] = {"flags", "path", NULL};
87 if (!PyArg_ParseTupleAndKeywords(args, keywds, "|iz", (char**) kwlist,
91 Py_BEGIN_ALLOW_THREADS
93 r = sd_journal_open_directory(&self->j, path, 0);
95 r = sd_journal_open(&self->j, flags);
98 return set_error(r, path, "Invalid flags or path");
101 PyDoc_STRVAR(Journal_get_next__doc__,
102 "get_next([skip]) -> dict\n\n"
103 "Return dictionary of the next log entry. Optional skip value will\n"
104 "return the `skip`\\-th log entry.");
105 static PyObject* Journal_get_next(Journal *self, PyObject *args)
110 const char *delim_ptr;
111 PyObject *key, *value, *cur_value, *tmp_list;
113 int64_t skip = 1LL, r = -EINVAL;
114 if (!PyArg_ParseTuple(args, "|L", &skip))
118 PyErr_SetString(PyExc_ValueError, "skip must be nonzero");
122 Py_BEGIN_ALLOW_THREADS
124 r = sd_journal_next(self->j);
125 else if (skip == -1LL)
126 r = sd_journal_previous(self->j);
128 r = sd_journal_next_skip(self->j, skip);
129 else if (skip < -1LL)
130 r = sd_journal_previous_skip(self->j, -skip);
133 set_error(r, NULL, NULL);
136 else if (r == 0) /* EOF */
141 SD_JOURNAL_FOREACH_DATA(self->j, msg, msg_len) {
142 delim_ptr = memchr(msg, '=', msg_len);
143 key = unicode_FromStringAndSize(msg, delim_ptr - (const char*) msg);
144 value = PyBytes_FromStringAndSize(delim_ptr + 1, (const char*) msg + msg_len - (delim_ptr + 1) );
145 if (PyDict_Contains(dict, key)) {
146 cur_value = PyDict_GetItem(dict, key);
147 if (PyList_CheckExact(cur_value)) {
148 PyList_Append(cur_value, value);
150 tmp_list = PyList_New(0);
151 PyList_Append(tmp_list, cur_value);
152 PyList_Append(tmp_list, value);
153 PyDict_SetItem(dict, key, tmp_list);
157 PyDict_SetItem(dict, key, value);
165 if (sd_journal_get_realtime_usec(self->j, &realtime) == 0) {
166 char realtime_str[20];
167 sprintf(realtime_str, "%llu", (long long unsigned) realtime);
168 key = unicode_FromString("__REALTIME_TIMESTAMP");
169 value = PyBytes_FromString(realtime_str);
170 PyDict_SetItem(dict, key, value);
179 if (sd_journal_get_monotonic_usec(self->j, &monotonic, &sd_id) == 0) {
180 char monotonic_str[20];
181 sprintf(monotonic_str, "%llu", (long long unsigned) monotonic);
182 key = unicode_FromString("__MONOTONIC_TIMESTAMP");
183 value = PyBytes_FromString(monotonic_str);
185 PyDict_SetItem(dict, key, value);
193 if (sd_journal_get_cursor(self->j, &cursor) > 0) { //Should return 0...
194 key = unicode_FromString("__CURSOR");
195 value = PyBytes_FromString(cursor);
196 PyDict_SetItem(dict, key, value);
206 PyDoc_STRVAR(Journal_get_previous__doc__,
207 "get_previous([skip]) -> dict\n\n"
208 "Return dictionary of the previous log entry. Optional skip value\n"
209 "will return the -`skip`\\-th log entry. Equivalent to get_next(-skip).");
210 static PyObject* Journal_get_previous(Journal *self, PyObject *args)
213 if (!PyArg_ParseTuple(args, "|L", &skip))
216 return PyObject_CallMethod((PyObject *)self, (char*) "get_next",
220 PyDoc_STRVAR(Journal_add_match__doc__,
221 "add_match(match) -> None\n\n"
222 "Add a match to filter journal log entries. All matches of different\n"
223 "fields are combined with logical AND, and matches of the same field\n"
224 "are automatically combined with logical OR.\n"
225 "Match is a string of the form \"FIELD=value\".");
226 static PyObject* Journal_add_match(Journal *self, PyObject *args, PyObject *keywds)
230 if (!PyArg_ParseTuple(args, "s#", &match, &match_len))
233 r = sd_journal_add_match(self->j, match, match_len);
234 set_error(r, NULL, "Invalid match");
241 PyDoc_STRVAR(Journal_add_disjunction__doc__,
242 "add_disjunction() -> None\n\n"
243 "Inserts a logical OR between matches added before and afterwards.");
244 static PyObject* Journal_add_disjunction(Journal *self, PyObject *args)
247 r = sd_journal_add_disjunction(self->j);
248 set_error(r, NULL, NULL);
254 PyDoc_STRVAR(Journal_flush_matches__doc__,
255 "flush_matches() -> None\n\n"
256 "Clear all current match filters.");
257 static PyObject* Journal_flush_matches(Journal *self, PyObject *args)
259 sd_journal_flush_matches(self->j);
263 PyDoc_STRVAR(Journal_seek__doc__,
264 "seek(offset[, whence]) -> None\n\n"
265 "Jump `offset` entries in the journal. Argument\n"
266 "`whence` defines what the offset is relative to:\n"
267 "os.SEEK_SET (default) from first match in journal;\n"
268 "os.SEEK_CUR from current position in journal;\n"
269 "and os.SEEK_END is from last match in journal.");
270 static PyObject* Journal_seek(Journal *self, PyObject *args, PyObject *keywds)
273 int whence = SEEK_SET;
274 PyObject *result = NULL;
276 static const char* const kwlist[] = {"offset", "whence", NULL};
277 if (!PyArg_ParseTupleAndKeywords(args, keywds, "L|i", (char**) kwlist,
284 Py_BEGIN_ALLOW_THREADS
285 r = sd_journal_seek_head(self->j);
287 if (set_error(r, NULL, NULL))
291 result = PyObject_CallMethod((PyObject *)self, (char*) "get_next",
292 (char*) "L", offset);
296 result = PyObject_CallMethod((PyObject *)self, (char*) "get_next",
297 (char*) "L", offset);
301 Py_BEGIN_ALLOW_THREADS
302 r = sd_journal_seek_tail(self->j);
304 if (set_error(r, NULL, NULL))
307 result = PyObject_CallMethod((PyObject *)self, (char*) "get_next",
308 (char*) "L", offset < 0LL ? offset : -1LL);
312 PyErr_SetString(PyExc_ValueError, "Invalid value for whence");
316 if (PyErr_Occurred())
321 PyDoc_STRVAR(Journal_seek_realtime__doc__,
322 "seek_realtime(realtime) -> None\n\n"
323 "Seek to nearest matching journal entry to `realtime`. Argument\n"
324 "`realtime` can must be an integer unix timestamp.");
325 static PyObject* Journal_seek_realtime(Journal *self, PyObject *args)
331 if (!PyArg_ParseTuple(args, "d", &timedouble))
334 timestamp = (uint64_t) (timedouble * 1.0E6);
335 if ((int64_t) timestamp < 0LL) {
336 PyErr_SetString(PyExc_ValueError, "Time must be a positive integer");
340 Py_BEGIN_ALLOW_THREADS
341 r = sd_journal_seek_realtime_usec(self->j, timestamp);
343 if (set_error(r, NULL, NULL))
348 PyDoc_STRVAR(Journal_seek_monotonic__doc__,
349 "seek_monotonic(monotonic[, bootid]) -> None\n\n"
350 "Seek to nearest matching journal entry to `monotonic`. Argument\n"
351 "`monotonic` is an timestamp from boot in seconds.\n"
352 "Argument `bootid` is a string representing which boot the\n"
353 "monotonic time is reference to. Defaults to current bootid.");
354 static PyObject* Journal_seek_monotonic(Journal *self, PyObject *args)
362 if (!PyArg_ParseTuple(args, "d|z", &timedouble, &bootid))
365 timestamp = (uint64_t) (timedouble * 1.0E6);
367 if ((int64_t) timestamp < 0LL) {
368 PyErr_SetString(PyExc_ValueError, "Time must be positive number");
373 r = sd_id128_from_string(bootid, &sd_id);
374 if (set_error(r, NULL, "Invalid bootid"))
377 Py_BEGIN_ALLOW_THREADS
378 r = sd_id128_get_boot(&sd_id);
380 if (set_error(r, NULL, NULL))
384 Py_BEGIN_ALLOW_THREADS
385 r = sd_journal_seek_monotonic_usec(self->j, sd_id, timestamp);
387 if (set_error(r, NULL, NULL))
392 PyDoc_STRVAR(Journal_wait__doc__,
393 "wait([timeout]) -> state change (integer)\n\n"
394 "Wait for a change in the journal. Argument `timeout` specifies\n"
395 "the maximum number of seconds to wait before returning\n"
396 "regardless of wheter the journal has changed. If `timeout` is not given\n"
397 "or is 0, then block forever.\n"
398 "Will return constants: NOP if no change; APPEND if new\n"
399 "entries have been added to the end of the journal; and\n"
400 "INVALIDATE if journal files have been added or removed.");
401 static PyObject* Journal_wait(Journal *self, PyObject *args, PyObject *keywds)
404 int64_t timeout = 0LL;
406 if (!PyArg_ParseTuple(args, "|L", &timeout))
409 Py_BEGIN_ALLOW_THREADS
410 r = sd_journal_wait(self->j, timeout ==0 ? (uint64_t) -1 : timeout * 1E6);
412 if (set_error(r, NULL, NULL))
415 return long_FromLong(r);
418 PyDoc_STRVAR(Journal_seek_cursor__doc__,
419 "seek_cursor(cursor) -> None\n\n"
420 "Seek to journal entry by given unique reference `cursor`.");
421 static PyObject* Journal_seek_cursor(Journal *self, PyObject *args)
426 if (!PyArg_ParseTuple(args, "s", &cursor))
429 Py_BEGIN_ALLOW_THREADS
430 r = sd_journal_seek_cursor(self->j, cursor);
432 if (set_error(r, NULL, "Invalid cursor"))
437 static PyObject* Journal_iter(PyObject *self)
443 static PyObject* Journal_iternext(PyObject *self)
446 Py_ssize_t dict_size;
448 dict = PyObject_CallMethod(self, (char*) "get_next", (char*) "");
449 if (PyErr_Occurred())
451 dict_size = PyDict_Size(dict);
452 if ((int64_t) dict_size > 0LL) {
456 PyErr_SetNone(PyExc_StopIteration);
461 PyDoc_STRVAR(Journal_query_unique__doc__,
462 "query_unique(field) -> a set of values\n\n"
463 "Return a set of unique values appearing in journal for the\n"
464 "given `field`. Note this does not respect any journal matches.");
465 static PyObject* Journal_query_unique(Journal *self, PyObject *args)
471 PyObject *value_set, *key, *value;
473 if (!PyArg_ParseTuple(args, "s", &query))
476 Py_BEGIN_ALLOW_THREADS
477 r = sd_journal_query_unique(self->j, query);
479 if (set_error(r, NULL, "Invalid field name"))
482 value_set = PySet_New(0);
483 key = unicode_FromString(query);
485 SD_JOURNAL_FOREACH_UNIQUE(self->j, uniq, uniq_len) {
486 const char *delim_ptr;
488 delim_ptr = memchr(uniq, '=', uniq_len);
489 value = PyBytes_FromStringAndSize(delim_ptr + 1, (const char*) uniq + uniq_len - (delim_ptr + 1));
490 PySet_Add(value_set, value);
497 PyDoc_STRVAR(data_threshold__doc__,
498 "Threshold for field size truncation.\n\n"
499 "Fields longer than this will be truncated to the threshold size.\n"
500 "Defaults to 64Kb.");
502 static PyObject* Journal_get_data_threshold(Journal *self, void *closure)
507 r = sd_journal_get_data_threshold(self->j, &cvalue);
508 if (set_error(r, NULL, NULL))
511 return long_FromSize_t(cvalue);
514 static int Journal_set_data_threshold(Journal *self, PyObject *value, void *closure)
518 PyErr_SetString(PyExc_TypeError, "Cannot delete data threshold");
521 if (!long_Check(value)){
522 PyErr_SetString(PyExc_TypeError, "Data threshold must be an int");
525 r = sd_journal_set_data_threshold(self->j, (size_t) long_AsLong(value));
526 return set_error(r, NULL, NULL);
529 static PyGetSetDef Journal_getseters[] = {
530 {(char*) "data_threshold",
531 (getter)Journal_get_data_threshold,
532 (setter)Journal_set_data_threshold,
533 (char*) data_threshold__doc__,
538 static PyMethodDef Journal_methods[] = {
539 {"get_next", (PyCFunction)Journal_get_next, METH_VARARGS,
540 Journal_get_next__doc__},
541 {"get_previous", (PyCFunction)Journal_get_previous, METH_VARARGS,
542 Journal_get_previous__doc__},
543 {"add_match", (PyCFunction)Journal_add_match, METH_VARARGS|METH_KEYWORDS,
544 Journal_add_match__doc__},
545 {"add_disjunction", (PyCFunction)Journal_add_disjunction, METH_NOARGS,
546 Journal_add_disjunction__doc__},
547 {"flush_matches", (PyCFunction)Journal_flush_matches, METH_NOARGS,
548 Journal_flush_matches__doc__},
549 {"seek", (PyCFunction)Journal_seek, METH_VARARGS | METH_KEYWORDS,
550 Journal_seek__doc__},
551 {"seek_realtime", (PyCFunction)Journal_seek_realtime, METH_VARARGS,
552 Journal_seek_realtime__doc__},
553 {"seek_monotonic", (PyCFunction)Journal_seek_monotonic, METH_VARARGS,
554 Journal_seek_monotonic__doc__},
555 {"wait", (PyCFunction)Journal_wait, METH_VARARGS,
556 Journal_wait__doc__},
557 {"seek_cursor", (PyCFunction)Journal_seek_cursor, METH_VARARGS,
558 Journal_seek_cursor__doc__},
559 {"query_unique", (PyCFunction)Journal_query_unique, METH_VARARGS,
560 Journal_query_unique__doc__},
561 {NULL} /* Sentinel */
564 static PyTypeObject JournalType = {
565 PyVarObject_HEAD_INIT(NULL, 0)
566 "_reader._Journal", /*tp_name*/
567 sizeof(Journal), /*tp_basicsize*/
569 (destructor)Journal_dealloc, /*tp_dealloc*/
576 0, /*tp_as_sequence*/
584 Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,/*tp_flags*/
585 Journal__doc__, /* tp_doc */
588 0, /* tp_richcompare */
589 0, /* tp_weaklistoffset */
590 Journal_iter, /* tp_iter */
591 Journal_iternext, /* tp_iternext */
592 Journal_methods, /* tp_methods */
594 Journal_getseters, /* tp_getset */
597 0, /* tp_descr_get */
598 0, /* tp_descr_set */
599 0, /* tp_dictoffset */
600 (initproc)Journal_init, /* tp_init */
602 PyType_GenericNew, /* tp_new */
606 "Module that reads the systemd journal similar to journalctl."
608 #if PY_MAJOR_VERSION >= 3
609 static PyModuleDef _reader_module = {
610 PyModuleDef_HEAD_INIT,
614 NULL, NULL, NULL, NULL, NULL
618 #pragma GCC diagnostic push
619 #pragma GCC diagnostic ignored "-Wmissing-prototypes"
622 #if PY_MAJOR_VERSION >= 3
632 if (PyType_Ready(&JournalType) < 0)
633 #if PY_MAJOR_VERSION >= 3
639 #if PY_MAJOR_VERSION >= 3
640 m = PyModule_Create(&_reader_module);
644 m = Py_InitModule3("_reader", NULL, SUMMARY);
649 Py_INCREF(&JournalType);
650 PyModule_AddObject(m, "_Journal", (PyObject *)&JournalType);
651 PyModule_AddIntConstant(m, "NOP", SD_JOURNAL_NOP);
652 PyModule_AddIntConstant(m, "APPEND", SD_JOURNAL_APPEND);
653 PyModule_AddIntConstant(m, "INVALIDATE", SD_JOURNAL_INVALIDATE);
654 PyModule_AddIntConstant(m, "LOCAL_ONLY", SD_JOURNAL_LOCAL_ONLY);
655 PyModule_AddIntConstant(m, "RUNTIME_ONLY", SD_JOURNAL_RUNTIME_ONLY);
656 PyModule_AddIntConstant(m, "SYSTEM_ONLY", SD_JOURNAL_SYSTEM_ONLY);
658 #if PY_MAJOR_VERSION >= 3
663 #pragma GCC diagnostic pop