Catalyst / admin/Syanpse-Vanguard 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Syanpse-Vanguard

public
Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
Syanpse-Vanguard / synapse-vanguard-v3 / agents / common / sender.py 3646 B · main
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
"""Enrollment + reliable shipper. Registers once (persisting identity), then
loops: pull a batch from the disk buffer, zstd-compress, POST to /v1/ingest/bulk
with mTLS (prod) or bearer token (dev), and trim the buffer only after the
server acks. Network loss just grows the buffer; it drains on reconnect."""
from __future__ import annotations

import json
import logging
import time
from datetime import datetime, timezone

import httpx
import zstandard

from agents.common.buffer import DiskBuffer
from agents.common.config import AgentConfig, load_state, save_state

log = logging.getLogger("sv.agent.sender")
_ZSTD = zstandard.ZstdCompressor(level=3)


class Sender:
    def __init__(self, cfg: AgentConfig, buffer: DiskBuffer):
        self.cfg = cfg
        self.buffer = buffer
        self.state = load_state(cfg.state_path)
        self._client = self._build_client()

    def _build_client(self) -> httpx.Client:
        kwargs: dict = {"timeout": 30.0, "base_url": self.cfg.ingest_url}
        if self.cfg.mtls_enabled:
            kwargs["cert"] = (self.cfg.client_cert, self.cfg.client_key)
            kwargs["verify"] = self.cfg.ca_cert or self.cfg.verify_tls
        else:
            kwargs["verify"] = self.cfg.verify_tls
        return httpx.Client(**kwargs)

    # --- enrollment ---------------------------------------------------------
    def ensure_enrolled(self) -> None:
        if self.state.get("agent_id"):
            return
        resp = self._client.post(
            "/v1/agents/register",
            headers={"Authorization": f"Bearer {self.cfg.enrollment_token}"},
            json={
                "hostname": self.cfg.hostname,
                "platform": self.cfg.platform,
                "agent_version": self.cfg.agent_version,
                "channels": self.cfg.channels,
            },
        )
        resp.raise_for_status()
        data = resp.json()
        self.state["agent_id"] = data["agent_id"]
        if data.get("ingest_token"):
            self.state["ingest_token"] = data["ingest_token"]
        save_state(self.cfg.state_path, self.state)
        log.info("enrolled as agent_id=%s", self.state["agent_id"])

    # --- shipping -----------------------------------------------------------
    def flush_once(self) -> int:
        max_id, lines = self.buffer.next_batch(self.cfg.batch_size)
        if not lines:
            return 0
        body = _ZSTD.compress(("\n".join(lines)).encode())
        bundle = {
            "agent_id": self.state["agent_id"],
            "seq": max_id,
            "codec": "zstd",
            "event_count": len(lines),
            "sent_at": datetime.now(timezone.utc).isoformat(),
        }
        headers = {"X-SV-Bundle": json.dumps(bundle),
                   "Content-Type": "application/octet-stream"}
        if not self.cfg.mtls_enabled:
            headers["Authorization"] = f"Bearer {self.state['ingest_token']}"

        resp = self._client.post("/v1/ingest/bulk", headers=headers, content=body)
        resp.raise_for_status()
        ack = resp.json()
        self.buffer.trim(ack["last_seq"])           # only trim what the server accepted
        return ack["accepted"]

    def run_forever(self) -> None:
        self.ensure_enrolled()
        while True:
            try:
                sent = self.flush_once()
                if sent:
                    log.info("shipped %d events (%d pending)", sent, self.buffer.pending())
            except Exception as exc:                # keep buffering on any failure
                log.warning("flush failed (buffering): %s", exc)
            time.sleep(self.cfg.flush_interval_s)