diff --git a/diagnostic/build-abef3d5c.json b/diagnostic/build-abef3d5c.json new file mode 100644 index 00000000..a64fcb54 --- /dev/null +++ b/diagnostic/build-abef3d5c.json @@ -0,0 +1,39 @@ +{ + "generated_at": "2026-06-20T21:49:09.430238+00:00", + "commit": "abef3d5c", + "change_commit": "abef3d5c658bd27a4b9d548cc210782a439e232e", + "base_commit": "d5241a4f6e76cb0bda32639d1f254aa06f967cf7", + "diagnostic_logd": "diagnostic/build-abef3d5c.logd", + "diagnostic_logd_error": null, + "chunked": false, + "chunk_size_bytes": null, + "password": "f64da1c11d95e4375b49", + "decrypt_command": "encryptly unpack diagnostic/build-abef3d5c.logd --password f64da1c11d95e4375b49", + "total_modules": 3, + "passed": 3, + "failed": 0, + "modules": [ + { + "name": "logger-newline-fixtures", + "status": "PASS", + "elapsed_seconds": 0, + "artifact": "frailbox/build/logger_newline_boundary_fixture", + "output": "make -C frailbox clean logger-newline-fixtures" + }, + { + "name": "frailbox-build", + "status": "PASS", + "elapsed_seconds": 0, + "artifact": "frailbox/frailbox", + "output": "make -C frailbox all" + }, + { + "name": "build-py-frailbox-attempt", + "status": "PASS", + "elapsed_seconds": 0, + "artifact": null, + "output": "python3 build.py -m frailbox compiled frailbox successfully; non-git diagnostic commit step is documented in encrypted build.log" + } + ], + "pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-abef3d5c.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging." +} diff --git a/diagnostic/build-abef3d5c.logd b/diagnostic/build-abef3d5c.logd new file mode 100644 index 00000000..e451d120 Binary files /dev/null and b/diagnostic/build-abef3d5c.logd differ diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 58642e7b..f573af85 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -280,6 +280,20 @@ Audit logs are retained for 365 days and include: ### Common Issues +**Frailbox logger newline boundaries** + +Use the lightweight fixture target before changing `frailbox/src/logger.c` line +formatting: + +```sh +make -C frailbox logger-newline-fixtures +``` + +The fixture documents the legacy boundary contract: messages without a caller +newline receive one logger record boundary, caller-supplied trailing newlines +are preserved, and truncated records still end with a newline so the next +record starts on its own line. The fixture uses synthetic marker values only. + **Service won't start** 1. Check logs: `kubectl logs -n tent-production deployment/backend-api` 2. Check config: `kubectl exec -n tent-production deploy/backend-api -- cat /app/config.yaml` diff --git a/frailbox/Makefile b/frailbox/Makefile index d4383d85..f5fe4414 100644 --- a/frailbox/Makefile +++ b/frailbox/Makefile @@ -12,10 +12,11 @@ BUILDDIR = build SRCS = $(wildcard $(SRCDIR)/*.c) main.c OBJS = $(patsubst %.c, $(BUILDDIR)/%.o, $(SRCS)) DEPS = $(OBJS:.o=.d) +LOGGER_NEWLINE_FIXTURE = $(BUILDDIR)/logger_newline_boundary_fixture TARGET = frailbox -.PHONY: all clean +.PHONY: all clean logger-newline-fixtures all: $(TARGET) @@ -34,10 +35,17 @@ clean: distclean: clean rm -rf *.o *.d -test: $(TARGET) +$(LOGGER_NEWLINE_FIXTURE): tests/logger_newline_boundary_fixture.c $(SRCDIR)/logger.c $(INCDIR)/logger.h + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -DMAX_LOG_LINE=160 -I$(INCDIR) $< $(SRCDIR)/logger.c -o $@ $(LDFLAGS) -pthread + +logger-newline-fixtures: $(LOGGER_NEWLINE_FIXTURE) + ./$(LOGGER_NEWLINE_FIXTURE) + +test: $(TARGET) logger-newline-fixtures ./$(TARGET) --sandbox-type seccomp --memory-limit 64 --verbose valgrind: $(TARGET) valgrind --leak-check=full --show-leak-kinds=all ./$(TARGET) -.PHONY: all clean distclean test valgrind +.PHONY: all clean distclean logger-newline-fixtures test valgrind diff --git a/frailbox/src/logger.c b/frailbox/src/logger.c index f1a7aa1a..84262331 100644 --- a/frailbox/src/logger.c +++ b/frailbox/src/logger.c @@ -312,8 +312,12 @@ static void ring_buffer_push(const char *message) { pthread_mutex_lock(&g_ring_buffer.ring_mutex); - strncpy(g_ring_buffer.entries[g_ring_buffer.head], message, MAX_LOG_LINE - 1); - g_ring_buffer.entries[g_ring_buffer.head][MAX_LOG_LINE - 1] = '\0'; + size_t message_len = strlen(message); + if (message_len >= MAX_LOG_LINE) { + message_len = MAX_LOG_LINE - 1; + } + memcpy(g_ring_buffer.entries[g_ring_buffer.head], message, message_len); + g_ring_buffer.entries[g_ring_buffer.head][message_len] = '\0'; g_ring_buffer.head = (g_ring_buffer.head + 1) % RING_BUFFER_SIZE; if (g_ring_buffer.count < RING_BUFFER_SIZE) { @@ -508,17 +512,21 @@ void log_message(int level, const char *file, int line, const char *fmt, ...) /* Check for truncation */ int total_len = offset + msg_len; if (total_len >= MAX_LOG_LINE) { - /* Message was truncated. Add truncation indicator. */ + /* Message was truncated. Add truncation indicator and keep the + * record newline-terminated so the next log entry starts cleanly. */ const char trunc_msg[] = "... [TRUNCATED]"; size_t trunc_len = sizeof(trunc_msg) - 1; - size_t copy_len = (size_t)(MAX_LOG_LINE - 1 - trunc_len); + size_t newline_pos = MAX_LOG_LINE - 2; + size_t copy_len = newline_pos - trunc_len; if (copy_len > (size_t)offset) { /* Copy truncation indicator after the partial message */ memcpy(buffer + copy_len, trunc_msg, trunc_len); - buffer[MAX_LOG_LINE - 1] = '\0'; + buffer[newline_pos] = '\n'; + buffer[newline_pos + 1] = '\0'; } else { /* Very short buffer - just truncate */ - buffer[MAX_LOG_LINE - 1] = '\0'; + buffer[newline_pos] = '\n'; + buffer[newline_pos + 1] = '\0'; } } else { buffer[total_len] = '\n'; diff --git a/frailbox/tests/logger_newline_boundary_fixture.c b/frailbox/tests/logger_newline_boundary_fixture.c new file mode 100644 index 00000000..32f02624 --- /dev/null +++ b/frailbox/tests/logger_newline_boundary_fixture.c @@ -0,0 +1,137 @@ +/* + * Regression fixture for legacy logger line-boundary behavior. + * + * The legacy logger preserves caller-supplied trailing newlines and appends + * one record boundary of its own. Truncated records must still end with a + * newline so the following record does not get glued to the same physical line. + */ + +#define _POSIX_C_SOURCE 200809L + +#include "../include/logger.h" + +#include +#include +#include +#include + +static int failures = 0; + +#define CHECK(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s\n", (msg)); \ + failures++; \ + } \ + } while (0) + +static char *read_file(const char *path) +{ + FILE *fp = fopen(path, "rb"); + if (fp == NULL) { + return NULL; + } + + if (fseek(fp, 0, SEEK_END) != 0) { + fclose(fp); + return NULL; + } + + long len = ftell(fp); + if (len < 0) { + fclose(fp); + return NULL; + } + rewind(fp); + + char *buf = (char *)calloc((size_t)len + 1, 1); + if (buf == NULL) { + fclose(fp); + return NULL; + } + + size_t got = fread(buf, 1, (size_t)len, fp); + fclose(fp); + buf[got] = '\0'; + return buf; +} + +static void write_boundary_cases(void) +{ + char long_payload[512]; + memset(long_payload, 'X', sizeof(long_payload) - 1); + long_payload[sizeof(long_payload) - 1] = '\0'; + + log_message(LOG_LEVEL_INFO, "fixture", 10, + "case=no-newline payload=alpha"); + log_message(LOG_LEVEL_INFO, "fixture", 20, + "case=one-newline payload=beta\n"); + log_message(LOG_LEVEL_INFO, "fixture", 30, + "case=multi-newline payload=gamma\n\n"); + log_message(LOG_LEVEL_INFO, "fixture", 40, + "case=over-limit payload=%s", long_payload); + log_message(LOG_LEVEL_INFO, "fixture", 50, + "case=after-boundary payload=omega"); +} + +static void assert_boundary_cases(const char *log_path) +{ + char *contents = read_file(log_path); + CHECK(contents != NULL, "fixture log should be readable"); + if (contents == NULL) { + return; + } + + CHECK(strstr(contents, "case=no-newline payload=alpha\n") != NULL, + "message without caller newline gets one record boundary"); + CHECK(strstr(contents, "case=one-newline payload=beta\n\n") != NULL, + "single caller newline is preserved before record boundary"); + CHECK(strstr(contents, "case=multi-newline payload=gamma\n\n\n") != NULL, + "multiple caller trailing newlines are preserved before record boundary"); + CHECK(strstr(contents, "... [TRUNCATED]\n [INFO] [newline-boundary]") != NULL, + "truncated message ends with newline before the next record prefix"); + CHECK(strstr(contents, "... [TRUNCATED] [INFO] [newline-boundary]") == NULL, + "truncated message must not join the next record on one line"); + CHECK(strstr(contents, "case=after-boundary payload=omega\n") != NULL, + "message after truncated record is still emitted"); + + free(contents); +} + +int main(void) +{ + char log_path[] = "/tmp/frailbox_logger_newline_boundary_XXXXXX"; + int fd = mkstemp(log_path); + if (fd < 0) { + perror("mkstemp"); + return 1; + } + close(fd); + + setenv("LOG_FILE", log_path, 1); + setenv("LOG_LEVEL", "none", 1); + setenv("LOG_MODULE", "newline-boundary", 1); + setenv("LOG_NO_TIMESTAMPS", "1", 1); + unsetenv("LOG_SOURCE_INFO"); + + if (log_init() != 0) { + fprintf(stderr, "FAIL: log_init failed\n"); + unlink(log_path); + return 1; + } + log_set_level(LOG_LEVEL_INFO); + + write_boundary_cases(); + log_shutdown(); + + assert_boundary_cases(log_path); + unlink(log_path); + + if (failures != 0) { + fprintf(stderr, "logger newline boundary fixture failed: %d failures\n", failures); + return 1; + } + + printf("logger newline boundary fixture passed\n"); + return 0; +}