#!/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://: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()