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, r;
90 static const char* const kwlist[] = {"flags", "path", NULL};
91 if (!PyArg_ParseTupleAndKeywords(args, keywds, "|iz", (char**) kwlist,
95 Py_BEGIN_ALLOW_THREADS
97 r = sd_journal_open_directory(&self->j, path, 0);
99 r = sd_journal_open(&self->j, flags);
102 return set_error(r, path, "Invalid flags or path");
105 PyDoc_STRVAR(Journal_get_next__doc__,
106 "get_next([skip]) -> dict\n\n"
107 "Return dictionary of the next log entry. Optional skip value will\n"
108 "return the `skip`th log entry.");
110 Journal_get_next(Journal *self, PyObject *args)
115 const char *delim_ptr;
116 PyObject *key, *value, *cur_value, *tmp_list;
118 int64_t skip = 1LL, r = -EINVAL;
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);
138 set_error(r, NULL, NULL);
141 else if (r == 0) /* EOF */
146 SD_JOURNAL_FOREACH_DATA(self->j, msg, msg_len) {
147 delim_ptr = memchr(msg, '=', msg_len);
148 key = unicode_FromStringAndSize(msg, delim_ptr - (const char*) msg);
149 value = PyBytes_FromStringAndSize(delim_ptr + 1, (const char*) msg + msg_len - (delim_ptr + 1) );
150 if (PyDict_Contains(dict, key)) {
151 cur_value = PyDict_GetItem(dict, key);
152 if (PyList_CheckExact(cur_value)) {
153 PyList_Append(cur_value, value);
155 tmp_list = PyList_New(0);
156 PyList_Append(tmp_list, cur_value);
157 PyList_Append(tmp_list, value);
158 PyDict_SetItem(dict, key, tmp_list);
162 PyDict_SetItem(dict, key, value);
170 if (sd_journal_get_realtime_usec(self->j, &realtime) == 0) {
171 char realtime_str[20];
172 sprintf(realtime_str, "%llu", (long long unsigned) realtime);
173 key = unicode_FromString("__REALTIME_TIMESTAMP");
174 value = PyBytes_FromString(realtime_str);
175 PyDict_SetItem(dict, key, value);
184 if (sd_journal_get_monotonic_usec(self->j, &monotonic, &sd_id) == 0) {
185 char monotonic_str[20];
186 sprintf(monotonic_str, "%llu", (long long unsigned) monotonic);
187 key = unicode_FromString("__MONOTONIC_TIMESTAMP");
188 value = PyBytes_FromString(monotonic_str);
190 PyDict_SetItem(dict, key, value);
198 if (sd_journal_get_cursor(self->j, &cursor) > 0) { //Should return 0...
199 key = unicode_FromString("__CURSOR");
200 value = PyBytes_FromString(cursor);
201 PyDict_SetItem(dict, key, value);
211 PyDoc_STRVAR(Journal_get_previous__doc__,
212 "get_previous([skip]) -> dict\n\n"
213 "Return dictionary of the previous log entry. Optional skip value\n"
214 "will return the -`skip`th log entry. Equivalent to get_next(-skip).");
216 Journal_get_previous(Journal *self, PyObject *args)
219 if (! PyArg_ParseTuple(args, "|L", &skip))
222 return PyObject_CallMethod((PyObject *)self, (char*) "get_next",
226 PyDoc_STRVAR(Journal_add_match__doc__,
227 "add_match(match) -> None\n\n"
228 "Add a match to filter journal log entries. All matches of different\n"
229 "fields are combined in logical AND, and matches of the same field\n"
230 "are automatically combined in logical OR.\n"
231 "Match is string of form \"field=value\".");
233 Journal_add_match(Journal *self, PyObject *args, PyObject *keywds)
237 if (!PyArg_ParseTuple(args, "s#", &match, &match_len))
240 r = sd_journal_add_match(self->j, match, match_len);
241 set_error(r, NULL, "Invalid match");
248 PyDoc_STRVAR(Journal_add_disjunction__doc__,
249 "add_disjunction() -> None\n\n"
250 "Once called, all matches before and after are combined in logical\n"
253 Journal_add_disjunction(Journal *self, PyObject *args)
256 r = sd_journal_add_disjunction(self->j);
257 set_error(r, NULL, NULL);
263 PyDoc_STRVAR(Journal_flush_matches__doc__,
264 "flush_matches() -> None\n\n"
265 "Clears all current match filters.");
267 Journal_flush_matches(Journal *self, PyObject *args)
269 sd_journal_flush_matches(self->j);
273 PyDoc_STRVAR(Journal_seek__doc__,
274 "seek(offset[, whence]) -> None\n\n"
275 "Seek through journal by `offset` number of entries. Argument\n"
276 "`whence` defines what the offset is relative to:\n"
277 "os.SEEK_SET (default) from first match in journal;\n"
278 "os.SEEK_CUR from current position in journal;\n"
279 "and os.SEEK_END is from last match in journal.");
281 Journal_seek(Journal *self, PyObject *args, PyObject *keywds)
284 int whence = SEEK_SET;
285 PyObject *result = NULL;
287 static const char* const kwlist[] = {"offset", "whence", NULL};
288 if (!PyArg_ParseTupleAndKeywords(args, keywds, "L|i", (char**) kwlist,
295 Py_BEGIN_ALLOW_THREADS
296 r = sd_journal_seek_head(self->j);
298 if (set_error(r, NULL, NULL))
302 result = PyObject_CallMethod((PyObject *)self, (char*) "get_next",
303 (char*) "L", offset);
307 result = PyObject_CallMethod((PyObject *)self, (char*) "get_next",
308 (char*) "L", offset);
312 Py_BEGIN_ALLOW_THREADS
313 r = sd_journal_seek_tail(self->j);
315 if (set_error(r, NULL, NULL))
318 result = PyObject_CallMethod((PyObject *)self, (char*) "get_next",
319 (char*) "L", offset < 0LL ? offset : -1LL);
323 PyErr_SetString(PyExc_ValueError, "Invalid value for whence");
327 if (PyErr_Occurred())
332 PyDoc_STRVAR(Journal_seek_realtime__doc__,
333 "seek_realtime(realtime) -> None\n\n"
334 "Seek to nearest matching journal entry to `realtime`. Argument\n"
335 "`realtime` can must be an integer unix timestamp.");
337 Journal_seek_realtime(Journal *self, PyObject *args)
343 if (!PyArg_ParseTuple(args, "d", &timedouble))
346 timestamp = (uint64_t) (timedouble * 1.0E6);
347 if ((int64_t) timestamp < 0LL) {
348 PyErr_SetString(PyExc_ValueError, "Time must be a positive integer");
352 Py_BEGIN_ALLOW_THREADS
353 r = sd_journal_seek_realtime_usec(self->j, timestamp);
355 if (set_error(r, NULL, NULL))
360 PyDoc_STRVAR(Journal_seek_monotonic__doc__,
361 "seek_monotonic(monotonic[, bootid]) -> None\n\n"
362 "Seek to nearest matching journal entry to `monotonic`. Argument\n"
363 "`monotonic` is an timestamp from boot in seconds.\n"
364 "Argument `bootid` is a string representing which boot the\n"
365 "monotonic time is reference to. Defaults to current bootid.");
367 Journal_seek_monotonic(Journal *self, PyObject *args)
375 if (! PyArg_ParseTuple(args, "d|z", &timedouble, &bootid))
378 timestamp = (uint64_t) (timedouble * 1.0E6);
380 if ((int64_t) timestamp < 0LL) {
381 PyErr_SetString(PyExc_ValueError, "Time must be positive number");
386 r = sd_id128_from_string(bootid, &sd_id);
387 if (set_error(r, NULL, "Invalid bootid"))
390 Py_BEGIN_ALLOW_THREADS
391 r = sd_id128_get_boot(&sd_id);
393 if (set_error(r, NULL, NULL))
397 Py_BEGIN_ALLOW_THREADS
398 r = sd_journal_seek_monotonic_usec(self->j, sd_id, timestamp);
400 if (set_error(r, NULL, NULL))
405 PyDoc_STRVAR(Journal_wait__doc__,
406 "wait([timeout]) -> Change state (integer)\n\n"
407 "Waits until there is a change in the journal. Argument `timeout`\n"
408 "is the maximum number of seconds to wait before returning\n"
409 "regardless if journal has changed. If `timeout` is not given or is\n"
410 "0, then it will block forever.\n"
411 "Will return constants: NOP if no change; APPEND if new\n"
412 "entries have been added to the end of the journal; and\n"
413 "INVALIDATE if journal files have been added or removed.");
415 Journal_wait(Journal *self, PyObject *args, PyObject *keywds)
418 int64_t timeout = 0LL;
420 if (!PyArg_ParseTuple(args, "|L", &timeout))
423 Py_BEGIN_ALLOW_THREADS
424 r = sd_journal_wait(self->j, timeout ==0 ? (uint64_t) -1 : timeout * 1E6);
426 if (set_error(r, NULL, NULL))
429 return long_FromLong(r);
432 PyDoc_STRVAR(Journal_seek_cursor__doc__,
433 "seek_cursor(cursor) -> None\n\n"
434 "Seeks to journal entry by given unique reference `cursor`.");
436 Journal_seek_cursor(Journal *self, PyObject *args)
441 if (! PyArg_ParseTuple(args, "s", &cursor))
444 Py_BEGIN_ALLOW_THREADS
445 r = sd_journal_seek_cursor(self->j, cursor);
447 if (set_error(r, NULL, "Invalid cursor"))
453 Journal_iter(PyObject *self)
460 Journal_iternext(PyObject *self)
463 Py_ssize_t dict_size;
465 dict = PyObject_CallMethod(self, (char*) "get_next", (char*) "");
466 if (PyErr_Occurred())
468 dict_size = PyDict_Size(dict);
469 if ((int64_t) dict_size > 0LL) {
473 PyErr_SetNone(PyExc_StopIteration);
478 PyDoc_STRVAR(Journal_query_unique__doc__,
479 "query_unique(field) -> a set of values\n\n"
480 "Returns a set of unique values in journal for given `field`.\n"
481 "Note this does not respect any journal matches.");
483 Journal_query_unique(Journal *self, PyObject *args)
489 PyObject *value_set, *key, *value;
491 if (! PyArg_ParseTuple(args, "s", &query))
494 Py_BEGIN_ALLOW_THREADS
495 r = sd_journal_query_unique(self->j, query);
497 if (set_error(r, NULL, "Invalid field name"))
500 value_set = PySet_New(0);
501 key = unicode_FromString(query);
503 SD_JOURNAL_FOREACH_UNIQUE(self->j, uniq, uniq_len) {
504 const char *delim_ptr;
506 delim_ptr = memchr(uniq, '=', uniq_len);
507 value = PyBytes_FromStringAndSize(delim_ptr + 1, (const char*) uniq + uniq_len - (delim_ptr + 1));
508 PySet_Add(value_set, value);
516 Journal_get_data_threshold(Journal *self, void *closure)
521 r = sd_journal_get_data_threshold(self->j, &cvalue);
522 if (set_error(r, NULL, NULL))
525 return long_FromSize_t(cvalue);
529 Journal_set_data_threshold(Journal *self, PyObject *value, void *closure)
533 PyErr_SetString(PyExc_TypeError, "Cannot delete data threshold");
536 if (!long_Check(value)){
537 PyErr_SetString(PyExc_TypeError, "Data threshold must be an int");
540 r = sd_journal_set_data_threshold(self->j, (size_t) long_AsLong(value));
541 return set_error(r, NULL, NULL);
544 static PyGetSetDef Journal_getseters[] = {
545 {(char*) "data_threshold",
546 (getter)Journal_get_data_threshold,
547 (setter)Journal_set_data_threshold,
548 (char*) "data threshold",
553 static PyMethodDef Journal_methods[] = {
554 {"get_next", (PyCFunction)Journal_get_next, METH_VARARGS,
555 Journal_get_next__doc__},
556 {"get_previous", (PyCFunction)Journal_get_previous, METH_VARARGS,
557 Journal_get_previous__doc__},
558 {"add_match", (PyCFunction)Journal_add_match, METH_VARARGS|METH_KEYWORDS,
559 Journal_add_match__doc__},
560 {"add_disjunction", (PyCFunction)Journal_add_disjunction, METH_NOARGS,
561 Journal_add_disjunction__doc__},
562 {"flush_matches", (PyCFunction)Journal_flush_matches, METH_NOARGS,
563 Journal_flush_matches__doc__},
564 {"seek", (PyCFunction)Journal_seek, METH_VARARGS | METH_KEYWORDS,
565 Journal_seek__doc__},
566 {"seek_realtime", (PyCFunction)Journal_seek_realtime, METH_VARARGS,
567 Journal_seek_realtime__doc__},
568 {"seek_monotonic", (PyCFunction)Journal_seek_monotonic, METH_VARARGS,
569 Journal_seek_monotonic__doc__},
570 {"wait", (PyCFunction)Journal_wait, METH_VARARGS,
571 Journal_wait__doc__},
572 {"seek_cursor", (PyCFunction)Journal_seek_cursor, METH_VARARGS,
573 Journal_seek_cursor__doc__},
574 {"query_unique", (PyCFunction)Journal_query_unique, METH_VARARGS,
575 Journal_query_unique__doc__},
576 {NULL} /* Sentinel */
579 static PyTypeObject JournalType = {
580 PyVarObject_HEAD_INIT(NULL, 0)
581 "_reader._Journal", /*tp_name*/
582 sizeof(Journal), /*tp_basicsize*/
584 (destructor)Journal_dealloc, /*tp_dealloc*/
591 0, /*tp_as_sequence*/
599 Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,/*tp_flags*/
600 Journal__doc__, /* tp_doc */
603 0, /* tp_richcompare */
604 0, /* tp_weaklistoffset */
605 Journal_iter, /* tp_iter */
606 Journal_iternext, /* tp_iternext */
607 Journal_methods, /* tp_methods */
609 Journal_getseters, /* tp_getset */
612 0, /* tp_descr_get */
613 0, /* tp_descr_set */
614 0, /* tp_dictoffset */
615 (initproc)Journal_init, /* tp_init */
617 PyType_GenericNew, /* tp_new */
620 #if PY_MAJOR_VERSION >= 3
621 static PyModuleDef _reader_module = {
622 PyModuleDef_HEAD_INIT,
624 "Module that reads systemd journal similar to journalctl.",
626 NULL, NULL, NULL, NULL, NULL
630 #pragma GCC diagnostic push
631 #pragma GCC diagnostic ignored "-Wmissing-prototypes"
634 #if PY_MAJOR_VERSION >= 3
644 if (PyType_Ready(&JournalType) < 0)
645 #if PY_MAJOR_VERSION >= 3
651 #if PY_MAJOR_VERSION >= 3
652 m = PyModule_Create(&_reader_module);
656 m = Py_InitModule3("_reader", NULL,
657 "Module that reads systemd journal similar to journalctl.");
662 Py_INCREF(&JournalType);
663 PyModule_AddObject(m, "_Journal", (PyObject *)&JournalType);
664 PyModule_AddIntConstant(m, "NOP", SD_JOURNAL_NOP);
665 PyModule_AddIntConstant(m, "APPEND", SD_JOURNAL_APPEND);
666 PyModule_AddIntConstant(m, "INVALIDATE", SD_JOURNAL_INVALIDATE);
667 PyModule_AddIntConstant(m, "LOCAL_ONLY", SD_JOURNAL_LOCAL_ONLY);
668 PyModule_AddIntConstant(m, "RUNTIME_ONLY", SD_JOURNAL_RUNTIME_ONLY);
669 PyModule_AddIntConstant(m, "SYSTEM_ONLY", SD_JOURNAL_SYSTEM_ONLY);
671 #if PY_MAJOR_VERSION >= 3
676 #pragma GCC diagnostic pop