远程日志系统基本运行成功
This commit is contained in:
@@ -105,24 +105,24 @@ Additional enforcement rules:
|
||||
## Build, Flash, and Validation
|
||||
|
||||
- Use the ESP-IDF/toolchain version declared by the project documentation or lockfiles.
|
||||
- Activate ESP-IDF v5.5.3 before building.
|
||||
- Activate ESP-IDF v5.5.4 before building.
|
||||
- Before running any `idf.py` command, first determine which activation style exists in the current environment:
|
||||
- Prefer path expressions that are portable across developer machines. Avoid hardcoded user-specific absolute paths such as `/home/<user>/...` or `/Users/<user>/...` in docs, scripts, and shell snippets when a variable-based form is sufficient.
|
||||
- For POSIX shells, prefer parameterized paths such as:
|
||||
```bash
|
||||
export ESPRESSIF_ROOT="${ESPRESSIF_ROOT:-$HOME/.espressif}"
|
||||
export IDF_VERSION_TAG="v5.5.3"
|
||||
export IDF_VERSION_TAG="v5.5.4"
|
||||
```
|
||||
- If `eim` exists on the current machine, prefer running ESP-IDF commands through `eim run`, for example:
|
||||
```bash
|
||||
eim run <COMMAND> [IDF_VERSION]
|
||||
eim run "idf.py build" v5.5.3
|
||||
eim run "idf.py build" v5.5.4
|
||||
eim run "idf.py --version"
|
||||
```
|
||||
- In sandboxed or other write-restricted environments, `eim` logging initialization may fail if the default user state/log directories are not writable. In that case, pass `--log-file` with a writable path, for example:
|
||||
```bash
|
||||
eim --log-file ./eim-build.log run "idf.py build" v5.5.3
|
||||
eim --log-file /tmp/eim.log run "idf.py --version" v5.5.3
|
||||
eim --log-file ./eim-build.log run "idf.py build" v5.5.4
|
||||
eim --log-file /tmp/eim.log run "idf.py --version" v5.5.4
|
||||
```
|
||||
- If `eim` does not exist, prefer the newer EIM-managed activation script when available:
|
||||
```bash
|
||||
|
||||
@@ -13,6 +13,7 @@ idf_component_register(
|
||||
"src/runtime/control_plane_fortune_service.c"
|
||||
"src/runtime/control_plane_system_events.c"
|
||||
"src/runtime/control_plane_key_input.c"
|
||||
"src/runtime/control_plane_remote_log_service.c"
|
||||
"src/runtime/control_plane_runtime_workers.c"
|
||||
"src/runtime/control_plane_factory_cli.c"
|
||||
INCLUDE_DIRS
|
||||
|
||||
@@ -142,3 +142,6 @@ esp_err_t control_plane_start_key_input_task(void);
|
||||
void control_plane_stop_key_input_task(void);
|
||||
esp_err_t control_plane_schedule_websocket_wait_task(void);
|
||||
void control_plane_set_ptt_active(bool active);
|
||||
|
||||
esp_err_t control_plane_remote_log_start(void);
|
||||
void control_plane_remote_log_stop(int timeout_ms);
|
||||
|
||||
@@ -0,0 +1,824 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "control_plane_internal.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "domain.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifndef CONFIG_TQ_REMOTE_LOG_ENABLE
|
||||
#define CONFIG_TQ_REMOTE_LOG_ENABLE 0
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_REMOTE_LOG_ENDPOINT
|
||||
#define CONFIG_TQ_REMOTE_LOG_ENDPOINT ""
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_REMOTE_LOG_AUTH_TOKEN
|
||||
#define CONFIG_TQ_REMOTE_LOG_AUTH_TOKEN ""
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_REMOTE_LOG_MIN_LEVEL
|
||||
#define CONFIG_TQ_REMOTE_LOG_MIN_LEVEL 2
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_REMOTE_LOG_PRESERVE_LOCAL_OUTPUT
|
||||
#define CONFIG_TQ_REMOTE_LOG_PRESERVE_LOCAL_OUTPUT 1
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_REMOTE_LOG_QUEUE_LEN
|
||||
#define CONFIG_TQ_REMOTE_LOG_QUEUE_LEN 128
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES
|
||||
#define CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES 256
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_REMOTE_LOG_BATCH_MAX_ENTRIES
|
||||
#define CONFIG_TQ_REMOTE_LOG_BATCH_MAX_ENTRIES 30
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_REMOTE_LOG_FLUSH_INTERVAL_MS
|
||||
#define CONFIG_TQ_REMOTE_LOG_FLUSH_INTERVAL_MS 5000
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_REMOTE_LOG_UPLOAD_TIMEOUT_MS
|
||||
#define CONFIG_TQ_REMOTE_LOG_UPLOAD_TIMEOUT_MS 8000
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_REMOTE_LOG_MAX_FILES
|
||||
#define CONFIG_TQ_REMOTE_LOG_MAX_FILES 32
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_REMOTE_LOG_FILE_MAX_BYTES
|
||||
#define CONFIG_TQ_REMOTE_LOG_FILE_MAX_BYTES 16384
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_NETWORK_TASK_CORE
|
||||
#define CONFIG_TQ_NETWORK_TASK_CORE 0
|
||||
#endif
|
||||
|
||||
#define REMOTE_LOG_TASK_STACK_SIZE (1024 * 8)
|
||||
#define REMOTE_LOG_TASK_PRIORITY 3
|
||||
#define REMOTE_LOG_DIR "/littlefs/remote_logs"
|
||||
#define REMOTE_LOG_FILE_PREFIX "log_"
|
||||
#define REMOTE_LOG_FILE_SUFFIX ".jsonl"
|
||||
#define REMOTE_LOG_PATH_MAX 96
|
||||
#define REMOTE_LOG_RESPONSE_MAX 256
|
||||
#define REMOTE_LOG_UPLOAD_BODY_MAX (CONFIG_TQ_REMOTE_LOG_FILE_MAX_BYTES + 4096)
|
||||
#define REMOTE_LOG_BACKOFF_MAX_MS 300000
|
||||
#define REMOTE_LOG_POLL_MS 200
|
||||
|
||||
#if CONFIG_TQ_REMOTE_LOG_ENABLE
|
||||
|
||||
typedef struct {
|
||||
int64_t ts_ms;
|
||||
char level;
|
||||
bool truncated;
|
||||
char text[CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES];
|
||||
} remote_log_entry_t;
|
||||
|
||||
static remote_log_entry_t s_queue[CONFIG_TQ_REMOTE_LOG_QUEUE_LEN];
|
||||
static size_t s_queue_read;
|
||||
static size_t s_queue_count;
|
||||
static uint32_t s_dropped_count;
|
||||
static uint32_t s_file_sequence;
|
||||
static TaskHandle_t s_remote_log_task;
|
||||
static volatile bool s_stop_requested;
|
||||
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static vprintf_like_t s_original_vprintf;
|
||||
static bool s_hook_installed;
|
||||
|
||||
static bool remote_log_path_active(void)
|
||||
{
|
||||
return xTaskGetCurrentTaskHandle() == s_remote_log_task;
|
||||
}
|
||||
|
||||
static char log_level_from_line(const char *line)
|
||||
{
|
||||
if (line == NULL || line[0] == '\0') {
|
||||
return 'I';
|
||||
}
|
||||
if ((line[0] == 'E' || line[0] == 'W' || line[0] == 'I' ||
|
||||
line[0] == 'D' || line[0] == 'V') &&
|
||||
line[1] == ' ') {
|
||||
return line[0];
|
||||
}
|
||||
return 'I';
|
||||
}
|
||||
|
||||
static int log_level_rank(char level)
|
||||
{
|
||||
switch (level) {
|
||||
case 'E':
|
||||
return 1;
|
||||
case 'W':
|
||||
return 2;
|
||||
case 'I':
|
||||
return 3;
|
||||
case 'D':
|
||||
case 'V':
|
||||
return 4;
|
||||
default:
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
static bool should_capture_level(char level)
|
||||
{
|
||||
int min_level = CONFIG_TQ_REMOTE_LOG_MIN_LEVEL;
|
||||
if (min_level <= 0) {
|
||||
return false;
|
||||
}
|
||||
return log_level_rank(level) <= min_level;
|
||||
}
|
||||
|
||||
static void enqueue_line(const char *line, bool truncated)
|
||||
{
|
||||
if (line == NULL || line[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
char level = log_level_from_line(line);
|
||||
if (!should_capture_level(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
portENTER_CRITICAL(&s_lock);
|
||||
size_t write_index = (s_queue_read + s_queue_count) % CONFIG_TQ_REMOTE_LOG_QUEUE_LEN;
|
||||
if (s_queue_count == CONFIG_TQ_REMOTE_LOG_QUEUE_LEN) {
|
||||
s_queue_read = (s_queue_read + 1U) % CONFIG_TQ_REMOTE_LOG_QUEUE_LEN;
|
||||
s_queue_count--;
|
||||
s_dropped_count++;
|
||||
}
|
||||
|
||||
remote_log_entry_t *entry = &s_queue[write_index];
|
||||
entry->ts_ms = esp_timer_get_time() / 1000;
|
||||
entry->level = level;
|
||||
entry->truncated = truncated;
|
||||
strlcpy(entry->text, line, sizeof(entry->text));
|
||||
s_queue_count++;
|
||||
portEXIT_CRITICAL(&s_lock);
|
||||
}
|
||||
|
||||
static int remote_log_vprintf(const char *fmt, va_list args)
|
||||
{
|
||||
int ret = 0;
|
||||
|
||||
#if CONFIG_TQ_REMOTE_LOG_PRESERVE_LOCAL_OUTPUT
|
||||
if (s_original_vprintf != NULL) {
|
||||
va_list local_args;
|
||||
va_copy(local_args, args);
|
||||
ret = s_original_vprintf(fmt, local_args);
|
||||
va_end(local_args);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (remote_log_path_active()) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
char line[CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES];
|
||||
va_list copy_args;
|
||||
va_copy(copy_args, args);
|
||||
int written = vsnprintf(line, sizeof(line), fmt, copy_args);
|
||||
va_end(copy_args);
|
||||
if (written < 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool truncated = written >= (int)sizeof(line);
|
||||
size_t len = strlen(line);
|
||||
while (len > 0U && (line[len - 1U] == '\n' || line[len - 1U] == '\r')) {
|
||||
line[--len] = '\0';
|
||||
}
|
||||
enqueue_line(line, truncated);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool dequeue_entry(remote_log_entry_t *out_entry)
|
||||
{
|
||||
if (out_entry == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
portENTER_CRITICAL(&s_lock);
|
||||
if (s_queue_count > 0U) {
|
||||
*out_entry = s_queue[s_queue_read];
|
||||
s_queue_read = (s_queue_read + 1U) % CONFIG_TQ_REMOTE_LOG_QUEUE_LEN;
|
||||
s_queue_count--;
|
||||
ok = true;
|
||||
}
|
||||
portEXIT_CRITICAL(&s_lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static uint32_t snapshot_and_clear_dropped_count(void)
|
||||
{
|
||||
uint32_t dropped = 0;
|
||||
portENTER_CRITICAL(&s_lock);
|
||||
dropped = s_dropped_count;
|
||||
s_dropped_count = 0;
|
||||
portEXIT_CRITICAL(&s_lock);
|
||||
return dropped;
|
||||
}
|
||||
|
||||
static void restore_dropped_count(uint32_t dropped)
|
||||
{
|
||||
if (dropped == 0U) {
|
||||
return;
|
||||
}
|
||||
portENTER_CRITICAL(&s_lock);
|
||||
s_dropped_count += dropped;
|
||||
portEXIT_CRITICAL(&s_lock);
|
||||
}
|
||||
|
||||
static void sanitize_value(char *text, const char *key)
|
||||
{
|
||||
if (text == NULL || key == NULL || key[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t key_len = strlen(key);
|
||||
char *cursor = text;
|
||||
while ((cursor = strstr(cursor, key)) != NULL) {
|
||||
char *value = cursor + key_len;
|
||||
while (value[0] == ' ' || value[0] == ':' || value[0] == '=') {
|
||||
value++;
|
||||
}
|
||||
if (value[0] == '\0') {
|
||||
break;
|
||||
}
|
||||
char *end = value;
|
||||
while (end[0] != '\0' && end[0] != ' ' && end[0] != ',' &&
|
||||
end[0] != '&' && end[0] != '"' && end[0] != '\'') {
|
||||
end++;
|
||||
}
|
||||
const char *replacement = "***REDACTED***";
|
||||
size_t repl_len = strlen(replacement);
|
||||
size_t tail_len = strlen(end);
|
||||
if (repl_len <= (size_t)(end - value)) {
|
||||
memcpy(value, replacement, repl_len);
|
||||
memmove(value + repl_len, end, tail_len + 1U);
|
||||
cursor = value + repl_len;
|
||||
} else {
|
||||
cursor = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void sanitize_log_line(char *text)
|
||||
{
|
||||
sanitize_value(text, "password");
|
||||
sanitize_value(text, "Password");
|
||||
sanitize_value(text, "pwd");
|
||||
sanitize_value(text, "token");
|
||||
sanitize_value(text, "Token");
|
||||
sanitize_value(text, "Bearer");
|
||||
sanitize_value(text, "secret");
|
||||
sanitize_value(text, "api_key");
|
||||
}
|
||||
|
||||
static void json_append_escaped(char *out, size_t out_len, size_t *offset, const char *text)
|
||||
{
|
||||
if (out == NULL || out_len == 0U || offset == NULL || *offset >= out_len) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const char *p = text != NULL ? text : ""; p[0] != '\0' && *offset < out_len - 1U; ++p) {
|
||||
unsigned char ch = (unsigned char)p[0];
|
||||
const char *escape = NULL;
|
||||
switch (ch) {
|
||||
case '\\':
|
||||
escape = "\\\\";
|
||||
break;
|
||||
case '"':
|
||||
escape = "\\\"";
|
||||
break;
|
||||
case '\b':
|
||||
escape = "\\b";
|
||||
break;
|
||||
case '\f':
|
||||
escape = "\\f";
|
||||
break;
|
||||
case '\n':
|
||||
escape = "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
escape = "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
escape = "\\t";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (escape != NULL) {
|
||||
int written = snprintf(out + *offset, out_len - *offset, "%s", escape);
|
||||
if (written < 0) {
|
||||
return;
|
||||
}
|
||||
*offset += (size_t)written;
|
||||
} else if (ch < 0x20U) {
|
||||
int written = snprintf(out + *offset, out_len - *offset, "\\u%04x", ch);
|
||||
if (written < 0) {
|
||||
return;
|
||||
}
|
||||
*offset += (size_t)written;
|
||||
} else {
|
||||
out[(*offset)++] = (char)ch;
|
||||
out[*offset] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool make_log_dir(void)
|
||||
{
|
||||
struct stat st = {0};
|
||||
if (stat(REMOTE_LOG_DIR, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
return true;
|
||||
}
|
||||
return mkdir(REMOTE_LOG_DIR, 0755) == 0 || errno == EEXIST;
|
||||
}
|
||||
|
||||
static void make_log_path(uint32_t sequence, char *out, size_t out_len)
|
||||
{
|
||||
snprintf(out,
|
||||
out_len,
|
||||
"%s/%s%08lu%s",
|
||||
REMOTE_LOG_DIR,
|
||||
REMOTE_LOG_FILE_PREFIX,
|
||||
(unsigned long)sequence,
|
||||
REMOTE_LOG_FILE_SUFFIX);
|
||||
}
|
||||
|
||||
static bool parse_log_filename(const char *name, uint32_t *out_sequence)
|
||||
{
|
||||
if (name == NULL || out_sequence == NULL) {
|
||||
return false;
|
||||
}
|
||||
size_t prefix_len = strlen(REMOTE_LOG_FILE_PREFIX);
|
||||
size_t suffix_len = strlen(REMOTE_LOG_FILE_SUFFIX);
|
||||
size_t name_len = strlen(name);
|
||||
if (name_len <= prefix_len + suffix_len ||
|
||||
strncmp(name, REMOTE_LOG_FILE_PREFIX, prefix_len) != 0 ||
|
||||
strcmp(name + name_len - suffix_len, REMOTE_LOG_FILE_SUFFIX) != 0) {
|
||||
return false;
|
||||
}
|
||||
char number[12] = {0};
|
||||
size_t number_len = name_len - prefix_len - suffix_len;
|
||||
if (number_len >= sizeof(number)) {
|
||||
return false;
|
||||
}
|
||||
memcpy(number, name + prefix_len, number_len);
|
||||
char *end = NULL;
|
||||
unsigned long value = strtoul(number, &end, 10);
|
||||
if (end == NULL || end[0] != '\0') {
|
||||
return false;
|
||||
}
|
||||
*out_sequence = (uint32_t)value;
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint32_t find_next_file_sequence(void)
|
||||
{
|
||||
DIR *dir = opendir(REMOTE_LOG_DIR);
|
||||
if (dir == NULL) {
|
||||
return 1U;
|
||||
}
|
||||
|
||||
uint32_t max_sequence = 0U;
|
||||
struct dirent *entry = NULL;
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
uint32_t sequence = 0U;
|
||||
if (parse_log_filename(entry->d_name, &sequence) && sequence > max_sequence) {
|
||||
max_sequence = sequence;
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
return max_sequence + 1U;
|
||||
}
|
||||
|
||||
static size_t collect_files(uint32_t *out_sequences, size_t capacity)
|
||||
{
|
||||
if (out_sequences == NULL || capacity == 0U) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
DIR *dir = opendir(REMOTE_LOG_DIR);
|
||||
if (dir == NULL) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
size_t count = 0U;
|
||||
struct dirent *entry = NULL;
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
uint32_t sequence = 0U;
|
||||
if (parse_log_filename(entry->d_name, &sequence)) {
|
||||
if (count < capacity) {
|
||||
out_sequences[count++] = sequence;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
for (size_t j = i + 1U; j < count; ++j) {
|
||||
if (out_sequences[j] < out_sequences[i]) {
|
||||
uint32_t tmp = out_sequences[i];
|
||||
out_sequences[i] = out_sequences[j];
|
||||
out_sequences[j] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static void trim_old_files(void)
|
||||
{
|
||||
uint32_t sequences[CONFIG_TQ_REMOTE_LOG_MAX_FILES + 8] = {0};
|
||||
size_t count = collect_files(sequences, sizeof(sequences) / sizeof(sequences[0]));
|
||||
while (count > CONFIG_TQ_REMOTE_LOG_MAX_FILES) {
|
||||
char path[REMOTE_LOG_PATH_MAX] = {0};
|
||||
make_log_path(sequences[0], path, sizeof(path));
|
||||
(void)unlink(path);
|
||||
memmove(&sequences[0], &sequences[1], (count - 1U) * sizeof(sequences[0]));
|
||||
count--;
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t append_entry_to_file(const remote_log_entry_t *entry)
|
||||
{
|
||||
if (entry == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (!make_log_dir()) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
char path[REMOTE_LOG_PATH_MAX] = {0};
|
||||
make_log_path(s_file_sequence, path, sizeof(path));
|
||||
struct stat st = {0};
|
||||
if (stat(path, &st) == 0 && st.st_size >= CONFIG_TQ_REMOTE_LOG_FILE_MAX_BYTES) {
|
||||
s_file_sequence++;
|
||||
make_log_path(s_file_sequence, path, sizeof(path));
|
||||
}
|
||||
|
||||
FILE *file = fopen(path, "a");
|
||||
if (file == NULL) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
char text[CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES];
|
||||
strlcpy(text, entry->text, sizeof(text));
|
||||
sanitize_log_line(text);
|
||||
|
||||
char escaped[CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES * 2];
|
||||
escaped[0] = '\0';
|
||||
size_t escaped_len = 0U;
|
||||
json_append_escaped(escaped, sizeof(escaped), &escaped_len, text);
|
||||
|
||||
int written = fprintf(file,
|
||||
"{\"ts_ms\":%lld,\"level\":\"%c\",\"truncated\":%s,\"message\":\"%s\"}\n",
|
||||
(long long)entry->ts_ms,
|
||||
entry->level,
|
||||
entry->truncated ? "true" : "false",
|
||||
escaped);
|
||||
int flush_err = fflush(file);
|
||||
int fsync_err = fsync(fileno(file));
|
||||
int close_err = fclose(file);
|
||||
if (written <= 0 || flush_err != 0 || fsync_err != 0 || close_err != 0) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
trim_old_files();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void drain_memory_queue_to_flash(void)
|
||||
{
|
||||
remote_log_entry_t entry = {0};
|
||||
while (dequeue_entry(&entry)) {
|
||||
if (append_entry_to_file(&entry) != ESP_OK) {
|
||||
portENTER_CRITICAL(&s_lock);
|
||||
s_dropped_count++;
|
||||
portEXIT_CRITICAL(&s_lock);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool realtime_path_busy(void)
|
||||
{
|
||||
#if CONFIG_ENABLE_MULTIMODAL_INTERACTION
|
||||
control_plane_multimodal_runtime_state_t state = {0};
|
||||
if (control_plane_multimodal_get_runtime_state(&state) != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
return state.client_speech_active ||
|
||||
state.remote_responding ||
|
||||
state.request_to_speak_pending ||
|
||||
state.received_downstream_audio;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static esp_err_t build_upload_body(uint32_t sequence,
|
||||
uint32_t dropped_count,
|
||||
char *body,
|
||||
size_t body_len,
|
||||
size_t *out_entry_count)
|
||||
{
|
||||
if (body == NULL || body_len == 0U || out_entry_count == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
*out_entry_count = 0U;
|
||||
|
||||
char device_id[64] = {0};
|
||||
char serial_number[64] = {0};
|
||||
domain_runtime_read_device_identity(device_id, sizeof(device_id), serial_number, sizeof(serial_number));
|
||||
|
||||
size_t offset = 0U;
|
||||
int written = snprintf(body,
|
||||
body_len,
|
||||
"{\"schema\":\"tq.remote_log.v1\",\"device_id\":\"");
|
||||
if (written < 0 || (size_t)written >= body_len) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
offset = (size_t)written;
|
||||
json_append_escaped(body, body_len, &offset, device_id);
|
||||
written = snprintf(body + offset,
|
||||
body_len - offset,
|
||||
"\",\"serial_number\":\"");
|
||||
if (written < 0 || (size_t)written >= body_len - offset) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
offset += (size_t)written;
|
||||
json_append_escaped(body, body_len, &offset, serial_number);
|
||||
written = snprintf(body + offset,
|
||||
body_len - offset,
|
||||
"\",\"batch_id\":\"%lu\",\"sent_at_ms\":%lld,\"dropped_count\":%lu,\"entries\":[",
|
||||
(unsigned long)sequence,
|
||||
(long long)(esp_timer_get_time() / 1000),
|
||||
(unsigned long)dropped_count);
|
||||
if (written < 0 || (size_t)written >= body_len - offset) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
offset += (size_t)written;
|
||||
|
||||
char path[REMOTE_LOG_PATH_MAX] = {0};
|
||||
make_log_path(sequence, path, sizeof(path));
|
||||
FILE *file = fopen(path, "r");
|
||||
if (file == NULL) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
char line[CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES * 3];
|
||||
bool first = true;
|
||||
while (fgets(line, sizeof(line), file) != NULL) {
|
||||
size_t line_len = strlen(line);
|
||||
while (line_len > 0U && (line[line_len - 1U] == '\n' || line[line_len - 1U] == '\r')) {
|
||||
line[--line_len] = '\0';
|
||||
}
|
||||
if (line_len == 0U) {
|
||||
continue;
|
||||
}
|
||||
written = snprintf(body + offset,
|
||||
body_len - offset,
|
||||
"%s%s",
|
||||
first ? "" : ",",
|
||||
line);
|
||||
if (written < 0 || (size_t)written >= body_len - offset) {
|
||||
fclose(file);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
offset += (size_t)written;
|
||||
first = false;
|
||||
(*out_entry_count)++;
|
||||
}
|
||||
fclose(file);
|
||||
|
||||
written = snprintf(body + offset, body_len - offset, "]}");
|
||||
if (written < 0 || (size_t)written >= body_len - offset) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
return (*out_entry_count > 0U || dropped_count > 0U) ? ESP_OK : ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
static bool upload_one_file(uint32_t sequence)
|
||||
{
|
||||
uint32_t dropped = snapshot_and_clear_dropped_count();
|
||||
char *body = (char *)malloc(REMOTE_LOG_UPLOAD_BODY_MAX);
|
||||
char *response = (char *)malloc(REMOTE_LOG_RESPONSE_MAX);
|
||||
if (body == NULL || response == NULL) {
|
||||
free(body);
|
||||
free(response);
|
||||
restore_dropped_count(dropped);
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t entry_count = 0U;
|
||||
esp_err_t err = build_upload_body(sequence,
|
||||
dropped,
|
||||
body,
|
||||
REMOTE_LOG_UPLOAD_BODY_MAX,
|
||||
&entry_count);
|
||||
if (err != ESP_OK) {
|
||||
free(body);
|
||||
free(response);
|
||||
restore_dropped_count(dropped);
|
||||
if (err == ESP_ERR_NOT_FOUND) {
|
||||
char path[REMOTE_LOG_PATH_MAX] = {0};
|
||||
make_log_path(sequence, path, sizeof(path));
|
||||
(void)unlink(path);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int status = -1;
|
||||
size_t response_len = 0U;
|
||||
err = domain_runtime_post_json(CONFIG_TQ_REMOTE_LOG_ENDPOINT,
|
||||
body,
|
||||
NULL,
|
||||
CONFIG_TQ_REMOTE_LOG_AUTH_TOKEN,
|
||||
CONFIG_TQ_REMOTE_LOG_UPLOAD_TIMEOUT_MS,
|
||||
0,
|
||||
0,
|
||||
response,
|
||||
REMOTE_LOG_RESPONSE_MAX,
|
||||
&response_len,
|
||||
&status);
|
||||
|
||||
free(body);
|
||||
free(response);
|
||||
|
||||
if (err == ESP_OK && (status == 200 || status == 202 || status == 204)) {
|
||||
char path[REMOTE_LOG_PATH_MAX] = {0};
|
||||
make_log_path(sequence, path, sizeof(path));
|
||||
(void)unlink(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (err == ESP_OK && status == 400) {
|
||||
char path[REMOTE_LOG_PATH_MAX] = {0};
|
||||
make_log_path(sequence, path, sizeof(path));
|
||||
(void)unlink(path);
|
||||
restore_dropped_count(dropped);
|
||||
return true;
|
||||
}
|
||||
|
||||
restore_dropped_count(dropped);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool upload_pending_files(void)
|
||||
{
|
||||
if (!control_plane_is_sta_connected() || realtime_path_busy()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t sequences[CONFIG_TQ_REMOTE_LOG_MAX_FILES] = {0};
|
||||
size_t count = collect_files(sequences, CONFIG_TQ_REMOTE_LOG_MAX_FILES);
|
||||
if (count == 0U) {
|
||||
return true;
|
||||
}
|
||||
return upload_one_file(sequences[0]);
|
||||
}
|
||||
|
||||
static void install_hook(void)
|
||||
{
|
||||
if (s_hook_installed) {
|
||||
return;
|
||||
}
|
||||
s_original_vprintf = esp_log_set_vprintf(remote_log_vprintf);
|
||||
s_hook_installed = true;
|
||||
}
|
||||
|
||||
static void uninstall_hook(void)
|
||||
{
|
||||
if (!s_hook_installed) {
|
||||
return;
|
||||
}
|
||||
(void)esp_log_set_vprintf(s_original_vprintf);
|
||||
s_original_vprintf = NULL;
|
||||
s_hook_installed = false;
|
||||
}
|
||||
|
||||
static void remote_log_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
esp_err_t mount_err = domain_runtime_mount_littlefs();
|
||||
if (mount_err != ESP_OK) {
|
||||
s_remote_log_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
(void)make_log_dir();
|
||||
s_file_sequence = find_next_file_sequence();
|
||||
|
||||
install_hook();
|
||||
|
||||
TickType_t last_flush = xTaskGetTickCount();
|
||||
uint32_t backoff_ms = 0U;
|
||||
TickType_t next_upload_tick = xTaskGetTickCount();
|
||||
|
||||
while (!s_stop_requested) {
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
if ((now - last_flush) >= pdMS_TO_TICKS(CONFIG_TQ_REMOTE_LOG_FLUSH_INTERVAL_MS)) {
|
||||
drain_memory_queue_to_flash();
|
||||
last_flush = now;
|
||||
}
|
||||
|
||||
if (backoff_ms == 0U || now >= next_upload_tick) {
|
||||
drain_memory_queue_to_flash();
|
||||
bool ok = upload_pending_files();
|
||||
if (ok) {
|
||||
backoff_ms = 0U;
|
||||
} else if (control_plane_is_sta_connected()) {
|
||||
backoff_ms = backoff_ms == 0U ? 5000U : backoff_ms * 2U;
|
||||
if (backoff_ms > REMOTE_LOG_BACKOFF_MAX_MS) {
|
||||
backoff_ms = REMOTE_LOG_BACKOFF_MAX_MS;
|
||||
}
|
||||
next_upload_tick = xTaskGetTickCount() + pdMS_TO_TICKS(backoff_ms);
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(REMOTE_LOG_POLL_MS));
|
||||
}
|
||||
|
||||
drain_memory_queue_to_flash();
|
||||
uninstall_hook();
|
||||
s_remote_log_task = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
esp_err_t control_plane_remote_log_start(void)
|
||||
{
|
||||
if (CONFIG_TQ_REMOTE_LOG_ENDPOINT[0] == '\0') {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (s_remote_log_task != NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
s_stop_requested = false;
|
||||
BaseType_t created = xTaskCreatePinnedToCore(remote_log_task,
|
||||
"cp_remote_log",
|
||||
REMOTE_LOG_TASK_STACK_SIZE,
|
||||
NULL,
|
||||
REMOTE_LOG_TASK_PRIORITY,
|
||||
&s_remote_log_task,
|
||||
CONFIG_TQ_NETWORK_TASK_CORE < 0 ? tskNO_AFFINITY :
|
||||
(BaseType_t)CONFIG_TQ_NETWORK_TASK_CORE);
|
||||
return created == pdPASS ? ESP_OK : ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
void control_plane_remote_log_stop(int timeout_ms)
|
||||
{
|
||||
if (s_remote_log_task == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
s_stop_requested = true;
|
||||
TickType_t start = xTaskGetTickCount();
|
||||
TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms > 0 ? timeout_ms : 1);
|
||||
while (s_remote_log_task != NULL &&
|
||||
(xTaskGetTickCount() - start) < timeout_ticks) {
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
}
|
||||
if (s_remote_log_task != NULL) {
|
||||
uninstall_hook();
|
||||
vTaskDelete(s_remote_log_task);
|
||||
s_remote_log_task = NULL;
|
||||
}
|
||||
}
|
||||
#else
|
||||
esp_err_t control_plane_remote_log_start(void)
|
||||
{
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
void control_plane_remote_log_stop(int timeout_ms)
|
||||
{
|
||||
(void)timeout_ms;
|
||||
}
|
||||
#endif
|
||||
@@ -24,6 +24,7 @@
|
||||
#define CONTROL_PLANE_LCD_RENDER_LOOP_TASK_STACK_SIZE (1024 * 12)
|
||||
#define CONTROL_PLANE_WIFI_STATE_TASK_STACK_SIZE (1024 * 12)
|
||||
#define CONTROL_PLANE_TASK_MONITOR_TASK_STACK_SIZE (1024 * 6)
|
||||
#define CONTROL_PLANE_REMOTE_LOG_STOP_WAIT_MS 1500
|
||||
#define CONTROL_PLANE_MODEL_CMD_QUEUE_LEN 4
|
||||
#define CONTROL_PLANE_MODEL_CMD_RECV_TIMEOUT_MS 200
|
||||
#define CONTROL_PLANE_TASK_STOP_WAIT_MS 1500
|
||||
@@ -1812,6 +1813,11 @@ esp_err_t control_plane_start_runtime_workers(void)
|
||||
return key_input_err;
|
||||
}
|
||||
|
||||
esp_err_t remote_log_err = control_plane_remote_log_start();
|
||||
if (remote_log_err != ESP_OK && remote_log_err != ESP_ERR_NOT_SUPPORTED) {
|
||||
ESP_LOGW(TAG, "Remote log start failed: %s", esp_err_to_name(remote_log_err));
|
||||
}
|
||||
|
||||
#if CONFIG_FORTUNE_ENABLE
|
||||
if (control_plane_get_task_handle(&s_daily_fortune_task) == NULL) {
|
||||
control_plane_request_fortune_refresh();
|
||||
@@ -1856,6 +1862,7 @@ void control_plane_stop_event_observer_runtime(void)
|
||||
control_plane_set_runtime_stop_requested(true);
|
||||
control_plane_stop_key_input_task();
|
||||
control_plane_stop_system_event_task();
|
||||
control_plane_remote_log_stop(CONTROL_PLANE_REMOTE_LOG_STOP_WAIT_MS);
|
||||
control_plane_set_runtime_stop_requested(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -187,6 +187,7 @@ esp_err_t domain_runtime_slot_feature_switch_init(void);
|
||||
bool domain_runtime_slot_feature_switch_is_active(void);
|
||||
|
||||
esp_err_t domain_runtime_prepare_storage(void);
|
||||
esp_err_t domain_runtime_mount_littlefs(void);
|
||||
void domain_runtime_read_device_identity(char *device_id,
|
||||
size_t device_id_len,
|
||||
char *serial_number,
|
||||
|
||||
@@ -753,6 +753,11 @@ esp_err_t domain_runtime_prepare_storage(void)
|
||||
return platform_prepare_storage();
|
||||
}
|
||||
|
||||
esp_err_t domain_runtime_mount_littlefs(void)
|
||||
{
|
||||
return platform_mount_littlefs();
|
||||
}
|
||||
|
||||
void domain_runtime_read_device_identity(char *device_id,
|
||||
size_t device_id_len,
|
||||
char *serial_number,
|
||||
|
||||
@@ -132,6 +132,7 @@ typedef struct {
|
||||
|
||||
esp_err_t platform_init(void);
|
||||
esp_err_t platform_prepare_storage(void);
|
||||
esp_err_t platform_mount_littlefs(void);
|
||||
void platform_read_device_identity(char *device_id,
|
||||
size_t device_id_len,
|
||||
char *serial_number,
|
||||
|
||||
@@ -111,7 +111,7 @@ void platform_power_hold_init(void)
|
||||
power_manager_set_keep_awake(true);
|
||||
}
|
||||
|
||||
static esp_err_t voice_prompt_mount_littlefs(void)
|
||||
esp_err_t platform_mount_littlefs(void)
|
||||
{
|
||||
if (s_voice_prompt_littlefs_mount_attempted) {
|
||||
return s_voice_prompt_littlefs_mount_result;
|
||||
@@ -160,7 +160,7 @@ esp_err_t voice_prompt_play_file(const char *path)
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
esp_err_t mount_err = voice_prompt_mount_littlefs();
|
||||
esp_err_t mount_err = platform_mount_littlefs();
|
||||
if (mount_err != ESP_OK) {
|
||||
return mount_err;
|
||||
}
|
||||
|
||||
@@ -132,6 +132,88 @@ menu "Example Configuration"
|
||||
help
|
||||
Stack size in bytes for the control-plane system event task.
|
||||
|
||||
menu "Remote Logging"
|
||||
|
||||
config TQ_REMOTE_LOG_ENABLE
|
||||
bool "Enable remote log upload"
|
||||
default n
|
||||
help
|
||||
Capture ESP-IDF logs, persist them to the LittleFS-backed
|
||||
queue, and upload batches to the configured HTTPS server.
|
||||
|
||||
config TQ_REMOTE_LOG_ENDPOINT
|
||||
string "Remote log endpoint URL"
|
||||
default "https://gz.api.talkingq.com/log/batch"
|
||||
depends on TQ_REMOTE_LOG_ENABLE
|
||||
|
||||
config TQ_REMOTE_LOG_AUTH_TOKEN
|
||||
string "Remote log bearer token"
|
||||
default ""
|
||||
depends on TQ_REMOTE_LOG_ENABLE
|
||||
help
|
||||
Optional bearer token sent as Authorization: Bearer
|
||||
<token>. Leave empty for unauthenticated test receivers.
|
||||
|
||||
config TQ_REMOTE_LOG_MIN_LEVEL
|
||||
int "Minimum remote log level"
|
||||
default 2
|
||||
range 0 4
|
||||
depends on TQ_REMOTE_LOG_ENABLE
|
||||
help
|
||||
0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG.
|
||||
|
||||
config TQ_REMOTE_LOG_PRESERVE_LOCAL_OUTPUT
|
||||
bool "Preserve existing local log output"
|
||||
default y
|
||||
depends on TQ_REMOTE_LOG_ENABLE
|
||||
help
|
||||
Keep the original ESP-IDF vprintf output while also
|
||||
enqueueing logs for server upload.
|
||||
|
||||
config TQ_REMOTE_LOG_QUEUE_LEN
|
||||
int "Remote log memory queue length"
|
||||
default 128
|
||||
range 16 512
|
||||
depends on TQ_REMOTE_LOG_ENABLE
|
||||
|
||||
config TQ_REMOTE_LOG_ENTRY_MAX_BYTES
|
||||
int "Remote log entry max bytes"
|
||||
default 256
|
||||
range 128 1024
|
||||
depends on TQ_REMOTE_LOG_ENABLE
|
||||
|
||||
config TQ_REMOTE_LOG_BATCH_MAX_ENTRIES
|
||||
int "Remote log batch max entries"
|
||||
default 30
|
||||
range 5 100
|
||||
depends on TQ_REMOTE_LOG_ENABLE
|
||||
|
||||
config TQ_REMOTE_LOG_FLUSH_INTERVAL_MS
|
||||
int "Remote log flush interval"
|
||||
default 5000
|
||||
range 1000 60000
|
||||
depends on TQ_REMOTE_LOG_ENABLE
|
||||
|
||||
config TQ_REMOTE_LOG_UPLOAD_TIMEOUT_MS
|
||||
int "Remote log upload timeout"
|
||||
default 8000
|
||||
range 3000 30000
|
||||
depends on TQ_REMOTE_LOG_ENABLE
|
||||
|
||||
config TQ_REMOTE_LOG_MAX_FILES
|
||||
int "Remote log persisted file count"
|
||||
default 32
|
||||
range 4 128
|
||||
depends on TQ_REMOTE_LOG_ENABLE
|
||||
|
||||
config TQ_REMOTE_LOG_FILE_MAX_BYTES
|
||||
int "Remote log persisted file max bytes"
|
||||
default 16384
|
||||
range 4096 65536
|
||||
depends on TQ_REMOTE_LOG_ENABLE
|
||||
|
||||
endmenu
|
||||
|
||||
config TQ_KEY_INPUT_ENABLE
|
||||
bool "Enable interrupt-driven key input task"
|
||||
default y
|
||||
|
||||
@@ -24,6 +24,18 @@ CONFIG_TQ_TASK_MONITOR_MAX_TASKS=64
|
||||
CONFIG_TQ_SYSTEM_EVENT_ENABLE=y
|
||||
CONFIG_TQ_SYSTEM_EVENT_QUEUE_LEN=32
|
||||
CONFIG_TQ_SYSTEM_EVENT_TASK_STACK=4096
|
||||
CONFIG_TQ_REMOTE_LOG_ENABLE=y
|
||||
CONFIG_TQ_REMOTE_LOG_ENDPOINT="http://47.98.175.183:8090/log/batch"
|
||||
CONFIG_TQ_REMOTE_LOG_AUTH_TOKEN=""
|
||||
CONFIG_TQ_REMOTE_LOG_MIN_LEVEL=2
|
||||
CONFIG_TQ_REMOTE_LOG_PRESERVE_LOCAL_OUTPUT=y
|
||||
CONFIG_TQ_REMOTE_LOG_QUEUE_LEN=128
|
||||
CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES=256
|
||||
CONFIG_TQ_REMOTE_LOG_BATCH_MAX_ENTRIES=30
|
||||
CONFIG_TQ_REMOTE_LOG_FLUSH_INTERVAL_MS=5000
|
||||
CONFIG_TQ_REMOTE_LOG_UPLOAD_TIMEOUT_MS=8000
|
||||
CONFIG_TQ_REMOTE_LOG_MAX_FILES=32
|
||||
CONFIG_TQ_REMOTE_LOG_FILE_MAX_BYTES=16384
|
||||
CONFIG_TQ_KEY_INPUT_ENABLE=y
|
||||
CONFIG_TQ_KEY_INPUT_TASK_STACK=4096
|
||||
CONFIG_TQ_KEY_IRQ_QUEUE_LEN=16
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal HTTP receiver for TQ remote log batches.
|
||||
|
||||
Usage:
|
||||
python tools/remote_log_receiver.py --host 0.0.0.0 --port 8080 --dir ./remote_logs
|
||||
|
||||
Configure the device endpoint as:
|
||||
http://<server-ip>:8080/log/batch
|
||||
|
||||
For production, put this behind HTTPS or a reverse proxy that terminates TLS.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import json
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class LogReceiver(BaseHTTPRequestHandler):
|
||||
server_version = "TQRemoteLogReceiver/1.0"
|
||||
|
||||
def do_POST(self) -> None:
|
||||
if self.path != "/log/batch":
|
||||
self._send_json(404, {"code": 404, "message": "not found"})
|
||||
return
|
||||
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
if length <= 0 or length > self.server.max_body_bytes:
|
||||
self._send_json(413, {"code": 413, "message": "payload too large"})
|
||||
return
|
||||
|
||||
raw = self.rfile.read(length)
|
||||
try:
|
||||
payload = json.loads(raw.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
self._send_json(400, {"code": 400, "message": f"invalid json: {exc}"})
|
||||
return
|
||||
|
||||
now = dt.datetime.now(dt.timezone.utc)
|
||||
day_dir = self.server.output_dir / now.strftime("%Y%m%d")
|
||||
day_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
device_id = str(payload.get("device_id") or "unknown").replace("/", "_")
|
||||
batch_id = str(payload.get("batch_id") or now.strftime("%H%M%S%f")).replace("/", "_")
|
||||
json_path = day_dir / f"{device_id}_{batch_id}.json"
|
||||
jsonl_path = day_dir / f"{device_id}.jsonl"
|
||||
|
||||
with json_path.open("w", encoding="utf-8") as file:
|
||||
json.dump(payload, file, ensure_ascii=False, indent=2)
|
||||
file.write("\n")
|
||||
|
||||
entries = payload.get("entries") or []
|
||||
with jsonl_path.open("a", encoding="utf-8") as file:
|
||||
for entry in entries:
|
||||
row = {
|
||||
"received_at": now.isoformat(),
|
||||
"device_id": payload.get("device_id"),
|
||||
"serial_number": payload.get("serial_number"),
|
||||
"batch_id": payload.get("batch_id"),
|
||||
**entry,
|
||||
}
|
||||
file.write(json.dumps(row, ensure_ascii=False, separators=(",", ":")))
|
||||
file.write("\n")
|
||||
|
||||
self._send_json(
|
||||
200,
|
||||
{
|
||||
"code": 200,
|
||||
"message": "OK",
|
||||
"data": {
|
||||
"accepted": len(entries),
|
||||
"stored": str(json_path),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def log_message(self, fmt: str, *args: object) -> None:
|
||||
print(f"{self.address_string()} - {fmt % args}")
|
||||
|
||||
def _send_json(self, status: int, payload: dict) -> None:
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
|
||||
class LogServer(ThreadingHTTPServer):
|
||||
def __init__(self, server_address: tuple[str, int], handler_class, output_dir: Path, max_body_bytes: int):
|
||||
super().__init__(server_address, handler_class)
|
||||
self.output_dir = output_dir
|
||||
self.max_body_bytes = max_body_bytes
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Receive TQ device remote log batches.")
|
||||
parser.add_argument("--host", default="0.0.0.0")
|
||||
parser.add_argument("--port", type=int, default=8080)
|
||||
parser.add_argument("--dir", type=Path, default=Path("remote_logs"))
|
||||
parser.add_argument("--max-body-bytes", type=int, default=256 * 1024)
|
||||
args = parser.parse_args()
|
||||
|
||||
args.dir.mkdir(parents=True, exist_ok=True)
|
||||
server = LogServer((args.host, args.port), LogReceiver, args.dir, args.max_body_bytes)
|
||||
print(f"listening on http://{args.host}:{args.port}/log/batch")
|
||||
print(f"writing logs under {args.dir.resolve()}")
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user