Ctrl K
ComunidadGuías…

scripts/deploy_observation_qa.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Observability acceptance on the disposable Linux Deploy QA host only."""

import http.server
import json
import subprocess
import threading
import time
import uuid


class Collector:
    """Local OTLP/HTTP receiver with a controllable failure response."""

    def __init__(self):
        self.records = []
        self.errors = []
        self.fail = False
        self.lock = threading.Lock()
        owner = self

        class Handler(http.server.BaseHTTPRequestHandler):
            def do_POST(self):
                try:
                    length = int(self.headers.get("Content-Length", "0"))
                    assert self.path == "/v1/logs" and self.headers.get_content_type() == "application/json"
                    assert 0 < length <= 1024 * 1024
                    body = json.loads(self.rfile.read(length))
                    with owner.lock:
                        owner.records.append(body)
                        failure = owner.fail
                    response = b"{}"
                    self.send_response(400 if failure else 200)
                    self.send_header("Content-Type", "application/json")
                    self.send_header("Content-Length", str(len(response)))
                    self.end_headers()
                    self.wfile.write(response)
                except Exception as error:
                    with owner.lock:
                        owner.errors.append(str(error))
                    self.send_error(400)

            def log_message(self, *args):
                pass

        self.server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
        self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
        self.thread.start()

    @property
    def endpoint(self):
        return f"http://127.0.0.1:{self.server.server_port}"

    def set_failure(self, fail):
        with self.lock:
            self.fail = fail

    def observations(self):
        with self.lock:
            requests = list(self.records)
        output = []
        for request in requests:
            for resource in request["resourceLogs"]:
                attrs = {item["key"]: item["value"]["stringValue"] for item in resource["resource"]["attributes"]}
                for scope in resource["scopeLogs"]:
                    for record in scope["logRecords"]:
                        fields = {item["key"]: item["value"]["stringValue"] for item in record["attributes"]}
                        output.append((attrs, fields, record["body"]["stringValue"]))
        return output

    def close(self):
        self.server.shutdown()
        self.server.server_close()
        self.thread.join(timeout=5)
        assert not self.thread.is_alive(), "collector did not stop"
        assert not self.errors, self.errors


def run_observation(api, wait_for, traffic, network, spec_a, port, evidence):
    services = []
    collector = Collector()
    errors = []
    metrics = {"stages": [], "cleanupErrors": errors}
    evidence["observability"] = metrics
    started = time.monotonic()

    def path(service, suffix=""):
        return "/api/micro-services/" + service["id"] + suffix

    def current(service):
        return api.call("GET", path(service, "/deployment"))

    def snapshot(service):
        return api.call("GET", path(service, "/observability"))

    def log_exists(service, stream, message):
        return any(row["stream"] == stream and message in row["message"] for row in snapshot(service)["logs"])

    def create(label):
        name = "observe-" + uuid.uuid4().hex[:8]
        spec = {**spec_a, "name": name, "domain": name + ".example.test", "replicas": 1,
                "env": {**spec_a.get("env", {}), "OBS_ID": label}}
        service = {"id": api.call("POST", "/api/micro-services", spec)["id"], "spec": spec}
        services.append(service)
        api.call("POST", path(service, "/deployment"), {"microNetworkId": network})
        service["deployment"] = wait_for(lambda: (record if (record := current(service))["phase"] == "running" else None), timeout=180)
        return service

    def stop_service(service):
        phase = wait_for(lambda: (record if (record := current(service))["phase"] in ["running", "failed", "degraded", "stopped"] else None), timeout=180)["phase"]
        if phase != "stopped":
            api.call("POST", path(service, "/stop"))
            wait_for(lambda: current(service)["phase"] == "stopped", timeout=180)
        api.call("DELETE", path(service))

    def request_log(service):
        assert traffic("127.0.0.1", port, service["spec"]["domain"], "/cgi-bin/observation") == (200, "observed")

    try:
        alpha = create("alpha")
        beta = create("beta")
        for service, label in [(alpha, "alpha"), (beta, "beta")]:
            for stream in ["stdout", "stderr"]:
                wait_for(lambda: log_exists(service, stream, f"fixture-{stream}-{label}"), timeout=60,
                         message=f"{label} {stream} startup output was not collected")
        assert "fixture-stdout-beta" not in json.dumps(snapshot(alpha))
        assert "fixture-stderr-alpha" not in json.dumps(snapshot(beta))
        assert any(event["kind"] == "deployment" for event in snapshot(alpha)["events"])
        metrics["stages"].append("isolated stdout, stderr and deployment events")

        result = api.call("PUT", path(alpha, "/observability/otlp"), {"endpoint": collector.endpoint})
        assert result["endpoint"].rstrip("/") == collector.endpoint
        request_log(alpha)
        wait_for(lambda: any(attrs.get("firecrab.service.id") == alpha["id"] and
                             fields.get("log.iostream") == "stderr" and "fixture-cgi-alpha" in body
                             for attrs, fields, body in collector.observations()), timeout=60,
                 message="collector did not receive the service log")
        assert not any("fixture-stdout-alpha" in body for _, _, body in collector.observations()), "pre-opt-in history exported"
        assert not any(attrs.get("firecrab.service.id") == beta["id"] for attrs, _, _ in collector.observations())
        metrics["stages"].append("platform logs exported as OTLP/HTTP JSON after opt-in with service and stream labels")

        old = current(alpha)
        updated_spec = {**alpha["spec"], "env": {**alpha["spec"]["env"], "OBS_REDEPLOY": "1"}}
        update = api.call("POST", path(alpha, "/updates"), {"operationId": str(uuid.uuid4()),
            "expectedReleaseId": old["id"], "spec": updated_spec, "strategy": "rolling"})
        wait_for(lambda: (record if (record := current(alpha))["id"] == update["id"] and record["phase"] == "running" else None), timeout=180,
                 message="observability environment redeploy failed")
        status, app_env = traffic("127.0.0.1", port, alpha["spec"]["domain"], "/cgi-bin/otel-environment")
        assert status == 200
        endpoint, protocol, service_name = app_env.split("|")
        assert endpoint.rstrip("/") == collector.endpoint and protocol == "http/protobuf" and service_name == alpha["spec"]["name"]
        metrics["stages"].append("app OTLP environment applied at new VM boot; app metrics/traces are not generated by this fixture")

        collector.set_failure(True)
        request_log(alpha)
        wait_for(lambda: snapshot(alpha)["otlp"]["failedBatches"] > 0, timeout=60,
                 message="collector failure was not visible")
        collector.set_failure(False)
        metrics["stages"].append("collector failure visible in export status")

        subprocess.run(["systemctl", "stop", "firecrab-api.service"], check=True)
        try:
            address = beta["deployment"]["instances"][0]["ipv4"]
            assert traffic(address, 8080, "fixture", "/cgi-bin/observation") == (200, "observed")
        finally:
            subprocess.run(["systemctl", "start", "firecrab-api.service"], check=True)
        api.ready()
        wait_for(lambda: log_exists(beta, "stderr", "fixture-cgi-beta"), timeout=60,
                 message="output produced while the API was down was not recovered")
        assert "fixture-cgi-beta" not in json.dumps(snapshot(alpha))
        metrics["stages"].append("API restart buffered guest output without cross-service exposure")

        api.call("POST", path(alpha, "/stop"))
        wait_for(lambda: current(alpha)["phase"] == "stopped", timeout=180)
        wait_for(lambda: any(attrs.get("firecrab.service.id") == alpha["id"] and
                             fields.get("firecrab.observation.kind") == "event" and "stopped" in body
                             for attrs, fields, body in collector.observations()), timeout=60,
                 message="collector did not receive the lifecycle event")
        metrics["stages"].append("lifecycle event exported after API restart")
        print("PASS isolated service logs, API restart buffer, events and OTLP export", flush=True)
    except BaseException as error:
        metrics["failure"] = str(error)
        raise
    finally:
        try:
            api.ready()
        except Exception as error:
            errors.append("API readiness: " + str(error))
        for service in reversed(services):
            try:
                stop_service(service)
            except Exception as error:
                errors.append("service cleanup: " + str(error))
        try:
            collector.close()
        except Exception as error:
            errors.append("collector cleanup: " + str(error))
        metrics["elapsedSeconds"] = round(time.monotonic() - started, 2)
        assert not errors, errors