116 lines
4.1 KiB
Python
116 lines
4.1 KiB
Python
#!/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()
|