"""Remote-telemetry posture reporting and the dry-run-safe flush composition.
Wires the CLI surface promised by ``2026-07-04-remote-telemetry-adr`` (GitHub
issue #407) over the core telemetry package
(:mod:`~core.telemetry`) without re-implementing any of its gate, schema,
or sink logic (``composition-service-no-parallel-write-path``):
* :func:`~application.diagnostics_telemetry.build_telemetry_status_report`
projects the current
:class:`~core.config.Settings` posture (opt-in, tier, gestor mode,
endpoint) plus whether an emission would currently be permitted, so an
operator can inspect the deployment's telemetry posture without guessing at
environment variable names.
* :func:`~application.diagnostics_telemetry.build_telemetry_flush_preview`
aggregates the same local, non-sensitive LLM run-timing signal
:func:`~application.diagnostics_run_health.build_run_health_report` already
reads (:class:`~adapters.outbound.llm.LLMRunTelemetryRecorder`)
into ONE allowlisted :class:`~core.telemetry.TelemetryEventPayload` via
:func:`~core.telemetry.build_telemetry_payload`, and reports whether the
consent gate (:func:`~core.telemetry.telemetry_emit_permitted`) would
currently permit sending it. This function never sends anything: it is the
``--dry-run`` preview surface. ``build_telemetry_flush_preview`` is also the
payload-construction step the real (non-dry-run) flush reuses, so preview and
send can never observe a different payload shape.
* :func:`~application.diagnostics_telemetry.flush_telemetry` performs the real
send: it reuses the identical
preview payload, re-checks the consent gate, and -- only when both the gate
permits AND an endpoint is configured -- hands the payload to a real
:class:`~core.telemetry.HttpTelemetrySink`. When the gate refuses or no
endpoint is configured, it is a pure no-op (mirroring
:func:`~core.telemetry.emit_telemetry_event`'s own no-op contract), so
calling this function is always safe regardless of posture.
No producer here reads transaction content, profile identity, or file
contents; the aggregate is built from the same accounting/timing-only
:class:`~adapters.outbound.llm.LLMRunRecord` rows the local-only
``run-health`` diagnostics already expose.
See Also:
:mod:`~core.telemetry`
Consent gate, closed payload schema, workspace hash, and optional HTTP
sink reused by this application service.
:func:`~application.diagnostics_run_health.build_run_health_report`
Local-only LLM run accounting source aggregated by the flush preview.
:mod:`~entrypoints.cli._app_diagnostics_telemetry`
CLI transport that exposes status and dry-run-safe flush commands.
:class:`~core.telemetry.TelemetryEventPayload`
Closed allowlisted payload shape built by preview and reused by send.
"""
from __future__ import annotations
from pydantic import BaseModel, ConfigDict
from ..core.config import Settings, load_settings
from ..core.telemetry import (
HttpTelemetrySink,
TelemetryEventPayload,
TelemetryTier,
build_telemetry_payload,
emit_telemetry_event,
telemetry_emit_permitted,
workspace_hash,
)
from ..core.time import now
from .diagnostics_run_health import build_run_health_report
__all__ = [
"TelemetryFlushPreview",
"TelemetryStatusReport",
"build_telemetry_flush_preview",
"build_telemetry_status_report",
"flush_telemetry",
]
_STRICT_FROZEN = ConfigDict(strict=True, frozen=True)
_FLUSH_COMMAND = "diagnostics.llm_run"
[docs]
class TelemetryStatusReport(BaseModel):
"""The deployment's current remote-telemetry posture.
Projects the raw :class:`~core.config.Settings` telemetry fields
plus the derived ``would_emit`` verdict a hypothetical fully-acknowledged
invocation would currently receive from
:func:`~core.telemetry.telemetry_emit_permitted`. Never triggers an
emission; this is a read-only report.
"""
model_config = _STRICT_FROZEN
opt_in: bool
tier: TelemetryTier
gestor_mode: bool
endpoint: str | None = None
would_emit_if_acknowledged: bool
[docs]
class TelemetryFlushPreview(BaseModel):
"""The payload a flush would send, plus whether it would currently send at all.
``payload`` is the exact allowlisted :class:`~core.telemetry.TelemetryEventPayload`
:func:`~application.diagnostics_telemetry.flush_telemetry` would hand to the
sink; ``gate_permits`` and ``would_send`` are evaluated against the SAME
``acknowledged`` value the caller supplied (never hardcoded to ``True``), so
a preview built without an acknowledgement honestly reports "would not
currently send" even when opt-in, tier, and endpoint are otherwise fully
configured. ``would_send`` folds the consent-gate verdict with whether an
endpoint is configured, so a dry-run preview can honestly report "built, but
would not transmit" (refused consent, or no endpoint) versus "would
transmit".
"""
model_config = _STRICT_FROZEN
payload: TelemetryEventPayload
gate_permits: bool
endpoint_configured: bool
would_send: bool
[docs]
def build_telemetry_status_report(*, settings: Settings | None = None) -> TelemetryStatusReport:
"""Report the current remote-telemetry consent posture.
Args:
settings: Resolved deployment settings; defaults to
:func:`~core.config.load_settings`.
Returns:
The populated
:class:`~application.diagnostics_telemetry.TelemetryStatusReport`.
"""
resolved_settings = settings if settings is not None else load_settings()
return TelemetryStatusReport(
opt_in=resolved_settings.aeat_telemetry_opt_in,
tier=resolved_settings.aeat_telemetry_tier,
gestor_mode=resolved_settings.aeat_telemetry_gestor_mode,
endpoint=resolved_settings.aeat_telemetry_endpoint,
would_emit_if_acknowledged=telemetry_emit_permitted(resolved_settings, acknowledged=True),
)
def _build_flush_payload(settings: Settings) -> TelemetryEventPayload:
report = build_run_health_report()
return build_telemetry_payload(
workspace_hash=workspace_hash(settings.aeat_local_storage_root),
command=_FLUSH_COMMAND,
counters={
"runs": report.total_runs,
"succeeded": report.total_succeeded,
"failed": report.total_failed,
},
succeeded=report.total_failed == 0,
captured_at=now().isoformat(),
)
[docs]
def build_telemetry_flush_preview(
*,
settings: Settings | None = None,
acknowledged: bool = False,
) -> TelemetryFlushPreview:
"""Build the allowlisted payload a flush would send, without sending it.
Aggregates every locally recorded LLM run
(:class:`~adapters.outbound.llm.LLMRunRecord`, read via
:func:`~application.diagnostics_run_health.build_run_health_report`)
into one ``diagnostics.llm_run`` :class:`~core.telemetry.TelemetryEventPayload`.
This is the sole payload-construction step; both the ``--dry-run`` preview
and the real
:func:`~application.diagnostics_telemetry.flush_telemetry` call this function
so they can never observe a different payload shape.
``gate_permits``/``would_send`` are evaluated against ``acknowledged``
exactly as supplied -- defaulting to ``False`` (the honest state of a bare
``--dry-run`` invocation with no acknowledgement flag), never hardcoded to
``True``. This mirrors :func:`~core.telemetry.telemetry_emit_permitted`'s
own never-sticky per-invocation acknowledgement contract.
Args:
settings: Resolved deployment settings; defaults to
:func:`~core.config.load_settings`.
acknowledged: Whether the operator acknowledged remote telemetry for
this specific invocation. Never sticky.
Returns:
The populated
:class:`~application.diagnostics_telemetry.TelemetryFlushPreview`. Never
performs a network call.
"""
resolved_settings = settings if settings is not None else load_settings()
payload = _build_flush_payload(resolved_settings)
gate_permits = telemetry_emit_permitted(resolved_settings, acknowledged=acknowledged)
endpoint_configured = bool(resolved_settings.aeat_telemetry_endpoint)
return TelemetryFlushPreview(
payload=payload,
gate_permits=gate_permits,
endpoint_configured=endpoint_configured,
would_send=gate_permits and endpoint_configured,
)
[docs]
def flush_telemetry(*, settings: Settings | None = None, acknowledged: bool) -> TelemetryFlushPreview:
"""Send the aggregate local telemetry payload, honouring the consent gate.
Reuses
:func:`~application.diagnostics_telemetry.build_telemetry_flush_preview`
(with the SAME ``acknowledged`` value) for payload construction and verdict
computation, so the returned report always reflects the real invocation's
acknowledgement -- never a hardcoded optimistic verdict. Delegates the
actual gate check and dispatch to :func:`~core.telemetry.emit_telemetry_event`
(``composition-service-no-parallel-write-path``): this function never
re-implements the consent gate or the HTTP transport.
A real send requires ALL of: the consent gate permits (deployment opt-in,
non-``off`` tier, gestor mode off) AND ``acknowledged`` is ``True`` for
THIS invocation (never sticky) AND ``settings.aeat_telemetry_endpoint`` is
configured. Any missing condition makes this call a pure no-op -- nothing
is sent -- mirroring :func:`~core.telemetry.emit_telemetry_event`'s own
no-op contract.
Args:
settings: Resolved deployment settings; defaults to
:func:`~core.config.load_settings`.
acknowledged: Whether the operator acknowledged remote telemetry for
this specific invocation. Never sticky; must be re-affirmed on
every call.
Returns:
The :class:`~application.diagnostics_telemetry.TelemetryFlushPreview`
reflecting exactly what was (or, because a condition was refused, would
have been) sent for THIS invocation's ``acknowledged`` value.
"""
resolved_settings = settings if settings is not None else load_settings()
preview = build_telemetry_flush_preview(settings=resolved_settings, acknowledged=acknowledged)
sink = HttpTelemetrySink(endpoint=resolved_settings.aeat_telemetry_endpoint)
emit_telemetry_event(preview.payload, settings=resolved_settings, acknowledged=acknowledged, sink=sink)
return preview