LogListener Virtual Interface Replaced with LogCallback
The logger::LogListener abstract class has been removed. Components that receive log messages must now register a callback with logger::global_logger->add_log_callback(instance, callback) instead of inheriting from LogListener and calling add_log_listener().
This is a breaking change for external components in ESPHome 2026.3.0 and later.
Background
PR #14084: Replace LogListener virtual interface with LogCallback struct
The LogListener pattern required every log-receiving class to inherit from LogListener and implement its pure virtual on_log() method. This added a vtable sub-table, thunk code, and constructor overhead to every implementer — all unnecessary since the call pattern is a simple function dispatch.
The replacement uses a LogCallback struct containing a function pointer + instance pointer. Non-capturing lambdas at registration sites decay to plain function pointers at compile time, producing zero closure overhead.
Savings: 112 bytes of flash saved across the built-in implementers (APIServer, WebServer, MQTTClientComponent, Syslog), with net zero heap change.
What's Changing
The LogListener class and add_log_listener() method are removed entirely:
// Removed
class LogListener {
public:
virtual void on_log(uint8_t level, const char *tag,
const char *message, size_t message_len) = 0;
};
// Removed
void Logger::add_log_listener(LogListener *listener);
Replaced by:
// New
struct LogCallback {
void *instance;
void (*callback)(void *instance, uint8_t level, const char *tag,
const char *message, size_t message_len);
};
void Logger::add_log_callback(void *instance,
void (*callback)(void *, uint8_t, const char *, const char *, size_t));
Who This Affects
External components that inherit from logger::LogListener to receive log messages. Common use cases include custom log forwarders, remote logging over serial/network, and debug components.
Standard YAML configurations are not affected.
Migration Guide
Replace LogListener inheritance with add_log_callback()
// Before
#include "esphome/components/logger/logger.h"
class MyLogForwarder : public Component, public logger::LogListener {
public:
void setup() override {
if (logger::global_logger != nullptr)
logger::global_logger->add_log_listener(this);
}
void on_log(uint8_t level, const char *tag,
const char *message, size_t message_len) override {
// Forward log message
this->send_log(level, tag, message, message_len);
}
};
// After
#include "esphome/components/logger/logger.h"
class MyLogForwarder : public Component {
public:
void setup() override {
if (logger::global_logger != nullptr)
logger::global_logger->add_log_callback(
this,
[](void *self, uint8_t level, const char *tag,
const char *message, size_t message_len) {
static_cast<MyLogForwarder *>(self)->on_log(
level, tag, message, message_len);
});
}
void on_log(uint8_t level, const char *tag,
const char *message, size_t message_len) {
// Forward log message (no longer 'override')
this->send_log(level, tag, message, message_len);
}
};
Key changes:
- Remove
, public logger::LogListenerfrom the class declaration - Replace
add_log_listener(this)withadd_log_callback(this, lambda) - The lambda must be non-capturing (use the
void *selfparameter instead) - Remove
overridefromon_log()— it's now a regular method
Supporting Multiple ESPHome Versions
void setup() override {
if (logger::global_logger != nullptr) {
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2026, 3, 0)
logger::global_logger->add_log_callback(
this,
[](void *self, uint8_t level, const char *tag,
const char *message, size_t message_len) {
static_cast<MyLogForwarder *>(self)->on_log(
level, tag, message, message_len);
});
#else
logger::global_logger->add_log_listener(this);
#endif
}
}
When using the version guard, keep the LogListener inheritance behind the same guard:
class MyLogForwarder : public Component
#if ESPHOME_VERSION_CODE < VERSION_CODE(2026, 3, 0)
, public logger::LogListener
#endif
{
// ...
};
Timeline
- ESPHome 2026.3.0 (March 2026):
LogListenerclass andadd_log_listener()removed,add_log_callback(instance, callback)available
Finding Code That Needs Updates
# Find LogListener inheritance
grep -rn 'LogListener' your_component/
# Find add_log_listener calls
grep -rn 'add_log_listener' your_component/
# Find on_log overrides
grep -rn 'on_log.*override' your_component/
Questions?
If you have questions about migrating your external component, please ask in:
- ESPHome Discord - #devs channel
- ESPHome GitHub Discussions
Related Documentation
Comments
Feel free to leave a comment here to discuss this post wth others. You can ask questions, share your experience, or suggest improvements. If you have a question about a specific feature or issue, please consider using the ESPHome Discord. Stick to English and follow ESPHome's code of conduct. These comments exist on a discussion on GitHub, so you can also comment there directly if you prefer.