Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions diagnostic/build-abef3d5c.json
Original file line number Diff line number Diff line change
@@ -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 <outdir> --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."
}
Binary file added diagnostic/build-abef3d5c.logd
Binary file not shown.
14 changes: 14 additions & 0 deletions docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
14 changes: 11 additions & 3 deletions frailbox/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
20 changes: 14 additions & 6 deletions frailbox/src/logger.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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';
Expand Down
137 changes: 137 additions & 0 deletions frailbox/tests/logger_newline_boundary_fixture.c
Original file line number Diff line number Diff line change
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

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;
}