merge: T045 install telemetry write-back
This commit is contained in:
commit
88e9a39503
3 changed files with 100 additions and 0 deletions
|
|
@ -6,6 +6,7 @@ import socket
|
||||||
import sys
|
import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -20,6 +21,7 @@ from m2_market.ledger import InsufficientFunds, LedgerClient
|
||||||
from m2_market.output import emit_json, format_price, render_kv, render_table, search_results_table
|
from m2_market.output import emit_json, format_price, render_kv, render_table, search_results_table
|
||||||
from m2_market.publish import PublishValidationError, publish_bundle, validate_bundle
|
from m2_market.publish import PublishValidationError, publish_bundle, validate_bundle
|
||||||
from m2_market.registry import RegistryClient, RegistryError
|
from m2_market.registry import RegistryClient, RegistryError
|
||||||
|
from m2_market.telemetry import post_install_outcome
|
||||||
|
|
||||||
# Exit-code map (contracts/cli.md).
|
# Exit-code map (contracts/cli.md).
|
||||||
EXIT_OK = 0 # ok / no-op
|
EXIT_OK = 0 # ok / no-op
|
||||||
|
|
@ -201,6 +203,7 @@ def install(ctx: click.Context, listing_id: str, yes: bool, adapter_name: str |
|
||||||
ref = f"{listing_id}:{socket.gethostname()}:{uuid.uuid4()}"
|
ref = f"{listing_id}:{socket.gethostname()}:{uuid.uuid4()}"
|
||||||
seller = listing.get("seller") or solution.get("seller")
|
seller = listing.get("seller") or solution.get("seller")
|
||||||
amount = price.get("amount")
|
amount = price.get("amount")
|
||||||
|
started = time.monotonic()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
grant = ledger.install(buyer=config.operator_id, seller=seller, amount=amount, ref=ref)
|
grant = ledger.install(buyer=config.operator_id, seller=seller, amount=amount, ref=ref)
|
||||||
|
|
@ -221,6 +224,15 @@ def install(ctx: click.Context, listing_id: str, yes: bool, adapter_name: str |
|
||||||
bundle_root = _extract_bundle(bundle_path, tmp_dir / "extracted")
|
bundle_root = _extract_bundle(bundle_path, tmp_dir / "extracted")
|
||||||
except (RegistryError, OSError, tarfile.TarError) as exc:
|
except (RegistryError, OSError, tarfile.TarError) as exc:
|
||||||
ledger.refund(ref)
|
ledger.refund(ref)
|
||||||
|
post_install_outcome(
|
||||||
|
config,
|
||||||
|
listing_id,
|
||||||
|
seller,
|
||||||
|
amount,
|
||||||
|
ref,
|
||||||
|
"fetch_failed_refunded",
|
||||||
|
time.monotonic() - started,
|
||||||
|
)
|
||||||
click.echo(
|
click.echo(
|
||||||
f"m2-market install: bundle fetch/verify failed, refunded: {exc}",
|
f"m2-market install: bundle fetch/verify failed, refunded: {exc}",
|
||||||
err=True,
|
err=True,
|
||||||
|
|
@ -244,6 +256,15 @@ def install(ctx: click.Context, listing_id: str, yes: bool, adapter_name: str |
|
||||||
return
|
return
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
ledger.refund(ref)
|
ledger.refund(ref)
|
||||||
|
post_install_outcome(
|
||||||
|
config,
|
||||||
|
listing_id,
|
||||||
|
seller,
|
||||||
|
amount,
|
||||||
|
ref,
|
||||||
|
"apply_failed_refunded",
|
||||||
|
time.monotonic() - started,
|
||||||
|
)
|
||||||
click.echo(
|
click.echo(
|
||||||
f"m2-market install: apply failed, refunded: {exc}\n"
|
f"m2-market install: apply failed, refunded: {exc}\n"
|
||||||
"Remediation: inspect the recipe's post-install checks, fix the "
|
"Remediation: inspect the recipe's post-install checks, fix the "
|
||||||
|
|
@ -260,6 +281,16 @@ def install(ctx: click.Context, listing_id: str, yes: bool, adapter_name: str |
|
||||||
changeset.changeset_hash,
|
changeset.changeset_hash,
|
||||||
"applied",
|
"applied",
|
||||||
)
|
)
|
||||||
|
post_install_outcome(
|
||||||
|
config,
|
||||||
|
listing_id,
|
||||||
|
seller,
|
||||||
|
amount,
|
||||||
|
ref,
|
||||||
|
"applied",
|
||||||
|
time.monotonic() - started,
|
||||||
|
grant_id=grant.get("grant_id"),
|
||||||
|
)
|
||||||
|
|
||||||
_print_entrypoint_hint(json_output, solution, {"listing_id": listing_id, "noop": False})
|
_print_entrypoint_hint(json_output, solution, {"listing_id": listing_id, "noop": False})
|
||||||
|
|
||||||
|
|
|
||||||
58
cli/src/m2_market/telemetry.py
Normal file
58
cli/src/m2_market/telemetry.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
"""Install outcome telemetry write-back (contracts/catalog-index.md §Telemetry write-back).
|
||||||
|
|
||||||
|
Fire-and-forget: writes an episodic memory into the `market:evidence` partition so
|
||||||
|
listings accrue install evidence. Telemetry must never break an install — any
|
||||||
|
failure is swallowed after printing one warning line to stderr.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from m2_market.config import Config
|
||||||
|
|
||||||
|
PARTITION = "market:evidence"
|
||||||
|
|
||||||
|
|
||||||
|
def post_install_outcome(
|
||||||
|
config: Config,
|
||||||
|
listing_id: str,
|
||||||
|
seller: str | None,
|
||||||
|
amount,
|
||||||
|
ref: str,
|
||||||
|
outcome: str,
|
||||||
|
wall_seconds: float,
|
||||||
|
grant_id: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Best-effort write of an install outcome to memory-api. Never raises."""
|
||||||
|
try:
|
||||||
|
with httpx.Client(
|
||||||
|
base_url=config.memory_api_url,
|
||||||
|
headers={"X-API-Key": config.memory_api_key},
|
||||||
|
timeout=10.0,
|
||||||
|
) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/memory/store",
|
||||||
|
json={
|
||||||
|
"content": f"install {outcome}: {listing_id}",
|
||||||
|
"agent_id": PARTITION,
|
||||||
|
"memory_type": "episodic",
|
||||||
|
"importance": 0.8,
|
||||||
|
"source": "m2-market-cli",
|
||||||
|
"metadata": {
|
||||||
|
"listing_id": listing_id,
|
||||||
|
"buyer": config.operator_id,
|
||||||
|
"seller": seller,
|
||||||
|
"amount": amount,
|
||||||
|
"outcome": outcome,
|
||||||
|
"ref": ref,
|
||||||
|
"wall_seconds": wall_seconds,
|
||||||
|
"grant_id": grant_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
except Exception as exc: # noqa: BLE001 - telemetry must never break an install
|
||||||
|
print(f"m2-market: telemetry write-back failed (ignored): {exc}", file=sys.stderr)
|
||||||
|
|
@ -85,6 +85,7 @@ class FakeState:
|
||||||
self.refund_calls = 0
|
self.refund_calls = 0
|
||||||
self.license_exists = False
|
self.license_exists = False
|
||||||
self.insufficient_funds = False
|
self.insufficient_funds = False
|
||||||
|
self.telemetry_calls = []
|
||||||
|
|
||||||
|
|
||||||
def _b64_json(payload: dict) -> str:
|
def _b64_json(payload: dict) -> str:
|
||||||
|
|
@ -164,6 +165,12 @@ def make_handler(state: FakeState):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if host == "catalog.test" and path == "/memory/store":
|
||||||
|
body = json.loads(request.content)
|
||||||
|
assert body["agent_id"] == "market:evidence", body
|
||||||
|
state.telemetry_calls.append(body)
|
||||||
|
return httpx.Response(200, json={"id": "mem-telemetry-1"})
|
||||||
|
|
||||||
raise AssertionError(f"unexpected request: {request.method} {request.url}")
|
raise AssertionError(f"unexpected request: {request.method} {request.url}")
|
||||||
|
|
||||||
return handler
|
return handler
|
||||||
|
|
@ -255,6 +262,10 @@ def test_funded_install_happy_path(fake_backend, config_env, monkeypatch):
|
||||||
assert calls.plan_calls == 1
|
assert calls.plan_calls == 1
|
||||||
assert calls.apply_calls == 1
|
assert calls.apply_calls == 1
|
||||||
assert calls.verify_calls == 1
|
assert calls.verify_calls == 1
|
||||||
|
assert len(fake_backend.telemetry_calls) == 1
|
||||||
|
telemetry = fake_backend.telemetry_calls[0]
|
||||||
|
assert telemetry["metadata"]["outcome"] == "applied"
|
||||||
|
assert telemetry["metadata"]["listing_id"] == LISTING_ID
|
||||||
|
|
||||||
|
|
||||||
def test_insufficient_funds_no_adapter_call(fake_backend, config_env, monkeypatch):
|
def test_insufficient_funds_no_adapter_call(fake_backend, config_env, monkeypatch):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue