[Concept,13/27] sandbox: log: Add file log driver

Message ID 20260119204130.3972647-14-sjg@u-boot.org
State New
Headers
Series Expo debugging and textedit improvements (part E) |

Commit Message

Simon Glass Jan. 19, 2026, 8:41 p.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

Add a new log driver that writes log records to a file. This is useful
for capturing log output during testing or debugging in sandbox.

The filename can be set via the 'log_file' environment variable, or
programmatically using log_file_set_fname().

Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 common/Kconfig            |   8 +++
 common/Makefile           |   1 +
 common/log_file.c         | 103 ++++++++++++++++++++++++++++++++++++++
 configs/sandbox_defconfig |   2 +
 doc/develop/logging.rst   |   5 ++
 include/log.h             |  11 ++++
 lib/getopt.c              |   4 +-
 test/log/Makefile         |   1 +
 test/log/log_file_test.c  |  43 ++++++++++++++++
 test/log/log_filter.c     |   9 ++--
 10 files changed, 181 insertions(+), 6 deletions(-)
 create mode 100644 common/log_file.c
 create mode 100644 test/log/log_file_test.c
  

Patch

diff --git a/common/Kconfig b/common/Kconfig
index 597bea70b9b..8d3bda6f588 100644
--- a/common/Kconfig
+++ b/common/Kconfig
@@ -453,6 +453,14 @@  config LOG_SYSLOG
 	  Enables a log driver which broadcasts log records via UDP port 514
 	  to syslog servers.
 
+config LOG_FILE
+	bool "Log output to a file (sandbox only)"
+	depends on SANDBOX
+	help
+	  Enables a log driver which writes log records to a file. Set the
+	  'log_file' environment variable to the filename to use, or call
+	  log_file_set_fname() to set it programmatically.
+
 config SPL_LOG
 	bool "Enable logging support in SPL"
 	depends on LOG && SPL
diff --git a/common/Makefile b/common/Makefile
index fdf4cff94f4..125f768ef53 100644
--- a/common/Makefile
+++ b/common/Makefile
@@ -97,6 +97,7 @@  obj-$(CONFIG_DFU_OVER_USB) += dfu.o
 obj-y += command.o
 obj-$(CONFIG_$(PHASE_)LOG) += log.o
 obj-$(CONFIG_$(PHASE_)LOG_CONSOLE) += log_console.o
+obj-$(CONFIG_LOG_FILE) += log_file.o
 obj-$(CONFIG_$(PHASE_)LOG_SYSLOG) += log_syslog.o
 obj-y += s_record.o
 obj-$(CONFIG_CMD_LOADB) += xyzModem.o
diff --git a/common/log_file.c b/common/log_file.c
new file mode 100644
index 00000000000..6f445a9d561
--- /dev/null
+++ b/common/log_file.c
@@ -0,0 +1,103 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Log driver to write to a file (sandbox only)
+ *
+ * Copyright 2026 Canonical Ltd
+ * Written by Simon Glass <simon.glass@canonical.com>
+ */
+
+#include <env.h>
+#include <log.h>
+#include <os.h>
+#include <asm/global_data.h>
+
+DECLARE_GLOBAL_DATA_PTR;
+
+static int log_fd = -1;
+
+static void append(char **buf, char *buf_end, const char *fmt, ...)
+{
+	va_list args;
+	size_t size = buf_end - *buf;
+
+	va_start(args, fmt);
+	vsnprintf(*buf, size, fmt, args);
+	va_end(args);
+	*buf += strlen(*buf);
+}
+
+int log_file_set_fname(const char *fname)
+{
+	if (log_fd != -1) {
+		os_close(log_fd);
+		log_fd = -1;
+	}
+
+	if (!fname)
+		return 0;
+
+	log_fd = os_open(fname, OS_O_WRONLY | OS_O_CREAT | OS_O_TRUNC);
+	if (log_fd < 0)
+		return log_fd;
+
+	return 0;
+}
+
+static int log_file_emit(struct log_device *ldev, struct log_rec *rec)
+{
+	int fmt = gd->log_fmt;
+	char buf[512];
+	char *buf_end = buf + sizeof(buf);
+	char *ptr = buf;
+	const char *fname;
+	int len;
+
+	/* If no file open, try to open one from the environment */
+	if (log_fd == -1) {
+		fname = env_get("log_file");
+		if (!fname)
+			return 0;
+
+		log_fd = os_open(fname, OS_O_WRONLY | OS_O_CREAT | OS_O_TRUNC);
+		if (log_fd < 0)
+			return 0;
+	}
+
+	/*
+	 * The output format is designed to give someone a fighting chance of
+	 * figuring out which field is which:
+	 *    - level is in CAPS
+	 *    - cat is lower case and ends with comma
+	 *    - file normally has a .c extension and ends with a colon
+	 *    - line is integer and ends with a -
+	 *    - function is an identifier and ends with ()
+	 *    - message has a space before it unless it is on its own
+	 */
+	if (!(rec->flags & LOGRECF_CONT) && fmt != BIT(LOGF_MSG)) {
+		if (fmt & BIT(LOGF_LEVEL))
+			append(&ptr, buf_end, "%s.",
+			       log_get_level_name(rec->level));
+		if (fmt & BIT(LOGF_CAT))
+			append(&ptr, buf_end, "%s,",
+			       log_get_cat_name(rec->cat));
+		if (fmt & BIT(LOGF_FILE))
+			append(&ptr, buf_end, "%s:", rec->file);
+		if (fmt & BIT(LOGF_LINE))
+			append(&ptr, buf_end, "%d-", rec->line);
+		if (fmt & BIT(LOGF_FUNC))
+			append(&ptr, buf_end, "%s() ", rec->func ?: "?");
+	}
+	if (fmt & BIT(LOGF_MSG))
+		append(&ptr, buf_end, "%s", rec->msg);
+
+	len = ptr - buf;
+	os_write(log_fd, buf, len);
+
+	return 0;
+}
+
+LOG_DRIVER(file) = {
+	.name	= "file",
+	.emit	= log_file_emit,
+	.flags	= LOGDF_ENABLE,
+};
diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig
index 4dbb77abcff..93ef88d597e 100644
--- a/configs/sandbox_defconfig
+++ b/configs/sandbox_defconfig
@@ -57,6 +57,7 @@  CONFIG_LOG=y
 CONFIG_LOG_MAX_LEVEL=9
 CONFIG_LOG_DEFAULT_LEVEL=6
 CONFIG_LOGF_FUNC=y
+CONFIG_LOG_FILE=y
 CONFIG_DISPLAY_BOARDINFO_LATE=y
 CONFIG_STACKPROTECTOR=y
 CONFIG_ANDROID_AB=y
@@ -149,6 +150,7 @@  CONFIG_CMD_EROFS=y
 CONFIG_CMD_EXT4_WRITE=y
 CONFIG_CMD_SQUASHFS=y
 CONFIG_CMD_MTDPARTS=y
+CONFIG_CMD_LOG=y
 CONFIG_CMD_STACKPROTECTOR_TEST=y
 CONFIG_MAC_PARTITION=y
 CONFIG_OF_CONTROL=y
diff --git a/doc/develop/logging.rst b/doc/develop/logging.rst
index d7a40c94bf0..6314cae380c 100644
--- a/doc/develop/logging.rst
+++ b/doc/develop/logging.rst
@@ -172,10 +172,15 @@  enabled or disabled independently:
 
 * console - goes to stdout
 * syslog - broadcast RFC 3164 messages to syslog servers on UDP port 514
+* file - writes to a file (sandbox only)
 
 The syslog driver sends the value of environmental variable 'log_hostname' as
 HOSTNAME if available.
 
+The file driver sends log records to a file and is only available in sandbox.
+Set the 'log_file' environment variable to specify the filename, or call
+log_file_set_fname() to set it programmatically.
+
 Filters
 -------
 
diff --git a/include/log.h b/include/log.h
index 8e933071cf1..c79042f1a5c 100644
--- a/include/log.h
+++ b/include/log.h
@@ -675,6 +675,17 @@  int log_remove_filter(const char *drv_name, int filter_num);
  */
 int log_device_set_enable(struct log_driver *drv, bool enable);
 
+/**
+ * log_file_set_fname() - Set the filename for the file log driver
+ *
+ * This sets or changes the file used by the file log driver. If a file is
+ * already open it is closed first.
+ *
+ * @fname: Filename to use, or NULL to close any existing file
+ * Return: 0 if OK, -ve on error
+ */
+int log_file_set_fname(const char *fname);
+
 #if CONFIG_IS_ENABLED(LOG)
 /**
  * log_init() - Set up the log system ready for use
diff --git a/lib/getopt.c b/lib/getopt.c
index e9175e2fff4..c7bb6d3671a 100644
--- a/lib/getopt.c
+++ b/lib/getopt.c
@@ -25,8 +25,8 @@  int __getopt(struct getopt_state *gs, int argc, char *const argv[],
 	const char *curoptp; /* pointer to the current option in optstring */
 
 	while (1) {
-		log_debug("arg_index: %d index: %d\n", gs->arg_index,
-			  gs->index);
+		log_content("arg_index: %d index: %d\n", gs->arg_index,
+			    gs->index);
 
 		/* `--` indicates the end of options */
 		if (gs->arg_index == 1 && argv[gs->index] &&
diff --git a/test/log/Makefile b/test/log/Makefile
index 24b7c46786d..6fc45d59a16 100644
--- a/test/log/Makefile
+++ b/test/log/Makefile
@@ -10,6 +10,7 @@  ifdef CONFIG_UT_LOG
 ifdef CONFIG_SANDBOX
 obj-$(CONFIG_LOG_SYSLOG) += syslog_test.o
 obj-$(CONFIG_LOG_SYSLOG) += syslog_test_ndebug.o
+obj-$(CONFIG_LOG_FILE) += log_file_test.o
 endif
 
 ifdef CONFIG_LOG
diff --git a/test/log/log_file_test.c b/test/log/log_file_test.c
new file mode 100644
index 00000000000..e1d6b7dedf1
--- /dev/null
+++ b/test/log/log_file_test.c
@@ -0,0 +1,43 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2026 Canonical Ltd
+ * Written by Simon Glass <simon.glass@canonical.com>
+ *
+ * Test for log file driver
+ */
+
+#include <command.h>
+#include <log.h>
+#include <os.h>
+#include <test/log.h>
+#include <test/ut.h>
+
+/* Test that the log_file driver can write to a file */
+static int log_test_file_driver(struct unit_test_state *uts)
+{
+	const char *fname = "/tmp/log_test.log";
+	void *buf;
+	int size;
+
+	ut_assertok(log_file_set_fname(fname));
+
+	/* Generate some log messages using log rec command */
+	run_command("log format Lfm", 0);
+	run_command("log rec none warning test.c 123 my_func 'Test message'", 0);
+	run_command("log rec none err error.c 456 err_func 'Error occurred'", 0);
+
+	/* Close the file so we can read it */
+	ut_assertok(log_file_set_fname(NULL));
+
+	/* Read the file contents */
+	ut_assertok(os_read_file(fname, &buf, &size));
+
+	/* Check the contents */
+	ut_asserteq_strn("123-my_func() Test message\n", buf);
+	ut_assertnonnull(strstr(buf, "456-err_func() Error occurred\n"));
+
+	os_free(buf);
+
+	return 0;
+}
+LOG_TEST(log_test_file_driver);
diff --git a/test/log/log_filter.c b/test/log/log_filter.c
index 8622dcf2913..05ff4e94bd2 100644
--- a/test/log/log_filter.c
+++ b/test/log/log_filter.c
@@ -39,11 +39,12 @@  static int log_test_filter(struct unit_test_state *uts)
 
 #define create_filter(args, filter_num) do {\
 	ut_assertok(run_command("log filter-add -p " args, 0)); \
+	console_record_readline(uts->actual_str, sizeof(uts->actual_str)); \
 	ut_assertok(strict_strtoul(uts->actual_str, 10, &(filter_num))); \
 	ut_assert_console_end(); \
 } while (0)
 
-	create_filter("", filt1);
+	create_filter("-l info", filt1);
 	create_filter("-DL warning -cmmc -cspi -ffile", filt2);
 
 	ldev = log_device_find_by_name("console");
@@ -52,7 +53,7 @@  static int log_test_filter(struct unit_test_state *uts)
 		if (filt->filter_num == filt1) {
 			filt1_found = true;
 			ut_asserteq(0, filt->flags);
-			ut_asserteq(LOGL_MAX, filt->level);
+			ut_asserteq(LOGL_INFO, filt->level);
 			ut_assertnull(filt->file_list);
 		} else if (filt->filter_num == filt2) {
 			filt2_found = true;
@@ -89,8 +90,8 @@  static int log_test_filter(struct unit_test_state *uts)
 	ut_asserteq(false, filt1_found);
 	ut_asserteq(false, filt2_found);
 
-	create_filter("", filt1);
-	create_filter("", filt2);
+	create_filter("-l info", filt1);
+	create_filter("-l info", filt2);
 
 	ut_assertok(run_command("log filter-remove -a", 0));
 	ut_assert_console_end();