Initial commit

This commit is contained in:
Baryoniks
2026-08-12 18:38:57 +03:00
commit e28b594039
7 changed files with 140 additions and 0 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
__pycache__/
*.pyc
.venv/
.env
data/
.gitignore

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
__pycache__/
*.pyc
.venv/
.env
data/

12
docker-compose.yaml Normal file
View File

@@ -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

15
log-collector/Dockerfile Normal file
View File

@@ -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"]

97
log-collector/app.py Normal file
View File

@@ -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),
}

View File

@@ -0,0 +1,3 @@
fastapi
uvicorn[standard]
pydantic