| <!DOCTYPE html> |
| <html lang="th"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>ROGLAG — Production Plan & Stack</title> |
| <style> |
| :root{ |
| --bg:#0b0f17; --panel:#121826; --panel2:#0f1420; --line:#1f2a3d; |
| --txt:#e6edf6; --muted:#90a0b8; --accent:#4da3ff; --accent2:#7c5cff; |
| --ok:#34d399; --warn:#fbbf24; --crit:#f87171; --code:#0a0e16; |
| --chip:#16203250; |
| } |
| *{box-sizing:border-box} |
| html{scroll-behavior:smooth} |
| body{ |
| margin:0; background:radial-gradient(1200px 600px at 80% -10%, #15203a 0%, var(--bg) 55%); |
| color:var(--txt); font-family:-apple-system,"Segoe UI",Roboto,"Noto Sans Thai",sans-serif; |
| line-height:1.65; font-size:15px; |
| } |
| .wrap{max-width:1080px; margin:0 auto; padding:32px 22px 96px} |
| header.hero{ |
| border:1px solid var(--line); border-radius:18px; padding:28px 30px; |
| background:linear-gradient(135deg,#13203a,#0f1420); margin-bottom:14px; |
| } |
| .hero h1{margin:0 0 6px; font-size:30px; letter-spacing:.3px} |
| .hero p{margin:0; color:var(--muted)} |
| .tagline{display:flex; gap:8px; flex-wrap:wrap; margin-top:16px} |
| .chip{font-size:12px; padding:5px 11px; border:1px solid var(--line); |
| border-radius:999px; background:var(--chip); color:var(--accent)} |
| nav.toc{ |
| position:sticky; top:0; z-index:5; backdrop-filter:blur(8px); |
| background:#0b0f17cc; border:1px solid var(--line); border-radius:14px; |
| padding:12px 16px; margin:18px 0 26px; display:flex; gap:14px; flex-wrap:wrap; |
| } |
| nav.toc a{color:var(--muted); text-decoration:none; font-size:13px} |
| nav.toc a:hover{color:var(--accent)} |
| section{ |
| border:1px solid var(--line); border-radius:16px; padding:24px 26px; |
| background:var(--panel); margin-bottom:22px; |
| } |
| h2{font-size:21px; margin:0 0 4px; display:flex; align-items:center; gap:10px} |
| h2 .num{font-size:13px; color:var(--accent2); border:1px solid var(--line); |
| border-radius:8px; padding:2px 8px; background:var(--panel2)} |
| h3{font-size:16px; margin:22px 0 8px; color:var(--accent)} |
| .sub{color:var(--muted); margin:2px 0 18px; font-size:13.5px} |
| p{margin:8px 0} |
| ul,ol{margin:8px 0 8px 4px; padding-left:20px} |
| li{margin:4px 0} |
| code{background:var(--code); border:1px solid var(--line); border-radius:6px; |
| padding:1px 6px; font-size:13px; color:#cfe3ff; font-family:"SF Mono",Menlo,Consolas,monospace} |
| pre{background:var(--code); border:1px solid var(--line); border-radius:12px; |
| padding:16px 18px; overflow:auto; font-size:12.8px; line-height:1.55} |
| pre code{background:none; border:none; padding:0; color:#d6e2f2} |
| table{width:100%; border-collapse:collapse; margin:12px 0; font-size:13.5px} |
| th,td{border:1px solid var(--line); padding:9px 12px; text-align:left; vertical-align:top} |
| th{background:var(--panel2); color:var(--accent); font-weight:600} |
| td b{color:#fff} |
| .grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:14px; margin:14px 0} |
| .card{border:1px solid var(--line); border-radius:12px; padding:16px; background:var(--panel2)} |
| .card h4{margin:0 0 8px; font-size:14.5px; color:var(--txt)} |
| .card p{margin:0; color:var(--muted); font-size:13px} |
| .pill{display:inline-block; font-size:11px; padding:2px 8px; border-radius:999px; margin-left:6px} |
| .p-now{background:#0e2a1e; color:var(--ok); border:1px solid #1c4b36} |
| .p-next{background:#2a230e; color:var(--warn); border:1px solid #4b3d1c} |
| .p-later{background:#241a2e; color:#c4a3ff; border:1px solid #3d2c4b} |
| .flow{display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin:14px 0; font-size:13px} |
| .node{border:1px solid var(--line); background:var(--panel2); border-radius:10px; padding:8px 12px} |
| .arrow{color:var(--accent2); font-weight:700} |
| .callout{border-left:3px solid var(--accent); background:#0e1a2e; border-radius:0 10px 10px 0; |
| padding:12px 16px; margin:14px 0; color:#c8d8ee; font-size:13.5px} |
| .callout.warn{border-left-color:var(--warn); background:#241d0e} |
| .callout.crit{border-left-color:var(--crit); background:#2a1414} |
| footer{color:var(--muted); text-align:center; font-size:12px; margin-top:30px} |
| .legend{display:flex; gap:14px; flex-wrap:wrap; font-size:12px; color:var(--muted); margin-top:8px} |
| </style> |
| </head> |
| <body> |
| <div class="wrap"> |
|
|
| <header class="hero"> |
| <h1>🛡️ ROGLAG — Production Plan & Tech Stack</h1> |
| <p>แผนยกระดับจาก demo (stdlib HTTP + in-memory) ไปสู่ระบบ Security Log RAG + Automation ระดับ production</p> |
| <div class="tagline"> |
| <span class="chip">RAG</span> |
| <span class="chip">Security Log Analytics</span> |
| <span class="chip">n8n Automation</span> |
| <span class="chip">Vector DB</span> |
| <span class="chip">Docker</span> |
| <span class="chip">FastAPI</span> |
| <span class="chip">PostgreSQL</span> |
| </div> |
| <div class="legend"> |
| <span><span class="pill p-now">NOW</span> เฟส 1 (MVP รันได้)</span> |
| <span><span class="pill p-next">NEXT</span> เฟส 2 (production core)</span> |
| <span><span class="pill p-later">LATER</span> เฟส 3 (scale & ops)</span> |
| </div> |
| </header> |
|
|
| <nav class="toc"> |
| <a href="#run">▶ วิธีรัน</a> |
| <a href="#assess">① ประเมินของเดิม</a> |
| <a href="#arch">② Target Architecture</a> |
| <a href="#stack">③ Stack รวบรวม</a> |
| <a href="#pipeline">④ Pipeline จริง</a> |
| <a href="#n8n">⑤ n8n Workflows</a> |
| <a href="#db">⑥ Database</a> |
| <a href="#docker">⑦ Docker</a> |
| <a href="#roadmap">⑧ Roadmap</a> |
| </nav> |
|
|
| |
| <section id="run"> |
| <h2><span class="num">RUN</span> วิธีรันโปรเจกต์ตอนนี้</h2> |
| <p class="sub">ต้องมี Python 3.10+ — local mode ใช้ stdlib ล้วน ไม่ต้องลง dependency</p> |
| <pre><code># Local mode (keyword RAG + local fallback) — ใช้ได้ทันที |
| python3 server.py # เปิด http://127.0.0.1:8765 |
|
|
| # เปิด LLM summary ผ่าน OpenRouter |
| export OPENROUTER_API_KEY="your_key" |
| python3 server.py |
|
|
| # Vector RAG จริง (build index ก่อน) |
| python3 build_vector_index.py --limit 10000 --batch-size 32 |
| python3 server.py |
|
|
| # รันกับชุด 50k |
| LOGRAG_DATA_FILE=Data/files/security_logs_extended_50k.jsonl python3 server.py</code></pre> |
| <div class="callout"> |
| โหมด local ทำงานได้แม้ไม่มี API key — ระบบ fallback ทุกชั้น (vector → keyword → rule-based local answer) |
| </div> |
| </section> |
|
|
| |
| <section id="assess"> |
| <h2><span class="num">①</span> ประเมินของเดิม (ทำไมยังไม่ production-ready)</h2> |
| <p class="sub">เข้าใจข้อจำกัดก่อน เพื่อรู้ว่าต้องเปลี่ยนอะไร</p> |
| <table> |
| <tr><th>ด้าน</th><th>ของเดิม (demo)</th><th>ปัญหาตอนขึ้นจริง</th></tr> |
| <tr><td><b>Web server</b></td><td><code>http.server</code> + ThreadingHTTPServer</td><td>ไม่มี async/connection pool, ไม่เหมาะ traffic จริง, ไม่มี auth</td></tr> |
| <tr><td><b>Data store</b></td><td>โหลด JSONL ทั้งไฟล์เข้า RAM ตอน start</td><td>ไม่ scale, restart ช้า, query ใหม่ไม่ได้, ไม่มี retention</td></tr> |
| <tr><td><b>Index</b></td><td>สร้าง TF-IDF/vector ใน memory</td><td>หายเมื่อ restart, ไม่ incremental, single process</td></tr> |
| <tr><td><b>Logic ซ้ำ</b></td><td><code>server.py</code> กับ <code>api/_lograg.py</code> ซ้ำกันเกือบหมด</td><td>แก้ rule ต้องแก้ 2 ที่ — แหล่ง bug</td></tr> |
| <tr><td><b>Ingestion</b></td><td>ไม่มี — ใช้ไฟล์สังเคราะห์ static</td><td>ไม่มีทางรับ log จริงแบบ real-time</td></tr> |
| <tr><td><b>Automation</b></td><td>หน้าเว็บ poll <code>/api/alerts</code> ทุก 15 วิ</td><td>ไม่มี action จริง (block IP, ticket, notify)</td></tr> |
| <tr><td><b>Secrets/Auth</b></td><td>ไม่มี authentication เลย</td><td>เปิด public = ใครก็เรียก API ได้</td></tr> |
| </table> |
| <div class="callout crit"> |
| <b>สิ่งที่เก็บไว้ได้:</b> CATEGORY_META (rule + MITRE + runbook), logic <code>detect_rule</code>, query expansion ไทย/อังกฤษ, โครงสร้าง API — ทั้งหมดนี้ port ไป FastAPI ได้ตรงๆ |
| </div> |
| </section> |
|
|
| |
| <section id="arch"> |
| <h2><span class="num">②</span> Target Architecture (production)</h2> |
| <p class="sub">แยกเป็นชั้นชัดเจน: ingest → process → store → RAG/API → automation → notify</p> |
| <div class="flow"> |
| <span class="node">Log Sources<br><small>auth/nginx/fw/syslog</small></span> |
| <span class="arrow">→</span> |
| <span class="node">Collector<br><small>Vector / Fluent Bit</small></span> |
| <span class="arrow">→</span> |
| <span class="node">Queue<br><small>Redpanda / Redis Stream</small></span> |
| <span class="arrow">→</span> |
| <span class="node">Processor<br><small>classify + embed</small></span> |
| <span class="arrow">→</span> |
| <span class="node">Stores<br><small>PG + Qdrant + OpenSearch</small></span> |
| </div> |
| <div class="flow"> |
| <span class="node">RAG API<br><small>FastAPI</small></span> |
| <span class="arrow">→</span> |
| <span class="node">n8n<br><small>orchestration</small></span> |
| <span class="arrow">→</span> |
| <span class="node">Enrich<br><small>GeoIP / threat intel</small></span> |
| <span class="arrow">→</span> |
| <span class="node">Action<br><small>ticket / block / notify</small></span> |
| </div> |
| <p>หลักการ: ทุก critical alert ที่ classifier จับได้ → ยิงเข้า n8n → enrich ด้วย threat intel → เรียก RAG API สรุปเป็นภาษาไทย → สร้าง ticket + แจ้งเตือน + (ออปชัน) block IP อัตโนมัติ</p> |
| </section> |
|
|
| |
| <section id="stack"> |
| <h2><span class="num">③</span> Tech Stack รวบรวม</h2> |
| <p class="sub">เลือกแบบ self-hostable ก่อน (ลง Docker ได้หมด) แล้วค่อยสลับไป managed service ทีหลัง</p> |
|
|
| <h3>Core Pipeline</h3> |
| <table> |
| <tr><th>ชั้น</th><th>เลือกใช้</th><th>ทางเลือก</th><th>เฟส</th></tr> |
| <tr><td><b>Log collector</b></td><td>Vector.dev</td><td>Fluent Bit, Filebeat</td><td><span class="pill p-next">NEXT</span></td></tr> |
| <tr><td><b>Message queue</b></td><td>Redpanda (Kafka API)</td><td>Redis Streams, NATS, Kafka</td><td><span class="pill p-next">NEXT</span></td></tr> |
| <tr><td><b>API / RAG service</b></td><td>FastAPI + Uvicorn</td><td>Litestar, Flask</td><td><span class="pill p-now">NOW</span></td></tr> |
| <tr><td><b>Task worker</b></td><td>ARQ / Celery + Redis</td><td>Dramatiq, RQ</td><td><span class="pill p-next">NEXT</span></td></tr> |
| </table> |
|
|
| <h3>Data Stores</h3> |
| <table> |
| <tr><th>ชนิด</th><th>เลือกใช้</th><th>ใช้เก็บอะไร</th></tr> |
| <tr><td><b>Relational</b></td><td>PostgreSQL 16</td><td>alerts, incidents, users, audit, runbook, rule config</td></tr> |
| <tr><td><b>Vector DB</b></td><td>Qdrant <small>(โค้ดเดิมรองรับอยู่แล้ว)</small></td><td>embeddings ของ log message สำหรับ RAG</td></tr> |
| <tr><td><b>Log search</b></td><td>OpenSearch <small>(หรือ ClickHouse)</small></td><td>full-text + time-series log จำนวนมหาศาล</td></tr> |
| <tr><td><b>Cache / queue</b></td><td>Redis</td><td>rate-limit, task queue, dedup alert key</td></tr> |
| </table> |
| <div class="callout"> |
| <b>ทางลัดประหยัด:</b> ใช้ <code>pgvector</code> บน PostgreSQL แทน Qdrant ได้ในเฟสแรก เหลือ DB เดียว — แต่ Qdrant scale ดีกว่าเมื่อ vector เยอะ. ClickHouse เหมาะกว่า OpenSearch ถ้าเน้น analytics/ราคาถูก |
| </div> |
|
|
| <h3>AI / RAG</h3> |
| <table> |
| <tr><th>หน้าที่</th><th>เลือกใช้</th><th>ทางเลือก</th></tr> |
| <tr><td><b>Embeddings</b></td><td>OpenRouter <small>(เดิม)</small></td><td>OpenAI, Ollama (local: nomic-embed-text)</td></tr> |
| <tr><td><b>LLM สรุป</b></td><td>OpenRouter <small>(เดิม)</small></td><td>Ollama (llama3/qwen), vLLM self-host</td></tr> |
| <tr><td><b>RAG framework</b></td><td>เขียนเอง <small>(เบา, คุมได้)</small></td><td>LlamaIndex, Haystack</td></tr> |
| </table> |
|
|
| <h3>Automation, Frontend & Ops</h3> |
| <div class="grid"> |
| <div class="card"><h4>Automation</h4><p>n8n (self-host) — orchestrate alert → enrich → action</p></div> |
| <div class="card"><h4>Frontend</h4><p>คงหน้า HTML เดิมได้ หรือย้ายไป Next.js + Tailwind เมื่อต้อง auth/role</p></div> |
| <div class="card"><h4>Notify</h4><p>Slack / Discord / Email (SMTP) / PagerDuty / LINE Notify</p></div> |
| <div class="card"><h4>Threat Intel</h4><p>AbuseIPDB, VirusTotal, GreyNoise, MaxMind GeoIP</p></div> |
| <div class="card"><h4>Observability</h4><p>Prometheus + Grafana + Loki</p></div> |
| <div class="card"><h4>Auth/Gateway</h4><p>Traefik / Caddy + OAuth2-proxy หรือ Keycloak</p></div> |
| <div class="card"><h4>Secrets</h4><p>Doppler / Infisical / .env + SOPS</p></div> |
| <div class="card"><h4>Container/CI</h4><p>Docker Compose → Kubernetes, GitHub Actions</p></div> |
| </div> |
| </section> |
|
|
| |
| <section id="pipeline"> |
| <h2><span class="num">④</span> Pipeline จริง (data flow ละเอียด)</h2> |
| <p class="sub">เส้นทางของ log ตั้งแต่เข้าระบบจนเกิด action</p> |
| <ol> |
| <li><b>Ingest</b> — Vector/Fluent Bit อ่าน log จาก source (syslog, file, http) → ส่งเข้า Redpanda topic <code>logs.raw</code></li> |
| <li><b>Classify</b> — worker consume <code>logs.raw</code> → รัน <code>detect_rule()</code> (port มาจากของเดิม) → ใส่ category/severity/MITRE → เขียนลง PostgreSQL + OpenSearch</li> |
| <li><b>Embed</b> — worker เดียวกัน (หรือแยก) เรียก embedding model → upsert vector เข้า Qdrant พร้อม payload (row + metadata)</li> |
| <li><b>Detect critical</b> — ถ้า severity = critical → group ตาม <code>alert_key</code> (logic เดิม) → publish เข้า topic <code>alerts.critical</code> + insert ตาราง <code>alerts</code></li> |
| <li><b>Orchestrate</b> — n8n trigger จาก webhook/queue → enrich → RAG → action (รายละเอียดข้อ ⑤)</li> |
| <li><b>Query/RAG</b> — FastAPI <code>/api/ask</code>: retrieve จาก Qdrant (vector) + OpenSearch (keyword) → hybrid rank → ส่ง context ให้ LLM → ตอบภาษาไทย</li> |
| </ol> |
| <pre><code>logs.raw ──> [classify worker] ──> PostgreSQL (structured + alerts) |
| │ └─> OpenSearch (full-text search) |
| └─> [embed worker] ──> Qdrant (vector RAG) |
|
|
| severity=critical ──> alerts.critical (queue) ──> n8n webhook ──> enrich+RAG+action |
|
|
| User/Frontend ──> FastAPI /api/ask ──> hybrid retrieve (Qdrant + OpenSearch) |
| └─> LLM (OpenRouter/Ollama) ──> Thai answer</code></pre> |
| </section> |
|
|
| |
| <section id="n8n"> |
| <h2><span class="num">⑤</span> n8n Workflows</h2> |
| <p class="sub">หัวใจของ automation — แต่ละ workflow คือ DAG ของ node</p> |
|
|
| <h3>WF-1: Critical Alert Responder</h3> |
| <div class="flow"> |
| <span class="node">Webhook<br><small>/critical</small></span><span class="arrow">→</span> |
| <span class="node">GeoIP + AbuseIPDB<br><small>enrich src_ip</small></span><span class="arrow">→</span> |
| <span class="node">HTTP: RAG<br><small>POST /api/ask</small></span><span class="arrow">→</span> |
| <span class="node">Switch<br><small>by category</small></span><span class="arrow">→</span> |
| <span class="node">Create Ticket<br><small>Jira/GitHub</small></span><span class="arrow">→</span> |
| <span class="node">Notify<br><small>Slack/LINE</small></span> |
| </div> |
| <ul> |
| <li><b>Trigger:</b> n8n Webhook node รับ payload จาก <code>alerts.critical</code> (หรือ Schedule poll <code>/api/alerts?since=</code>)</li> |
| <li><b>Enrich:</b> HTTP Request → AbuseIPDB/VirusTotal เช็ค reputation ของ <code>src_ip</code>, MaxMind GeoIP หาประเทศ</li> |
| <li><b>RAG summary:</b> POST <code>/api/ask</code> ด้วย <code>context_rows</code> ของ alert → ได้สรุปไทย + runbook</li> |
| <li><b>Route:</b> Switch ตาม category → <code>priv_esc/data_exfil</code> = แจ้ง on-call ทันที (PagerDuty); อื่นๆ = ticket ปกติ</li> |
| <li><b>Action:</b> สร้าง ticket, ส่ง Slack/LINE, (ออปชัน) เรียก firewall API block IP ที่ reputation แย่</li> |
| </ul> |
|
|
| <h3>WF-2: Daily Threat Digest</h3> |
| <p>Schedule (เช้า 9 โมง) → query <code>/api/stats</code> + top IPs → RAG สรุปภาพรวม 24 ชม. → ส่งรายงานเข้า Slack channel</p> |
|
|
| <h3>WF-3: Auto-block Feedback Loop</h3> |
| <p>เมื่อ analyst กด "confirm threat" บน dashboard → webhook → n8n update firewall rule + บันทึก audit ลง PostgreSQL + (ออปชัน) ใส่ IP เข้า blocklist ที่ feed กลับ collector</p> |
|
|
| <div class="callout warn"> |
| <b>ข้อควรระวัง auto-block:</b> การ block IP อัตโนมัติคือ action ที่ผลกระทบสูง — ควรมี allowlist (IP ภายใน/พาร์ทเนอร์), require approval node สำหรับ critical action, และ TTL ให้ block ชั่วคราวก่อน |
| </div> |
| </section> |
|
|
| |
| <section id="db"> |
| <h2><span class="num">⑥</span> Database Schema (PostgreSQL)</h2> |
| <p class="sub">โครงเริ่มต้น — เก็บ structured log, alert grouping, audit trail</p> |
| <pre><code>-- log ที่ผ่าน classify แล้ว (hot 30-90 วัน, retention policy แยก) |
| CREATE TABLE log_events ( |
| id BIGSERIAL PRIMARY KEY, |
| ts TIMESTAMPTZ NOT NULL, |
| source TEXT NOT NULL, -- auth/nginx/firewall/system |
| category TEXT NOT NULL, -- brute_force, c2_beacon, ... |
| severity TEXT NOT NULL, -- info/medium/high/critical |
| mitre TEXT, |
| risk_score SMALLINT, |
| src_ip INET, |
| dst_ip INET, |
| "user" TEXT, |
| message TEXT NOT NULL, |
| raw JSONB NOT NULL, -- เก็บ field ดิบทั้งหมด |
| embedded BOOLEAN DEFAULT FALSE -- ส่งเข้า Qdrant แล้วหรือยัง |
| ); |
| CREATE INDEX ON log_events (ts DESC); |
| CREATE INDEX ON log_events (category, severity); |
| CREATE INDEX ON log_events (src_ip); |
|
|
| -- alert ที่ group แล้ว (จาก alert_key เดิม) |
| CREATE TABLE alerts ( |
| id TEXT PRIMARY KEY, -- ALT-xxxxxx |
| alert_key TEXT NOT NULL, |
| title TEXT NOT NULL, |
| category TEXT NOT NULL, |
| severity TEXT NOT NULL DEFAULT 'critical', |
| mitre TEXT, |
| status TEXT NOT NULL DEFAULT 'new', -- new/ack/resolved/false_positive |
| event_count INT NOT NULL, |
| first_seen TIMESTAMPTZ NOT NULL, |
| last_seen TIMESTAMPTZ NOT NULL, |
| src_ip INET, dst_ip INET, "user" TEXT, |
| rag_summary TEXT, -- คำตอบจาก LLM |
| enrichment JSONB, -- ผล GeoIP/threat intel |
| assigned_to TEXT, |
| created_at TIMESTAMPTZ DEFAULT now(), |
| updated_at TIMESTAMPTZ DEFAULT now() |
| ); |
|
|
| -- audit ทุก action (โดยเฉพาะ auto-block) |
| CREATE TABLE audit_log ( |
| id BIGSERIAL PRIMARY KEY, |
| ts TIMESTAMPTZ DEFAULT now(), |
| actor TEXT NOT NULL, -- user / n8n / system |
| action TEXT NOT NULL, -- block_ip / create_ticket / ack |
| target TEXT, |
| alert_id TEXT REFERENCES alerts(id), |
| detail JSONB |
| ); |
|
|
| -- rule config (ย้าย CATEGORY_META ออกจาก hardcode มาเป็น data) |
| CREATE TABLE rule_config ( |
| category TEXT PRIMARY KEY, |
| label TEXT, mitre TEXT, risk SMALLINT, runbook TEXT, |
| enabled BOOLEAN DEFAULT TRUE |
| );</code></pre> |
| <div class="callout"> |
| ย้าย <code>CATEGORY_META</code> จาก hardcode ใน Python มาเป็นตาราง <code>rule_config</code> → แก้ runbook/risk ได้โดยไม่ต้อง deploy ใหม่ และเลิกปัญหา logic ซ้ำ 2 ไฟล์ |
| </div> |
| </section> |
|
|
| |
| <section id="docker"> |
| <h2><span class="num">⑦</span> Docker (โครงอ้างอิงสำหรับขึ้น proj จริง)</h2> |
| <p class="sub">ไฟล์อ้างอิง — ปรับ version/secret ตอนทำจริง</p> |
|
|
| <h3>Dockerfile (FastAPI service)</h3> |
| <pre><code>FROM python:3.12-slim |
| WORKDIR /app |
| ENV PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1 |
|
|
| COPY requirements.txt . |
| RUN pip install -r requirements.txt |
|
|
| COPY . . |
| EXPOSE 8000 |
| # ใช้ uvicorn worker จริง (อย่าใช้ http.server เดิม) |
| CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]</code></pre> |
|
|
| <h3>docker-compose.yml (full stack dev)</h3> |
| <pre><code>services: |
| api: |
| build: . |
| ports: ["8000:8000"] |
| env_file: .env |
| depends_on: [postgres, qdrant, redis] |
|
|
| worker: # classify + embed consumer |
| build: . |
| command: python -m app.worker |
| env_file: .env |
| depends_on: [postgres, qdrant, redis, redpanda] |
|
|
| postgres: |
| image: postgres:16-alpine |
| environment: |
| POSTGRES_PASSWORD: ${PG_PASSWORD} |
| POSTGRES_DB: roglag |
| volumes: ["pgdata:/var/lib/postgresql/data"] |
| ports: ["5432:5432"] |
|
|
| qdrant: |
| image: qdrant/qdrant:latest |
| volumes: ["qdrant:/qdrant/storage"] |
| ports: ["6333:6333"] |
|
|
| redis: |
| image: redis:7-alpine |
| ports: ["6379:6379"] |
|
|
| redpanda: # Kafka-compatible, เบากว่า Kafka |
| image: redpandadata/redpanda:latest |
| command: redpanda start --smp 1 --overprovisioned --node-id 0 |
| ports: ["9092:9092"] |
|
|
| n8n: |
| image: n8nio/n8n:latest |
| environment: |
| N8N_HOST: localhost |
| WEBHOOK_URL: http://n8n:5678/ |
| volumes: ["n8n:/home/node/.n8n"] |
| ports: ["5678:5678"] |
|
|
| vector: # log collector |
| image: timberio/vector:latest-alpine |
| volumes: ["./vector.toml:/etc/vector/vector.toml:ro"] |
| depends_on: [redpanda] |
|
|
| volumes: |
| pgdata: {} |
| qdrant: {} |
| n8n: {}</code></pre> |
| <div class="callout warn"> |
| ทุก service เปิด port ใน compose นี้ไม่มี auth — สำหรับ <b>dev เท่านั้น</b>. ตอนขึ้น production ให้วาง Traefik/Caddy + OAuth ข้างหน้า, ปิด port ที่ไม่จำเป็น, ใส่ password ทุกตัวผ่าน secret manager |
| </div> |
| </section> |
|
|
| |
| <section id="roadmap"> |
| <h2><span class="num">⑧</span> Migration Roadmap</h2> |
| <p class="sub">ทำเป็นเฟส — แต่ละเฟสรันได้จริงก่อนไปต่อ</p> |
|
|
| <h3><span class="pill p-now">เฟส 1</span> Refactor core (1-2 สัปดาห์)</h3> |
| <ul> |
| <li>รวม <code>server.py</code> + <code>api/_lograg.py</code> เป็น package เดียว ลบ logic ซ้ำ</li> |
| <li>ย้ายไป FastAPI + Uvicorn, เพิ่ม Pydantic models สำหรับ request/response</li> |
| <li>เก็บ vector ใน Qdrant (เลิกใช้ SQLite blob), keyword ใน OpenSearch หรือ pgvector</li> |
| <li>เพิ่ม API key auth ขั้นต่ำ + rate limit</li> |
| </ul> |
|
|
| <h3><span class="pill p-next">เฟส 2</span> Real ingestion + automation (2-4 สัปดาห์)</h3> |
| <ul> |
| <li>ต่อ Vector/Fluent Bit → Redpanda → classify/embed worker</li> |
| <li>PostgreSQL schema (ข้อ ⑥) + retention policy</li> |
| <li>n8n WF-1 (Critical Responder) + WF-2 (Daily Digest)</li> |
| <li>ต่อ threat intel (AbuseIPDB/GeoIP), Slack/LINE notify</li> |
| </ul> |
|
|
| <h3><span class="pill p-later">เฟส 3</span> Scale & Ops (ต่อเนื่อง)</h3> |
| <ul> |
| <li>Observability: Prometheus + Grafana + Loki</li> |
| <li>ย้าย compose → Kubernetes (Helm), HPA สำหรับ worker</li> |
| <li>Self-host LLM (Ollama/vLLM) ลดค่า API + ข้อมูลไม่ออกนอก</li> |
| <li>Hybrid search tuning, eval RAG ด้วยชุด 50k, A/B model</li> |
| <li>WF-3 auto-block + approval flow, SOAR-style playbook</li> |
| </ul> |
| </section> |
|
|
| <footer> |
| ROGLAG Production Plan · สร้างเป็น HTML · อัปเดตแผนได้ตามเฟสที่ทำจริง |
| </footer> |
|
|
| </div> |
| </body> |
| </html> |
|
|