blob: 19b0855886f7877973274da12e81c3f89c6e02c3 [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>
Stephan Pleines31f98da2024-05-22 17:31:23 -070019#include <stddef.h>
20#include <stdint.h>
James Kuszmaul7daef362019-12-31 18:28:17 -080021
Stephan Pleines31f98da2024-05-22 17:31:23 -070022#include <algorithm>
Tyler Chatowbf0609c2021-07-31 16:13:27 -070023#include <cerrno>
Stephan Pleines31f98da2024-05-22 17:31:23 -070024#include <chrono>
James Kuszmaul7daef362019-12-31 18:28:17 -080025#include <memory>
Stephan Pleines31f98da2024-05-22 17:31:23 -070026#include <string>
27#include <vector>
28
Austin Schuh99f7c6a2024-06-25 22:07:44 -070029#include "absl/log/check.h"
30#include "absl/log/log.h"
Stephan Pleines31f98da2024-05-22 17:31:23 -070031#include "absl/types/span.h"
James Kuszmaul7daef362019-12-31 18:28:17 -080032
33#include "aos/configuration.h"
Stephan Pleines31f98da2024-05-22 17:31:23 -070034#include "aos/events/context.h"
35#include "aos/events/event_loop.h"
Austin Schuhb06f03b2021-02-17 22:00:37 -080036#include "aos/events/logging/log_reader.h"
James Kuszmaul7daef362019-12-31 18:28:17 -080037#include "aos/events/simulated_event_loop.h"
38#include "aos/flatbuffer_merge.h"
Stephan Pleines31f98da2024-05-22 17:31:23 -070039#include "aos/flatbuffers.h"
Austin Schuh094d09b2020-11-20 23:26:52 -080040#include "aos/init.h"
James Kuszmaul7daef362019-12-31 18:28:17 -080041#include "aos/json_to_flatbuffer.h"
Stephan Pleines31f98da2024-05-22 17:31:23 -070042#include "aos/time/time.h"
James Kuszmaul7daef362019-12-31 18:28:17 -080043
Stephan Pleines9e40c8e2024-02-07 20:58:28 -080044namespace aos::analysis {
James Kuszmaul7daef362019-12-31 18:28:17 -080045namespace {
46
47// All the data corresponding to a single message.
48struct MessageData {
49 aos::monotonic_clock::time_point monotonic_sent_time;
50 aos::realtime_clock::time_point realtime_sent_time;
51 // JSON representation of the message.
52 std::string json_data;
53};
54
55// Data corresponding to an entire channel.
56struct ChannelData {
57 std::string name;
58 std::string type;
59 // Each message published on the channel, in order by monotonic time.
60 std::vector<MessageData> messages;
61};
62
63// All the objects that we need for managing reading a logfile.
64struct LogReaderTools {
65 std::unique_ptr<aos::logger::LogReader> reader;
James Kuszmaul7daef362019-12-31 18:28:17 -080066 // Event loop to use for subscribing to buses.
67 std::unique_ptr<aos::EventLoop> event_loop;
68 std::vector<ChannelData> channel_data;
69 // Whether we have called process() on the reader yet.
70 bool processed = false;
71};
72
73struct LogReaderType {
74 PyObject_HEAD;
75 LogReaderTools *tools = nullptr;
76};
77
78void LogReader_dealloc(LogReaderType *self) {
79 LogReaderTools *tools = self->tools;
James Kuszmaul7daef362019-12-31 18:28:17 -080080 delete tools;
81 Py_TYPE(self)->tp_free((PyObject *)self);
82}
83
84PyObject *LogReader_new(PyTypeObject *type, PyObject * /*args*/,
Tyler Chatowbf0609c2021-07-31 16:13:27 -070085 PyObject * /*kwds*/) {
James Kuszmaul7daef362019-12-31 18:28:17 -080086 LogReaderType *self;
87 self = (LogReaderType *)type->tp_alloc(type, 0);
88 if (self != nullptr) {
89 self->tools = new LogReaderTools();
90 if (self->tools == nullptr) {
91 return nullptr;
92 }
93 }
94 return (PyObject *)self;
95}
96
97int LogReader_init(LogReaderType *self, PyObject *args, PyObject *kwds) {
Austin Schuh094d09b2020-11-20 23:26:52 -080098 int count = 1;
99 if (!aos::IsInitialized()) {
100 // Fake out argc and argv to let InitGoogle run properly to instrument
101 // malloc, setup glog, and such.
102 char *name = program_invocation_name;
103 char **argv = &name;
104 aos::InitGoogle(&count, &argv);
105 }
106
James Kuszmaul7daef362019-12-31 18:28:17 -0800107 const char *kwlist[] = {"log_file_name", nullptr};
108
109 const char *log_file_name;
110 if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char **>(kwlist),
111 &log_file_name)) {
112 return -1;
113 }
114
Austin Schuh6bdcc372024-06-27 14:49:11 -0700115 LogReaderTools *tools = self->tools;
116 CHECK(tools != nullptr);
James Kuszmaul7daef362019-12-31 18:28:17 -0800117 tools->reader = std::make_unique<aos::logger::LogReader>(log_file_name);
James Kuszmaul84ff3e52020-01-03 19:48:53 -0800118 tools->reader->Register();
James Kuszmaul7daef362019-12-31 18:28:17 -0800119
Austin Schuh9fe5d202020-02-29 15:09:53 -0800120 if (aos::configuration::MultiNode(tools->reader->configuration())) {
121 tools->event_loop = tools->reader->event_loop_factory()->MakeEventLoop(
122 "data_fetcher",
123 aos::configuration::GetNode(tools->reader->configuration(), "roborio"));
124 } else {
125 tools->event_loop =
126 tools->reader->event_loop_factory()->MakeEventLoop("data_fetcher");
127 }
James Kuszmaul7daef362019-12-31 18:28:17 -0800128 tools->event_loop->SkipTimingReport();
Tyler Chatow67ddb032020-01-12 14:30:04 -0800129 tools->event_loop->SkipAosLog();
James Kuszmaul7daef362019-12-31 18:28:17 -0800130
131 return 0;
132}
133
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700134PyObject *LogReader_get_data_for_channel(LogReaderType *self, PyObject *args,
135 PyObject *kwds) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800136 const char *kwlist[] = {"name", "type", nullptr};
137
138 const char *name;
139 const char *type;
140 if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
141 const_cast<char **>(kwlist), &name, &type)) {
142 return nullptr;
143 }
144
Austin Schuh6bdcc372024-06-27 14:49:11 -0700145 LogReaderTools *tools = self->tools;
146 CHECK(tools != nullptr);
James Kuszmaul7daef362019-12-31 18:28:17 -0800147
148 if (!tools->processed) {
149 PyErr_SetString(PyExc_RuntimeError,
150 "Called get_data_for_bus before calling process().");
151 return nullptr;
152 }
153
154 for (const auto &channel : tools->channel_data) {
155 if (channel.name == name && channel.type == type) {
156 PyObject *list = PyList_New(channel.messages.size());
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700157 for (size_t ii = 0; ii < channel.messages.size(); ++ii) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800158 const auto &message = channel.messages[ii];
159 PyObject *monotonic_time = PyLong_FromLongLong(
160 std::chrono::duration_cast<std::chrono::nanoseconds>(
161 message.monotonic_sent_time.time_since_epoch())
162 .count());
163 PyObject *realtime_time = PyLong_FromLongLong(
164 std::chrono::duration_cast<std::chrono::nanoseconds>(
165 message.realtime_sent_time.time_since_epoch())
166 .count());
167 PyObject *json_data = PyUnicode_FromStringAndSize(
168 message.json_data.data(), message.json_data.size());
169 PyObject *entry =
170 PyTuple_Pack(3, monotonic_time, realtime_time, json_data);
171 if (PyList_SetItem(list, ii, entry) != 0) {
172 return nullptr;
173 }
174 }
175 return list;
176 }
177 }
178 PyErr_SetString(PyExc_ValueError,
179 "The provided channel was never subscribed to.");
180 return nullptr;
181}
182
183PyObject *LogReader_subscribe(LogReaderType *self, PyObject *args,
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700184 PyObject *kwds) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800185 const char *kwlist[] = {"name", "type", nullptr};
186
187 const char *name;
188 const char *type;
189 if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
190 const_cast<char **>(kwlist), &name, &type)) {
191 return nullptr;
192 }
193
Austin Schuh6bdcc372024-06-27 14:49:11 -0700194 LogReaderTools *tools = self->tools;
195 CHECK(tools != nullptr);
James Kuszmaul7daef362019-12-31 18:28:17 -0800196
197 if (tools->processed) {
198 PyErr_SetString(PyExc_RuntimeError,
199 "Called subscribe after calling process().");
200 return nullptr;
201 }
202
203 const aos::Channel *const channel = aos::configuration::GetChannel(
204 tools->reader->configuration(), name, type, "", nullptr);
205 if (channel == nullptr) {
206 return Py_False;
207 }
208 const int index = tools->channel_data.size();
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700209 tools->channel_data.push_back({.name = name, .type = type, .messages = {}});
James Kuszmaul7daef362019-12-31 18:28:17 -0800210 tools->event_loop->MakeRawWatcher(
211 channel, [channel, index, tools](const aos::Context &context,
212 const void *message) {
213 tools->channel_data[index].messages.push_back(
214 {.monotonic_sent_time = context.monotonic_event_time,
215 .realtime_sent_time = context.realtime_event_time,
216 .json_data = aos::FlatbufferToJson(
217 channel->schema(), static_cast<const uint8_t *>(message))});
218 });
219 return Py_True;
220}
221
222static PyObject *LogReader_process(LogReaderType *self,
223 PyObject *Py_UNUSED(ignored)) {
Austin Schuh6bdcc372024-06-27 14:49:11 -0700224 LogReaderTools *tools = self->tools;
225 CHECK(tools != nullptr);
James Kuszmaul7daef362019-12-31 18:28:17 -0800226
227 if (tools->processed) {
228 PyErr_SetString(PyExc_RuntimeError, "process() may only be called once.");
229 return nullptr;
230 }
231
232 tools->processed = true;
233
James Kuszmaul84ff3e52020-01-03 19:48:53 -0800234 tools->reader->event_loop_factory()->Run();
James Kuszmaul7daef362019-12-31 18:28:17 -0800235
236 Py_RETURN_NONE;
237}
238
239static PyObject *LogReader_configuration(LogReaderType *self,
240 PyObject *Py_UNUSED(ignored)) {
Austin Schuh6bdcc372024-06-27 14:49:11 -0700241 LogReaderTools *tools = self->tools;
242 CHECK(tools != nullptr);
James Kuszmaul7daef362019-12-31 18:28:17 -0800243
244 // I have no clue if the Configuration that we get from the log reader is in a
245 // contiguous chunk of memory, and I'm too lazy to either figure it out or
246 // figure out how to extract the actual data buffer + offset.
247 // Instead, copy the flatbuffer and return a copy of the new buffer.
248 aos::FlatbufferDetachedBuffer<aos::Configuration> buffer =
249 aos::CopyFlatBuffer(tools->reader->configuration());
250
251 return PyBytes_FromStringAndSize(
Austin Schuhadd6eb32020-11-09 21:24:26 -0800252 reinterpret_cast<const char *>(buffer.span().data()),
253 buffer.span().size());
James Kuszmaul7daef362019-12-31 18:28:17 -0800254}
255
256static PyMethodDef LogReader_methods[] = {
257 {"configuration", (PyCFunction)LogReader_configuration, METH_NOARGS,
258 "Return a bytes buffer for the Configuration of the logfile."},
259 {"process", (PyCFunction)LogReader_process, METH_NOARGS,
260 "Processes the logfile and all the subscribed to channels."},
261 {"subscribe", (PyCFunction)LogReader_subscribe,
262 METH_VARARGS | METH_KEYWORDS,
263 "Attempts to subscribe to the provided channel name + type. Returns True "
264 "if successful."},
265 {"get_data_for_channel", (PyCFunction)LogReader_get_data_for_channel,
266 METH_VARARGS | METH_KEYWORDS,
267 "Returns the logged data for a given channel. Raises an exception if you "
268 "did not subscribe to the provided channel. Returned data is a list of "
269 "tuples where each tuple is of the form (monotonic_nsec, realtime_nsec, "
270 "json_message_data)."},
271 {nullptr, 0, 0, nullptr} /* Sentinel */
272};
273
Brian Silverman4c7235a2021-11-17 19:04:37 -0800274#ifdef __clang__
275// These extensions to C++ syntax do surprising things in C++, but for these
276// uses none of them really matter I think, and the alternatives are really
277// annoying.
278#pragma clang diagnostic ignored "-Wc99-designator"
279#endif
280
James Kuszmaul7daef362019-12-31 18:28:17 -0800281static PyTypeObject LogReaderType = {
Brian Silverman4c7235a2021-11-17 19:04:37 -0800282 PyVarObject_HEAD_INIT(NULL, 0)
283 // The previous macro initializes some fields, leave a comment to help
284 // clang-format not make this uglier.
285 .tp_name = "py_log_reader.LogReader",
James Kuszmaul7daef362019-12-31 18:28:17 -0800286 .tp_basicsize = sizeof(LogReaderType),
287 .tp_itemsize = 0,
James Kuszmaul7daef362019-12-31 18:28:17 -0800288 .tp_dealloc = (destructor)LogReader_dealloc,
Brian Silverman4c7235a2021-11-17 19:04:37 -0800289 .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
290 .tp_doc = "LogReader objects",
James Kuszmaul7daef362019-12-31 18:28:17 -0800291 .tp_methods = LogReader_methods,
Brian Silverman4c7235a2021-11-17 19:04:37 -0800292 .tp_init = (initproc)LogReader_init,
293 .tp_new = LogReader_new,
James Kuszmaul7daef362019-12-31 18:28:17 -0800294};
295
296static PyModuleDef log_reader_module = {
297 PyModuleDef_HEAD_INIT,
298 .m_name = "py_log_reader",
299 .m_doc = "Example module that creates an extension type.",
300 .m_size = -1,
301};
302
303PyObject *InitModule() {
304 PyObject *m;
305 if (PyType_Ready(&LogReaderType) < 0) return nullptr;
306
307 m = PyModule_Create(&log_reader_module);
308 if (m == nullptr) return nullptr;
309
310 Py_INCREF(&LogReaderType);
311 if (PyModule_AddObject(m, "LogReader", (PyObject *)&LogReaderType) < 0) {
312 Py_DECREF(&LogReaderType);
313 Py_DECREF(m);
314 return nullptr;
315 }
316
317 return m;
318}
319
320} // namespace
Stephan Pleines9e40c8e2024-02-07 20:58:28 -0800321} // namespace aos::analysis
James Kuszmaul7daef362019-12-31 18:28:17 -0800322
323PyMODINIT_FUNC PyInit_py_log_reader(void) {
Stephan Pleines9e40c8e2024-02-07 20:58:28 -0800324 return aos::analysis::InitModule();
James Kuszmaul7daef362019-12-31 18:28:17 -0800325}