Ir al contenido

Reportar excepciones

QuietGuard\Monitor\Reporter es el cliente neutro respecto al framework. Los adaptadores lo conectan a los hooks de excepciones y de logs de un host; usted también puede pilotarlo a mano.

Cablear un Reporter

El constructor del Reporter recibe un Config, un Http\HttpClient, un Support\Scrubber, un Payload\ExceptionPayloadBuilder y un LoggerInterface PSR opcional:

php
use QuietGuard\Monitor\Config;
use QuietGuard\Monitor\Reporter;
use QuietGuard\Monitor\Http\CurlHttpClient;
use QuietGuard\Monitor\Support\Scrubber;
use QuietGuard\Monitor\Payload\ExceptionPayloadBuilder;

$config = new Config(
    url: 'https://monitor.example.com',
    key: 'token-de-proyecto',
    release: 'a1b2c3d',
);

$reporter = new Reporter(
    config: $config,
    http: new CurlHttpClient(),
    scrubber: new Scrubber(['password', 'authorization', 'api_key']),
    builder: new ExceptionPayloadBuilder(
        traceLimit: $config->traceLimit,
        release: $config->release,
        codeSnippets: $config->codeSnippets, // true por defecto; false envía solo archivo y línea
    ),
    logger: null, // Psr\Log\LoggerInterface opcional
);

Enviar datos

php
try {
    // su código
} catch (\Throwable $e) {
    $reporter->reportException($e, ['user_id' => 42]);
}

El Reporter expone tres métodos de envío, cada uno de los cuales devuelve un bool y nunca lanza excepciones: la supervisión no debe romper la aplicación anfitriona:

MétodoEndpointNotas
reportException(Throwable $e, array $context = [])POST /api/v1/ingestConstruye el payload, enmascara el context, lo envía.
sendLogs(array $logs)POST /api/v1/logsEnvuelve los logs en {"logs": [...]} y los enmascara. Devuelve true inmediatamente si $logs está vacío.
sendDependencies(array $packages)POST /api/v1/dependenciesEnvuelve en {"packages": [...]}. Devuelve false inmediatamente si $packages está vacío; las dependencias no se enmascaran.

Un envío solo devuelve true con un HTTP 2xx. Si el cliente no está configurado (Config::isConfigured() es falso), los métodos devuelven false sin tocar la red. Toda excepción de transporte se captura, se registra como advertencia a través del logger opcional, y se convierte en false.

Un rechazo también se registra, y para eso está el logger. Una respuesta que no es 2xx se escribe como advertencia, nombrando el estado y la dirección: un 401 por una clave mal pegada, un 402 mientras hay un pago pendiente, un 403 por un límite de plan. CurlHttpClient nunca lanza, devuelve 0: el catch anterior no podía activarse en esos casos, y cada uno llegaba a quien llamaba como un único false mudo. Pase un logger en un anfitrión que no tiene consola propia, o el cliente no tendrá forma alguna de decirle que nunca ha funcionado.

El payload de excepción

Payload\ExceptionPayloadBuilder::build() transforma un Throwable en la forma esperada en el cable:

php
[
    'exception' => [
        'class'   => $e::class,
        'message' => $e->getMessage(),
        'file'    => $e->getFile(),
        'line'    => $e->getLine(),
        'trace'   => [ // hasta traceLimit frames, el punto de lanzamiento primero
            ['class' => null, 'type' => null, 'function' => null, 'file' => $e->getFile(), 'line' => $e->getLine(),
             'code' => ['start' => ..., 'lines' => [...]]], // fragmento de código, solo frames de la aplicación
            ['class' => ..., 'type' => ..., 'function' => ..., 'file' => ..., 'line' => ...],
        ],
    ],
    'context' => [ /* release (si está definido) fusionado con su contexto */ ],
]

Desde la versión 0.3, la traza empieza en el punto de lanzamiento: el archivo y la línea de la propia excepción forman el frame cero, ya que getTrace() empieza en el llamador y nunca contuvo la línea que falló. La traza se trunca a traceLimit frames, ese frame incluido (1 = solo el punto de lanzamiento), cada uno reducido a class, type, function, file, line. Los frames de la aplicación llevan además un objeto code, producido por Payload\SourceSnippet: start, el número de la primera línea, y lines, las cinco líneas a cada lado de la línea del frame, leídas en el archivo que se ejecutó y cortadas a 500 caracteres, en como máximo diez frames por informe (ExceptionPayloadBuilder::MAX_SNIPPET_FRAMES). Un frame cuya ruta contiene /vendor/ nunca lo lleva, y cualquier lectura que falle responde null en lugar de perder el informe. codeSnippets: false en Config (y en el builder, que lo lee como tercer argumento) envía solo el archivo y la línea. El Reporter pasa los fragmentos por Scrubber::scrubSnippets(): una línea que nombra una clave de la lista de enmascaramiento, o una palabra de Scrubber::LINE_NEEDLES, y le da un valor se sustituye entera por [scrubbed], vea Enmascaramiento. Cuando release no es nulo, se fusiona en context (sus claves de contexto explícitas prevalecen en caso de colisión). El contrato exacto en el cable está descrito por la API de ingesta (/docs/tool/1.0/ingestion-api).

Cuatro campos se recortan a la longitud que acepta el servidor, mediante Support\Cut, marcando el corte con … [truncated]: message a 8192 caracteres, class a 255, file a 1024 y release a 255. El endpoint los valida y responde 422 para el reporte entero: una QueryException que lleva toda su consulta SQL, o una excepción de una clase anónima cuyo nombre contiene una ruta absoluta, eran precisamente los reportes que desaparecían. El corte cuenta caracteres y no bytes, de modo que un carácter multibyte nunca se parte en dos, lo que produciría un payload rechazado como UTF-8 inválido.

La abstracción de transporte

Http\HttpClient es un contrato deliberadamente mínimo:

php
interface HttpClient
{
    // devuelve el código de estado HTTP, o 0 si falla el transporte; no debe lanzar excepciones
    public function postJson(string $url, string $token, array $payload, int $timeout): int;
}

El Http\CurlHttpClient incluido lo implementa con ext-curl y sin ninguna otra dependencia: codifica el cuerpo en JSON y envía Content-Type: application/json, Accept: application/json y Authorization: Bearer <token>. Devuelve 0 (en lugar de lanzar una excepción) cuando la codificación JSON o curl_init() falla.

Traiga su propio cliente

Como HttpClient es una interfaz simple, y no PSR-18 en sí misma, puede encapsular cualquier cliente (un cliente PSR-18, Guzzle, el cliente HTTP del framework) detrás de ella. Las implementaciones no deben lanzar excepciones y deben devolver el código de estado (o 0):

php
use QuietGuard\Monitor\Http\HttpClient;
use Psr\Http\Client\ClientInterface;      // PSR-18
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;

final class Psr18HttpClient implements HttpClient
{
    public function __construct(
        private ClientInterface $client,
        private RequestFactoryInterface $requests,
        private StreamFactoryInterface $streams,
    ) {}

    public function postJson(string $url, string $token, array $payload, int $timeout): int
    {
        try {
            $request = $this->requests->createRequest('POST', $url)
                ->withHeader('Content-Type', 'application/json')
                ->withHeader('Accept', 'application/json')
                ->withHeader('Authorization', 'Bearer '.$token)
                ->withBody($this->streams->createStream((string) json_encode($payload)));

            return $this->client->sendRequest($request)->getStatusCode();
        } catch (\Throwable) {
            return 0;
        }
    }
}

Pase su implementación como argumento http al Reporter. Para capturar automáticamente los errores en una aplicación PHP simple, consulte el handler de errores global; para mantener los secretos fuera de los payloads, consulte el enmascaramiento.

Está leyendo la documentación PHP Core v1.0.