Skip to content
Open
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
87 changes: 87 additions & 0 deletions diagnostic/build-4c507ba1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"generated_at": "2026-06-22T12:54:20.076510+00:00",
"commit": "4c507ba1",
"diagnostic_logd": "diagnostic/build-4c507ba1.logd",
"diagnostic_logd_error": null,
"message_blocker": null,
"chunked": false,
"chunk_size_bytes": null,
"password": "dbd541d5ccd94db1cb2c",
"decrypt_command": "encryptly unpack diagnostic/build-4c507ba1.logd <outdir> --password dbd541d5ccd94db1cb2c",
"total_modules": 10,
"passed": 2,
"failed": 8,
"modules": [
{
"name": "backend",
"status": "FAIL",
"elapsed_seconds": 0.074,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'cargo'"
},
{
"name": "frontend",
"status": "PASS",
"elapsed_seconds": 45.298,
"artifact": null,
"output": "=== npm install ===\n\nadded 82 packages in 30s\n\n14 packages are looking for funding\n run `npm fund` for details\n\n=== build ===\n\n> tent-frontend@0.0.0 build\n> tsc -b && vite build\n\nvite v6.4.3 building for production...\ntransforming...\n\u2713 100 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.63 kB \u2502 gzip: 0.35 kB\ndist/assets/state-BkjSKDbY.js 8.91 kB \u2502 gzip: 3.54 kB \u2502 map: 57.15 kB\ndist/assets/vendor-CREcWLHI.js 48.93 kB \u2502 gzip: 17.25 kB \u2502 map: 481.27 kB\ndist/assets/index-CyxcoTyU.js 231.32 kB \u2502 gzip: 72.16 kB \u2502 map: 1,045.57 kB\n\u2713 built in 3.92s\n"
},
{
"name": "market",
"status": "FAIL",
"elapsed_seconds": 0.112,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'go'"
},
{
"name": "frailbox",
"status": "PASS",
"elapsed_seconds": 1.947,
"artifact": null,
"output": "=== build ===\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/arena.c -o build/src/arena.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/logger.c -o build/src/logger.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/sandbox.c -o build/src/sandbox.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c main.c -o build/main.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude build/src/arena.o build/src/logger.o build/src/sandbox.o build/main.o -o frailbox -pie -z relro -z now\nsrc/arena.c: In function \u2018arena_contains\u2019:\nsrc/arena.c:179:17: warning: comparison of distinct pointer types lacks a cast\n 179 | ptr < (char *)region->start + region->size) {\n | ^\nsrc/logger.c: In function \u2018log_message\u2019:\nsrc/logger.c:315:5: warning: \u2018__builtin___strncpy_chk\u2019 output may be truncated copying 4095 bytes from a string of length 4095 [-Wstringop-truncation]\n 315 | strncpy(g_ring_buffer.entries[g_ring_buffer.head], message, MAX_LOG_LINE - 1);\n | ^\n"
},
{
"name": "engine",
"status": "FAIL",
"elapsed_seconds": 0.116,
"artifact": null,
"output": "=== build ===\nCMake Error: The current CMakeCache.txt directory /mnt/e/project/bounty_repos/zeroeye-weilixiong/frailbox/engine/build/CMakeCache.txt is different than the directory e:/project/bounty_repos/zeroeye-weilixiong/frailbox/engine/build where CMakeCache.txt was created. This may result in binaries being created in the wrong place. If you are not sure, reedit the CMakeCache.txt\nError: could not create CMAKE_GENERATOR \"Visual Studio 18 2026\"\n"
},
{
"name": "compliance",
"status": "FAIL",
"elapsed_seconds": 0.08,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'javac'"
},
{
"name": "v2-market-stream",
"status": "FAIL",
"elapsed_seconds": 0.081,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'ruby'"
},
{
"name": "nfc-scanner",
"status": "FAIL",
"elapsed_seconds": 0.069,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'luac'"
},
{
"name": "openapi-haskell",
"status": "FAIL",
"elapsed_seconds": 0.077,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'ghc'"
},
{
"name": "openapi-tools",
"status": "FAIL",
"elapsed_seconds": 0.067,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'luac'"
}
],
"pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-4c507ba1.logd."
}
Binary file added diagnostic/build-4c507ba1.logd
Binary file not shown.
115 changes: 115 additions & 0 deletions tests/test_log_aggregator_jsonl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""
Tests for log aggregator JSONL output (bounty #3).

Covers JSONL export correctness: one JSON object per line, valid JSON
on each line, field round-tripping, max_entries cap, empty export, and
CLI --format jsonl integration.
"""

import sys
import json
import os
import tempfile
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent / "tools"))

from log_aggregator import LogAggregator, parse_args


def _populate(agg, n=5):
for i in range(n):
agg._parse_line(f"2026-06-22 10:0{i}:00 ERROR [api] error number {i}")
return agg


class TestExportJsonl(unittest.TestCase):
def test_creates_valid_jsonl(self):
agg = _populate(LogAggregator(), 3)
with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
path = f.name
try:
agg.export_jsonl(path)
with open(path, "r", encoding="utf-8") as fh:
lines = fh.readlines()
self.assertEqual(len(lines), 3)
for line in lines:
obj = json.loads(line)
self.assertIsInstance(obj, dict)
self.assertIn("level", obj)
self.assertIn("message", obj)
self.assertEqual(obj["level"], "error")
finally:
os.unlink(path)

def test_each_line_is_self_contained(self):
agg = _populate(LogAggregator(), 4)
with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
path = f.name
try:
agg.export_jsonl(path)
with open(path, "r", encoding="utf-8") as fh:
content = fh.read()
# every line must be valid JSON independently
for line in content.strip().split("\n"):
obj = json.loads(line)
self.assertIn("format", obj)
self.assertIn("timestamp", obj)
finally:
os.unlink(path)

def test_max_entries_cap(self):
agg = _populate(LogAggregator(), 10)
with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
path = f.name
try:
agg.export_jsonl(path, max_entries=3)
with open(path, "r", encoding="utf-8") as fh:
lines = fh.readlines()
self.assertEqual(len(lines), 3)
finally:
os.unlink(path)

def test_empty_aggregator(self):
agg = LogAggregator()
with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
path = f.name
try:
agg.export_jsonl(path)
with open(path, "r", encoding="utf-8") as fh:
content = fh.read()
self.assertEqual(content, "")
finally:
os.unlink(path)

def test_field_roundtrip(self):
agg = LogAggregator()
agg._parse_line('{"timestamp":1700000000,"level":"warn","service":"auth","message":"rate limited"}')
with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
path = f.name
try:
agg.export_jsonl(path)
with open(path, "r", encoding="utf-8") as fh:
obj = json.loads(fh.readline())
self.assertEqual(obj["level"], "warn")
self.assertEqual(obj["service"], "auth")
self.assertEqual(obj["message"], "rate limited")
self.assertEqual(obj["format"], "json")
finally:
os.unlink(path)


class TestCliFormatJsonl(unittest.TestCase):
def test_format_accepts_jsonl(self):
old_argv = sys.argv
sys.argv = ["log_aggregator.py", "--format", "jsonl", "-o", os.devnull]
try:
args = parse_args()
self.assertEqual(args.format, "jsonl")
finally:
sys.argv = old_argv


if __name__ == "__main__":
unittest.main()
18 changes: 17 additions & 1 deletion tools/log_aggregator.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,20 @@ def export_json(self, output_path: str):
}, f, indent=2, default=str)
logger.info(f"Report exported to {output_path}")

def export_jsonl(self, output_path: str, max_entries: int = 10000):
"""Export parsed entries as JSON Lines (one JSON object per line).

Each line is a self-contained JSON object representing a single log
entry, making the output suitable for streaming ingestion and
line-by-line processing (e.g. by jq or log shipping agents).
"""
exported = 0
with open(output_path, "w", encoding="utf-8") as f:
for entry in self.entries[:max_entries]:
f.write(json.dumps(entry, default=str) + "\n")
exported += 1
logger.info(f"Exported {exported} entries to {output_path}")

def generate_html_report(self, output_path: str):
summary = self.get_summary()
html = f"""<!DOCTYPE html>
Expand Down Expand Up @@ -409,7 +423,7 @@ def parse_args():
parser.add_argument("--input", "-i", help="Input log file or glob pattern")
parser.add_argument("--dir", help="Directory containing log files")
parser.add_argument("--output", "-o", default="log_report.json", help="Output file path")
parser.add_argument("--format", choices=["json", "csv", "html"], default="json", help="Output format")
parser.add_argument("--format", choices=["json", "csv", "html", "jsonl"], default="json", help="Output format")
parser.add_argument("--search", help="Search for a string in logs")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
return parser.parse_args()
Expand Down Expand Up @@ -456,6 +470,8 @@ def main():
aggregator.export_csv(args.output)
elif args.format == "html":
aggregator.generate_html_report(args.output)
elif args.format == "jsonl":
aggregator.export_jsonl(args.output)
else:
aggregator.export_json(args.output)

Expand Down