Keep the root topic out of the log: print endpoint ids and topics without their root

The root topic identifies the account, and Serial logs get posted in issues and forums.
"repeated directive on <topic> ignored" and the lines for dropped directives printed the full
MQTT topic; they now start with the endpoint id. The lines for subscriptions and publishes print
the topic relative to the root (AlexaBridgeLogic::topicForLog), and begin() prints the length of
the root topic instead of its text. No line at any level prints the root topic, the username or
the password.
basicLight: RAM 34,476 -> 34,508 B, flash 338,281 -> 338,533 B; host tests 45 -> 47.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 18:45:18 +00:00
parent 2ed52c8721
commit d534f23f27
7 changed files with 79 additions and 25 deletions

View file

@ -100,8 +100,9 @@ void Alex2ESP::begin(const char *username, const char *password, const char *roo
discoverTopicSend = this->rootTopic + "/discover_r";
directiveFilter = this->rootTopic + "/+/alexaDirective";
// Log the root topic (never the credentials)
ALEX2ESP_LOGI("root topic %s", this->rootTopic.c_str());
// The root topic identifies the account, like the credentials: the log has its length, which is enough to see
// that one was passed
ALEX2ESP_LOGI("root topic of %u characters", (unsigned)this->rootTopic.length());
// Reports carry the time of their samples, so the board needs the time of day: UTC from SNTP
if (useSntp)
@ -278,11 +279,11 @@ void Alex2ESP::onSubscribe(uint16_t packetId, uint8_t qos)
if (qos == SUBSCRIPTION_REFUSED)
{
// The session stays open and the state SUBSCRIBING: the broker would refuse the same topic again
ALEX2ESP_LOGE("the broker refused the subscription to %s: check the root topic passed to begin()", topic.c_str());
ALEX2ESP_LOGE("the broker refused the subscription to <root>/%s: check the root topic passed to begin()", logName(topic.c_str()));
return;
}
ALEX2ESP_LOGD("subscribed to %s", topic.c_str());
ALEX2ESP_LOGD("subscribed to <root>/%s", logName(topic.c_str()));
if (subscriptionsPending > 0 && --subscriptionsPending == 0)
{
state = Alex2ESPState::CONNECTED;
@ -337,7 +338,7 @@ void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessagePrope
// boards: not an error, and not worth a buffer
if (index == 0)
{
ALEX2ESP_LOGD("directive on %s is for another board", topic);
ALEX2ESP_LOGD("%.*s: directive for another board", (int)endpointLength, endpointId);
}
return;
}
@ -345,22 +346,22 @@ void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessagePrope
switch (directives.append(payload, length, index, total))
{
case AlexaDirectiveBuffer::Result::TOO_LARGE:
ALEX2ESP_LOGE("directive of %u bytes on %s dropped: the limit is %u (ALEX2ESP_MAX_DIRECTIVE)", (unsigned)total, topic, (unsigned)ALEX2ESP_MAX_DIRECTIVE);
ALEX2ESP_LOGE("%.*s: directive of %u bytes dropped: the limit is %u (ALEX2ESP_MAX_DIRECTIVE)", (int)endpointLength, endpointId, (unsigned)total, (unsigned)ALEX2ESP_MAX_DIRECTIVE);
break;
case AlexaDirectiveBuffer::Result::QUEUE_FULL:
ALEX2ESP_LOGE("directive of %u bytes on %s dropped: %u directives, %u bytes, already wait for loop()", (unsigned)total, topic, (unsigned)directives.waitingDirectives(), (unsigned)directives.waitingBytes());
ALEX2ESP_LOGE("%.*s: directive of %u bytes dropped: %u directives, %u bytes, already wait for loop()", (int)endpointLength, endpointId, (unsigned)total, (unsigned)directives.waitingDirectives(), (unsigned)directives.waitingBytes());
break;
case AlexaDirectiveBuffer::Result::NO_MEMORY:
ALEX2ESP_LOGE("directive of %u bytes on %s dropped: no memory (%u bytes of heap free)", (unsigned)total, topic, (unsigned)ESP.getFreeHeap());
ALEX2ESP_LOGE("%.*s: directive of %u bytes dropped: no memory (%u bytes of heap free)", (int)endpointLength, endpointId, (unsigned)total, (unsigned)ESP.getFreeHeap());
break;
case AlexaDirectiveBuffer::Result::OUT_OF_ORDER:
ALEX2ESP_LOGE("directive on %s dropped: the fragment at byte %u of %u does not continue it", topic, (unsigned)index, (unsigned)total);
ALEX2ESP_LOGE("%.*s: directive dropped: the fragment at byte %u of %u does not continue it", (int)endpointLength, endpointId, (unsigned)index, (unsigned)total);
break;
case AlexaDirectiveBuffer::Result::EMPTY:
ALEX2ESP_LOGE("directive on %s dropped: it is empty", topic);
ALEX2ESP_LOGE("%.*s: directive dropped: it is empty", (int)endpointLength, endpointId);
break;
case AlexaDirectiveBuffer::Result::DUPLICATE:
ALEX2ESP_LOGI("repeated directive on %s ignored", topic);
ALEX2ESP_LOGI("%.*s: repeated directive ignored", (int)endpointLength, endpointId);
break;
case AlexaDirectiveBuffer::Result::INCOMPLETE:
case AlexaDirectiveBuffer::Result::COMPLETE:
@ -509,7 +510,7 @@ AlexaSendResult Alex2ESP::publish(const char *topic, JsonDocument &doc)
AlexaSendResult result = trySend(topic, doc, &length);
if (result == AlexaSendResult::OK)
{
ALEX2ESP_LOGD("%u bytes -> %s, %u bytes of heap free", (unsigned)length, topic, (unsigned)ESP.getFreeHeap());
ALEX2ESP_LOGD("%u bytes -> %s, %u bytes of heap free", (unsigned)length, logName(topic), (unsigned)ESP.getFreeHeap());
}
else
{
@ -582,28 +583,33 @@ AlexaSendResult Alex2ESP::trySend(const char *topic, JsonDocument &doc, size_t *
return result;
}
const char *Alex2ESP::logName(const char *topic) const
{
return AlexaBridgeLogic::topicForLog(topic, rootTopic.c_str());
}
void Alex2ESP::logRefusal(const char *topic, AlexaSendResult result, size_t length)
{
switch (result)
{
case AlexaSendResult::NOT_CONNECTED:
ALEX2ESP_LOGE("%s: %u bytes not sent, no session with the broker", topic, (unsigned)length);
ALEX2ESP_LOGE("%s: %u bytes not sent, no session with the broker", logName(topic), (unsigned)length);
break;
case AlexaSendResult::REFUSED:
ALEX2ESP_LOGE("%s: %u bytes not sent, the MQTT client or the heap is full (%u bytes free)", topic, (unsigned)length, (unsigned)ESP.getFreeHeap());
ALEX2ESP_LOGE("%s: %u bytes not sent, the MQTT client or the heap is full (%u bytes free)", logName(topic), (unsigned)length, (unsigned)ESP.getFreeHeap());
break;
case AlexaSendResult::TOO_LARGE:
if (length == 0)
{
ALEX2ESP_LOGE("%s: not sent, the message ran out of memory while it was built", topic);
ALEX2ESP_LOGE("%s: not sent, the message ran out of memory while it was built", logName(topic));
}
else
{
ALEX2ESP_LOGE("%s: %u bytes not sent, the limit is %u (ALEX2ESP_MAX_MESSAGE)", topic, (unsigned)length, (unsigned)ALEX2ESP_MAX_MESSAGE);
ALEX2ESP_LOGE("%s: %u bytes not sent, the limit is %u (ALEX2ESP_MAX_MESSAGE)", logName(topic), (unsigned)length, (unsigned)ALEX2ESP_MAX_MESSAGE);
}
break;
case AlexaSendResult::EMPTY:
ALEX2ESP_LOGE("%s: not sent, the message is empty", topic);
ALEX2ESP_LOGE("%s: not sent, the message is empty", logName(topic));
break;
case AlexaSendResult::OK:
break;

View file

@ -130,6 +130,7 @@ private:
AlexaDevice *findDevice(const char *endpointId, size_t length);
AlexaSendResult trySend(const char *topic, JsonDocument &doc, size_t *length);
void logRefusal(const char *topic, AlexaSendResult result, size_t length);
const char *logName(const char *topic) const; // The topic as the log may print it: without the root topic
};
#endif // ALEX2ESP_H

View file

@ -280,6 +280,22 @@ const char *AlexaBridgeLogic::directiveEndpoint(const char *topic, const char *r
return endpointId;
}
const char *AlexaBridgeLogic::topicForLog(const char *topic, const char *rootTopic)
{
static const char outside[] = "(topic outside the root)";
if (topic == nullptr || rootTopic == nullptr)
{
return outside;
}
size_t rootLength = strlen(rootTopic);
if (strncmp(topic, rootTopic, rootLength) != 0 || topic[rootLength] != '/')
{
return outside;
}
return topic + rootLength + 1;
}
AlexaSendResult AlexaBridgeLogic::checkMessage(const JsonDocument &doc, size_t maxLength, size_t *length)
{
if (length != nullptr)

View file

@ -206,6 +206,11 @@ namespace AlexaBridgeLogic
// topic that 1.x boards use.
const char *directiveEndpoint(const char *topic, const char *rootTopic, size_t *length);
// The topic as a log line may print it. The root topic identifies the account and a log gets posted in forums
// and issues, so "<root>/ESP-01/alexaResponce" is printed as "ESP-01/alexaResponce". A topic that is not under
// the root is not printed at all, it may still begin with it: the result is a fixed text then.
const char *topicForLog(const char *topic, const char *rootTopic);
// Decides whether a document may be published and measures it: TOO_LARGE when it is over maxLength bytes or
// ran out of memory while it was built (it would be sent truncated), EMPTY when it holds nothing, otherwise OK
// with the serialised size in length.

View file

@ -1,7 +1,7 @@
// Serial diagnostics of the library. A line is printed when its level is within the level chosen at run time
// (Alex2ESP::setLogLevel, INFO unless changed) and within the ceiling compiled in (ALEX2ESP_LOG_MAX); a line above
// the ceiling costs no flash. Whatever the library refuses or drops is reported at ERROR. Credentials and
// correlation tokens are never printed.
// the ceiling costs no flash. Whatever the library refuses or drops is reported at ERROR. Credentials, the root
// topic (it identifies the account) and correlation tokens are never printed.
#ifndef ALEXA_LOG_H
#define ALEXA_LOG_H