commit e28b59403962710cdacac03881493a6b2f62ebd4 Author: Baryoniks Date: Wed Aug 12 18:38:57 2026 +0300 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..df6f3c3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +.venv/ +.env +data/ +.gitignore diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..226b4a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.pyc +.venv/ +.env +data/ \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..b5c9e76 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,12 @@ + +services: + log-collector: + build: ./log-collector + container_name: ctf-log-collector + ports: + - "8081:8081" + environment: + LOG_STORAGE_PATH: /data/logs.jsonl + volumes: + - ./data/logs:/data + restart: unless-stopped \ No newline at end of file diff --git a/log-collector/Dockerfile b/log-collector/Dockerfile new file mode 100644 index 0000000..7fd3af7 --- /dev/null +++ b/log-collector/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +ENV LOG_STORAGE_PATH=/data/logs.jsonl + +EXPOSE 8080 + +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8081"] \ No newline at end of file diff --git a/log-collector/app.py b/log-collector/app.py new file mode 100644 index 0000000..326b44d --- /dev/null +++ b/log-collector/app.py @@ -0,0 +1,97 @@ +import json +import os +import threading +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, List, Optional + +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, Field, field_validator + + +class LogEntry(BaseModel): + ts: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + level: str = "INFO" + service: str + event: str + message: Optional[str] = None + trace_id: Optional[str] = None + team_id: Optional[int] = None + user_id: Optional[int] = None + match_id: Optional[int] = None + round_id: Optional[int] = None + service_id: Optional[str] = None + status: Optional[str] = None + extra: Dict[str, Any] = Field(default_factory=dict) + + @field_validator("level") + @classmethod + def validate_level(cls, value: str) -> str: + value = value.upper() + allowed = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} + + if value not in allowed: + raise ValueError( + "level must be DEBUG, INFO, WARNING, ERROR or CRITICAL" + ) + + return value + + +class JsonlStorage: + def __init__(self, path: str): + self.path = Path(path) + self.path.parent.mkdir(parents=True, exist_ok=True) + self._lock = threading.Lock() + + def write(self, record: Dict[str, Any]) -> None: + line = json.dumps( + record, + ensure_ascii=False, + default=str, + ) + + with self._lock: + with self.path.open("a", encoding="utf-8") as f: + f.write(line + "\n") + + +storage = JsonlStorage( + os.getenv("LOG_STORAGE_PATH", "./data/logs.jsonl") +) + +app = FastAPI( + title="CTF Log Collector", + version="0.1.0", +) + + +@app.get("/health") +def health() -> Dict[str, str]: + return {"status": "ok"} + + +@app.post("/logs", status_code=202) +def collect_log(entry: LogEntry) -> Dict[str, str]: + payload = entry.model_dump(mode="json") + storage.write(payload) + + return {"status": "accepted"} + + +@app.post("/logs/batch", status_code=202) +def collect_logs_batch(entries: List[LogEntry]) -> Dict[str, Any]: + if len(entries) > 1000: + raise HTTPException( + status_code=400, + detail="Too many log entries in one batch. Max is 1000.", + ) + + for entry in entries: + payload = entry.model_dump(mode="json") + storage.write(payload) + + return { + "status": "accepted", + "count": len(entries), + } \ No newline at end of file diff --git a/log-collector/requirements.txt b/log-collector/requirements.txt new file mode 100644 index 0000000..50ac7c0 --- /dev/null +++ b/log-collector/requirements.txt @@ -0,0 +1,3 @@ +fastapi +uvicorn[standard] +pydantic