diff --git a/src/libcamera/include/.keep_empty b/src/libcamera/include/.keep_empty
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/src/libcamera/include/log.h b/src/libcamera/include/log.h
new file mode 100644
index 000000000000..76acd1520868
--- /dev/null
+++ b/src/libcamera/include/log.h
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018, Google Inc.
+ *
+ * log.h - Logging infrastructure
+ */
+#ifndef __LIBCAMERA_LOG_H__
+#define __LIBCAMERA_LOG_H__
+
+#include <sstream>
+
+namespace libcamera {
+
+enum LogSeverity {
+	LogInfo,
+	LogWarning,
+	LogError,
+};
+
+class LogMessage
+{
+public:
+	LogMessage(const char *fileName, unsigned int line,
+		  LogSeverity severity);
+	LogMessage(const LogMessage&) = delete;
+	~LogMessage();
+
+	std::ostream& stream() { return msgStream; }
+
+private:
+	std::ostringstream msgStream;
+};
+
+#define LOG(severity) LogMessage(__FILE__, __LINE__, Log##severity).stream()
+
+};
+
+#endif /* __LIBCAMERA_LOG_H__ */
diff --git a/src/libcamera/include/utils.h b/src/libcamera/include/utils.h
new file mode 100644
index 000000000000..3ffa6f4ea591
--- /dev/null
+++ b/src/libcamera/include/utils.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018, Google Inc.
+ *
+ * utils.h - Miscellaneous utility functions
+ */
+#ifndef __LIBCAMERA_UTILS_H__
+#define __LIBCAMERA_UTILS_H__
+
+#define ARRAY_SIZE(a)	(sizeof(a) / sizeof(a[0]))
+
+#endif /* __LIBCAMERA_UTILS_H__ */
diff --git a/src/libcamera/log.cpp b/src/libcamera/log.cpp
new file mode 100644
index 000000000000..18ccfa32d8b4
--- /dev/null
+++ b/src/libcamera/log.cpp
@@ -0,0 +1,81 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018, Google Inc.
+ *
+ * log.h - Logging infrastructure
+ */
+
+#include <cstdio>
+#include <ctime>
+#include <iomanip>
+#include <string.h>
+
+#include "log.h"
+#include "utils.h"
+
+/**
+ * \file log.h
+ * \brief Logging infrastructure
+ */
+
+namespace libcamera {
+
+/**
+ * \enum LogSeverity
+ * Log message severity
+ * \var Info
+ * Informational message
+ * \var Warning
+ * Warning message, signals a potential issue
+ * \var Error
+ * Error message, signals an unrecoverable issue
+ */
+
+/**
+ * \def LOG(severity)
+ * \brief Log a message
+ *
+ * Return an std::ostream reference to which a message can be logged using the
+ * iostream API. The \a severity controls whether the message is printed or
+ * dropped, depending on the global log level.
+ */
+
+static const char *log_severity_name(LogSeverity severity)
+{
+	static const char * const names[] = {
+		"INFO",
+		"WARN",
+		" ERR",
+	};
+
+	if ((unsigned int)severity < ARRAY_SIZE(names))
+		return names[severity];
+	else
+		return "UNKN";
+}
+
+LogMessage::LogMessage(const char *fileName, unsigned int line,
+		       LogSeverity severity)
+{
+	/* Log the timestamp, severity and file information. */
+	struct timespec timestamp;
+	clock_gettime(CLOCK_MONOTONIC, &timestamp);
+	msgStream << "[" << timestamp.tv_sec / (60 * 60) << ":"
+		  << std::setw(2) << (timestamp.tv_sec / 60) % 60 << ":"
+		  << std::setw(2) << timestamp.tv_sec % 60 << "."
+		  << std::setw(9) << timestamp.tv_nsec << "]";
+
+	msgStream << " " << log_severity_name(severity);
+	msgStream << " " << basename(fileName) << ":" << line << " ";
+}
+
+LogMessage::~LogMessage()
+{
+	msgStream << std::endl;
+
+	std::string msg(msgStream.str());
+	fwrite(msg.data(), msg.size(), 1, stderr);
+	fflush(stderr);
+}
+
+};
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index 07d9cd448342..fe38f8b2b5b4 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -1,4 +1,7 @@
-sources = ['main.cpp']
+sources = files([
+    'log.cpp',
+    'main.cpp',
+])
 
 includes = [
     libcamera_includes,
