blob: 4a97a0fbb248c8a464d1f386290b6185ba93974f [file] [log] [blame]
James Kuszmaul7daef362019-12-31 18:28:17 -08001// This file provides a Python module for reading logfiles. See
2// log_reader_test.py for usage.
3//
James Kuszmauld3a6d8a2021-06-23 21:08:22 -07004// NOTE: This code has not been maintained recently, and so is missing key
5// features to support reading multi-node logfiles (namely, it assumes the the
6// logfile is just a single file). Updating this code should not be difficult,
7// but hasn't been needed thus far.
8//
James Kuszmaul7daef362019-12-31 18:28:17 -08009// This reader works by having the user specify exactly what channels they want
10// data for. We then process the logfile and store all the data on that channel
11// into a list of timestamps + JSON message data. The user can then use an
12// accessor method (get_data_for_channel) to retrieve the cached data.
13
14// Defining PY_SSIZE_T_CLEAN seems to be suggested by most of the Python
15// documentation.
16#define PY_SSIZE_T_CLEAN
17// Note that Python.h needs to be included before anything else.
18#include <Python.h>
19
Tyler Chatowbf0609c2021-07-31 16:13:27 -070020#include <cerrno>
James Kuszmaul7daef362019-12-31 18:28:17 -080021#include <memory>
22
23#include "aos/configuration.h"
Austin Schuhb06f03b2021-02-17 22:00:37 -080024#include "aos/events/logging/log_reader.h"
James Kuszmaul7daef362019-12-31 18:28:17 -080025#include "aos/events/simulated_event_loop.h"
26#include "aos/flatbuffer_merge.h"
Austin Schuh094d09b2020-11-20 23:26:52 -080027#include "aos/init.h"
James Kuszmaul7daef362019-12-31 18:28:17 -080028#include "aos/json_to_flatbuffer.h"
29
Stephan Pleines9e40c8e2024-02-07 20:58:28 -080030namespace aos::analysis {
James Kuszmaul7daef362019-12-31 18:28:17 -080031namespace {
32
33// All the data corresponding to a single message.
34struct MessageData {
35 aos::monotonic_clock::time_point monotonic_sent_time;
36 aos::realtime_clock::time_point realtime_sent_time;
37 // JSON representation of the message.
38 std::string json_data;
39};
40
41// Data corresponding to an entire channel.
42struct ChannelData {
43 std::string name;
44 std::string type;
45 // Each message published on the channel, in order by monotonic time.
46 std::vector<MessageData> messages;
47};
48
49// All the objects that we need for managing reading a logfile.
50struct LogReaderTools {
51 std::unique_ptr<aos::logger::LogReader> reader;
James Kuszmaul7daef362019-12-31 18:28:17 -080052 // Event loop to use for subscribing to buses.
53 std::unique_ptr<aos::EventLoop> event_loop;
54 std::vector<ChannelData> channel_data;
55 // Whether we have called process() on the reader yet.
56 bool processed = false;
57};
58
59struct LogReaderType {
60 PyObject_HEAD;
61 LogReaderTools *tools = nullptr;
62};
63
64void LogReader_dealloc(LogReaderType *self) {
65 LogReaderTools *tools = self->tools;
James Kuszmaul7daef362019-12-31 18:28:17 -080066 delete tools;
67 Py_TYPE(self)->tp_free((PyObject *)self);
68}
69
70PyObject *LogReader_new(PyTypeObject *type, PyObject * /*args*/,
Tyler Chatowbf0609c2021-07-31 16:13:27 -070071 PyObject * /*kwds*/) {
James Kuszmaul7daef362019-12-31 18:28:17 -080072 LogReaderType *self;
73 self = (LogReaderType *)type->tp_alloc(type, 0);
74 if (self != nullptr) {
75 self->tools = new LogReaderTools();
76 if (self->tools == nullptr) {
77 return nullptr;
78 }
79 }
80 return (PyObject *)self;
81}
82
83int LogReader_init(LogReaderType *self, PyObject *args, PyObject *kwds) {
Austin Schuh094d09b2020-11-20 23:26:52 -080084 int count = 1;
85 if (!aos::IsInitialized()) {
86 // Fake out argc and argv to let InitGoogle run properly to instrument
87 // malloc, setup glog, and such.
88 char *name = program_invocation_name;
89 char **argv = &name;
90 aos::InitGoogle(&count, &argv);
91 }
92
James Kuszmaul7daef362019-12-31 18:28:17 -080093 const char *kwlist[] = {"log_file_name", nullptr};
94
95 const char *log_file_name;
96 if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char **>(kwlist),
97 &log_file_name)) {
98 return -1;
99 }
100
101 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
102 tools->reader = std::make_unique<aos::logger::LogReader>(log_file_name);
James Kuszmaul84ff3e52020-01-03 19:48:53 -0800103 tools->reader->Register();
James Kuszmaul7daef362019-12-31 18:28:17 -0800104
Austin Schuh9fe5d202020-02-29 15:09:53 -0800105 if (aos::configuration::MultiNode(tools->reader->configuration())) {
106 tools->event_loop = tools->reader->event_loop_factory()->MakeEventLoop(
107 "data_fetcher",
108 aos::configuration::GetNode(tools->reader->configuration(), "roborio"));
109 } else {
110 tools->event_loop =
111 tools->reader->event_loop_factory()->MakeEventLoop("data_fetcher");
112 }
James Kuszmaul7daef362019-12-31 18:28:17 -0800113 tools->event_loop->SkipTimingReport();
Tyler Chatow67ddb032020-01-12 14:30:04 -0800114 tools->event_loop->SkipAosLog();
James Kuszmaul7daef362019-12-31 18:28:17 -0800115
116 return 0;
117}
118
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700119PyObject *LogReader_get_data_for_channel(LogReaderType *self, PyObject *args,
120 PyObject *kwds) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800121 const char *kwlist[] = {"name", "type", nullptr};
122
123 const char *name;
124 const char *type;
125 if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
126 const_cast<char **>(kwlist), &name, &type)) {
127 return nullptr;
128 }
129
130 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
131
132 if (!tools->processed) {
133 PyErr_SetString(PyExc_RuntimeError,
134 "Called get_data_for_bus before calling process().");
135 return nullptr;
136 }
137
138 for (const auto &channel : tools->channel_data) {
139 if (channel.name == name && channel.type == type) {
140 PyObject *list = PyList_New(channel.messages.size());
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700141 for (size_t ii = 0; ii < channel.messages.size(); ++ii) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800142 const auto &message = channel.messages[ii];
143 PyObject *monotonic_time = PyLong_FromLongLong(
144 std::chrono::duration_cast<std::chrono::nanoseconds>(
145 message.monotonic_sent_time.time_since_epoch())
146 .count());
147 PyObject *realtime_time = PyLong_FromLongLong(
148 std::chrono::duration_cast<std::chrono::nanoseconds>(
149 message.realtime_sent_time.time_since_epoch())
150 .count());
151 PyObject *json_data = PyUnicode_FromStringAndSize(
152 message.json_data.data(), message.json_data.size());
153 PyObject *entry =
154 PyTuple_Pack(3, monotonic_time, realtime_time, json_data);
155 if (PyList_SetItem(list, ii, entry) != 0) {
156 return nullptr;
157 }
158 }
159 return list;
160 }
161 }
162 PyErr_SetString(PyExc_ValueError,
163 "The provided channel was never subscribed to.");
164 return nullptr;
165}
166
167PyObject *LogReader_subscribe(LogReaderType *self, PyObject *args,
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700168 PyObject *kwds) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800169 const char *kwlist[] = {"name", "type", nullptr};
170
171 const char *name;
172 const char *type;
173 if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
174 const_cast<char **>(kwlist), &name, &type)) {
175 return nullptr;
176 }
177
178 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
179
180 if (tools->processed) {
181 PyErr_SetString(PyExc_RuntimeError,
182 "Called subscribe after calling process().");
183 return nullptr;
184 }
185
186 const aos::Channel *const channel = aos::configuration::GetChannel(
187 tools->reader->configuration(), name, type, "", nullptr);
188 if (channel == nullptr) {
189 return Py_False;
190 }
191 const int index = tools->channel_data.size();
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700192 tools->channel_data.push_back({.name = name, .type = type, .messages = {}});
James Kuszmaul7daef362019-12-31 18:28:17 -0800193 tools->event_loop->MakeRawWatcher(
194 channel, [channel, index, tools](const aos::Context &context,
195 const void *message) {
196 tools->channel_data[index].messages.push_back(
197 {.monotonic_sent_time = context.monotonic_event_time,
198 .realtime_sent_time = context.realtime_event_time,
199 .json_data = aos::FlatbufferToJson(
200 channel->schema(), static_cast<const uint8_t *>(message))});
201 });
202 return Py_True;
203}
204
205static PyObject *LogReader_process(LogReaderType *self,
206 PyObject *Py_UNUSED(ignored)) {
207 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
208
209 if (tools->processed) {
210 PyErr_SetString(PyExc_RuntimeError, "process() may only be called once.");
211 return nullptr;
212 }
213
214 tools->processed = true;
215
James Kuszmaul84ff3e52020-01-03 19:48:53 -0800216 tools->reader->event_loop_factory()->Run();
James Kuszmaul7daef362019-12-31 18:28:17 -0800217
218 Py_RETURN_NONE;
219}
220
221static PyObject *LogReader_configuration(LogReaderType *self,
222 PyObject *Py_UNUSED(ignored)) {
223 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
224
225 // I have no clue if the Configuration that we get from the log reader is in a
226 // contiguous chunk of memory, and I'm too lazy to either figure it out or
227 // figure out how to extract the actual data buffer + offset.
228 // Instead, copy the flatbuffer and return a copy of the new buffer.
229 aos::FlatbufferDetachedBuffer<aos::Configuration> buffer =
230 aos::CopyFlatBuffer(tools->reader->configuration());
231
232 return PyBytes_FromStringAndSize(
Austin Schuhadd6eb32020-11-09 21:24:26 -0800233 reinterpret_cast<const char *>(buffer.span().data()),
234 buffer.span().size());
James Kuszmaul7daef362019-12-31 18:28:17 -0800235}
236
237static PyMethodDef LogReader_methods[] = {
238 {"configuration", (PyCFunction)LogReader_configuration, METH_NOARGS,
239 "Return a bytes buffer for the Configuration of the logfile."},
240 {"process", (PyCFunction)LogReader_process, METH_NOARGS,
241 "Processes the logfile and all the subscribed to channels."},
242 {"subscribe", (PyCFunction)LogReader_subscribe,
243 METH_VARARGS | METH_KEYWORDS,
244 "Attempts to subscribe to the provided channel name + type. Returns True "
245 "if successful."},
246 {"get_data_for_channel", (PyCFunction)LogReader_get_data_for_channel,
247 METH_VARARGS | METH_KEYWORDS,
248 "Returns the logged data for a given channel. Raises an exception if you "
249 "did not subscribe to the provided channel. Returned data is a list of "
250 "tuples where each tuple is of the form (monotonic_nsec, realtime_nsec, "
251 "json_message_data)."},
252 {nullptr, 0, 0, nullptr} /* Sentinel */
253};
254
Brian Silverman4c7235a2021-11-17 19:04:37 -0800255#ifdef __clang__
256// These extensions to C++ syntax do surprising things in C++, but for these
257// uses none of them really matter I think, and the alternatives are really
258// annoying.
259#pragma clang diagnostic ignored "-Wc99-designator"
260#endif
261
James Kuszmaul7daef362019-12-31 18:28:17 -0800262static PyTypeObject LogReaderType = {
Brian Silverman4c7235a2021-11-17 19:04:37 -0800263 PyVarObject_HEAD_INIT(NULL, 0)
264 // The previous macro initializes some fields, leave a comment to help
265 // clang-format not make this uglier.
266 .tp_name = "py_log_reader.LogReader",
James Kuszmaul7daef362019-12-31 18:28:17 -0800267 .tp_basicsize = sizeof(LogReaderType),
268 .tp_itemsize = 0,
James Kuszmaul7daef362019-12-31 18:28:17 -0800269 .tp_dealloc = (destructor)LogReader_dealloc,
Brian Silverman4c7235a2021-11-17 19:04:37 -0800270 .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
271 .tp_doc = "LogReader objects",
James Kuszmaul7daef362019-12-31 18:28:17 -0800272 .tp_methods = LogReader_methods,
Brian Silverman4c7235a2021-11-17 19:04:37 -0800273 .tp_init = (initproc)LogReader_init,
274 .tp_new = LogReader_new,
James Kuszmaul7daef362019-12-31 18:28:17 -0800275};
276
277static PyModuleDef log_reader_module = {
278 PyModuleDef_HEAD_INIT,
279 .m_name = "py_log_reader",
280 .m_doc = "Example module that creates an extension type.",
281 .m_size = -1,
282};
283
284PyObject *InitModule() {
285 PyObject *m;
286 if (PyType_Ready(&LogReaderType) < 0) return nullptr;
287
288 m = PyModule_Create(&log_reader_module);
289 if (m == nullptr) return nullptr;
290
291 Py_INCREF(&LogReaderType);
292 if (PyModule_AddObject(m, "LogReader", (PyObject *)&LogReaderType) < 0) {
293 Py_DECREF(&LogReaderType);
294 Py_DECREF(m);
295 return nullptr;
296 }
297
298 return m;
299}
300
301} // namespace
Stephan Pleines9e40c8e2024-02-07 20:58:28 -0800302} // namespace aos::analysis
James Kuszmaul7daef362019-12-31 18:28:17 -0800303
304PyMODINIT_FUNC PyInit_py_log_reader(void) {
Stephan Pleines9e40c8e2024-02-07 20:58:28 -0800305 return aos::analysis::InitModule();
James Kuszmaul7daef362019-12-31 18:28:17 -0800306}