Guard the replay, warn on dropped lines, cap captured text, and register the action as an apply action

This commit is contained in:
J. Nick Koston
2026-10-13 10:33:49 -10:00
parent bbec20a66b
commit e57fca4f78
5 changed files with 47 additions and 19 deletions
+6 -18
View File
@@ -402,8 +402,9 @@ async def to_code(config: ConfigType) -> None:
if (boot_log_buffer_size := config[CONF_BOOT_LOG_BUFFER_SIZE]) > 0:
cg.add_define("USE_LOGGER_BOOT_LOG_BUFFER")
cg.add_define("ESPHOME_LOGGER_BOOT_LOG_BUFFER_SIZE", boot_log_buffer_size)
cg.add_build_flag(
f"-DESPHOME_LOGGER_BOOT_LOG_LEVEL={LOG_LEVELS[config[CONF_BOOT_LOG_LEVEL]]}"
cg.add_define(
"ESPHOME_LOGGER_BOOT_LOG_LEVEL",
cg.RawExpression(str(LOG_LEVELS[config[CONF_BOOT_LOG_LEVEL]])),
)
if boot_logs_config := config[CONF_BOOT_LOGS]:
cg.add_define("USE_LOGGER_BOOT_LOG_PER_TAG_LEVELS")
@@ -663,30 +664,17 @@ async def logger_set_level_to_code(
)
@automation.register_action(
automation.register_apply_action(
"logger.replay_boot_logs",
LambdaAction,
automation.maybe_conf(
CONF_LOGGER_ID,
{
cv.GenerateID(CONF_LOGGER_ID): cv.use_id(Logger),
},
),
synchronous=True,
automation.ApplyCall("replay_boot_logs()"),
id_key=CONF_LOGGER_ID,
)
async def logger_replay_boot_logs_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
logger = await cg.get_variable(config[CONF_LOGGER_ID])
text = str(cg.statement(logger.replay_boot_logs()))
lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
return automation.new_lambda_pvariable(
action_id, lambda_, StatelessLambdaAction, template_arg
)
FILTER_SOURCE_FILES = filter_source_files_from_platform(
+20 -1
View File
@@ -218,6 +218,9 @@ void Logger::process_messages_() {
#ifdef USE_LOGGER_BOOT_LOG_BUFFER
// Record layout in boot_log_buffer_: u16 text_len | u8 level | u32 millis | const char *tag | text
static constexpr uint16_t BOOT_LOG_RECORD_HEADER_SIZE = 2 + 1 + 4 + sizeof(const char *);
// Replay writes "\033[0;3Xm@4294967295ms " (22 bytes) and "\033[0m" (4 bytes) around the stored text
static constexpr uint16_t BOOT_LOG_REPLAY_PREFIX_SIZE = 22;
static constexpr uint16_t BOOT_LOG_MAX_TEXT = ESPHOME_LOGGER_TX_BUFFER_SIZE - BOOT_LOG_REPLAY_PREFIX_SIZE - 4;
void Logger::boot_log_capture_(uint8_t level, const char *tag, const LogBuffer &buf) {
if (this->boot_log_done_ || level > boot_level_for(tag) || level == ESPHOME_LOG_LEVEL_NONE)
@@ -235,6 +238,9 @@ void Logger::boot_log_capture_(uint8_t level, const char *tag, const LogBuffer &
}
if (len >= 4 && memcmp(text + len - 4, "\033[0m", 4) == 0)
len -= 4;
// Leave room in the transmit buffer for the replay prefix and reset so no stored text is cut on replay
if (len > BOOT_LOG_MAX_TEXT)
len = BOOT_LOG_MAX_TEXT;
if (this->boot_log_pos_ + BOOT_LOG_RECORD_HEADER_SIZE + len > ESPHOME_LOGGER_BOOT_LOG_BUFFER_SIZE) {
this->boot_log_dropped_++;
return;
@@ -252,7 +258,20 @@ void Logger::boot_log_capture_(uint8_t level, const char *tag, const LogBuffer &
void Logger::replay_boot_logs() {
// Stop capturing first so the replay output is not buffered again.
this->boot_log_done_ = true;
ESP_LOGI(TAG, "Replaying boot logs (%u bytes used, %u lines dropped)", this->boot_log_pos_, this->boot_log_dropped_);
// Dropped lines are reported at WARN so a device at level WARN still learns the buffer was too small
esp_log_printf_(this->boot_log_dropped_ > 0 ? ESPHOME_LOG_LEVEL_WARN : ESPHOME_LOG_LEVEL_INFO, TAG, __LINE__,
ESPHOME_LOG_FORMAT("Replaying boot logs (%u bytes used, %u lines dropped)"), this->boot_log_pos_,
this->boot_log_dropped_);
// The replay shares tx_buffer_ with normal logging, so a listener that logs during
// the replay is dropped like any other re-entrant log; see process_messages_()
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
bool &recursion_guard = this->main_task_recursion_guard_;
#else
bool &recursion_guard = this->global_recursion_guard_;
#endif
if (recursion_guard)
return;
RecursionGuard guard(recursion_guard);
uint16_t pos = 0;
while (pos + BOOT_LOG_RECORD_HEADER_SIZE <= this->boot_log_pos_) {
const char *p = this->boot_log_buffer_ + pos;
+3
View File
@@ -17,6 +17,9 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
# read the replay through a log listener, which nothing else registers here.
cg.add_define("USE_LOGGER_BOOT_LOG_BUFFER")
cg.add_define("ESPHOME_LOGGER_BOOT_LOG_BUFFER_SIZE", 512)
cg.add_define(
"ESPHOME_LOGGER_BOOT_LOG_LEVEL", cg.RawExpression("ESPHOME_LOG_LEVEL_WARN")
)
request_log_listener()
manifest.to_code = to_code
@@ -99,4 +99,19 @@ TEST_F(BootLogBufferTest, LinesThatDoNotFitAreCounted) {
EXPECT_TRUE(summary);
}
TEST_F(BootLogBufferTest, LongestLineIsReplayedWithoutLosingItsTail) {
// A line that fills the transmit buffer must still fit once the replay adds its stamp
std::string text(ESPHOME_LOGGER_TX_BUFFER_SIZE, 'x');
ESP_LOGW(BOOT_TAG, "%s", text.c_str());
this->listen();
this->logger_->replay_boot_logs();
const auto lines = this->replayed();
ASSERT_EQ(lines.size(), 1u);
EXPECT_LE(lines[0].message.size(), static_cast<size_t>(ESPHOME_LOGGER_TX_BUFFER_SIZE));
// The reset code at the end shows the stored text was not cut short by the prefix
EXPECT_EQ(lines[0].message.substr(lines[0].message.size() - 4), "\033[0m") << lines[0].message.size();
EXPECT_NE(lines[0].message.find("xxxxxxxxxx\033[0m"), std::string::npos);
}
} // namespace esphome::logger::testing
@@ -9,3 +9,6 @@ esphome:
logger:
id: logger_id
boot_log_buffer_size: 2kB
boot_log_level: INFO
boot_logs:
wifi: DEBUG