Remonter les exceptions
QuietGuard\Monitor\Reporter est le client neutre vis-à-vis du framework. Les adaptateurs le branchent sur les hooks d'exceptions et de logs d'un hôte ; vous pouvez aussi le piloter à la main.
Câbler un Reporter
Le constructeur du Reporter prend un Config, un Http\HttpClient, un Support\Scrubber, un Payload\ExceptionPayloadBuilder et un LoggerInterface PSR optionnel :
Envoyer des données
Le Reporter expose trois méthodes d'envoi, chacune renvoyant un bool et ne levant jamais d'exception, la surveillance ne doit pas casser l'application hôte :
| Méthode | Endpoint | Notes |
|---|---|---|
reportException(Throwable $e, array $context = []) | POST /api/v1/ingest | Construit le payload, masque le context, l'envoie. |
sendLogs(array $logs) | POST /api/v1/logs | Enveloppe les logs en {"logs": [...]} et les masque. Renvoie true immédiatement si $logs est vide. |
sendDependencies(array $packages) | POST /api/v1/dependencies | Enveloppe en {"packages": [...]}. Renvoie false immédiatement si $packages est vide ; les dépendances ne sont pas masquées. |
Un envoi ne renvoie true que sur un HTTP 2xx. Si le client n'est pas configuré (Config::isConfigured() est faux), les méthodes renvoient false sans toucher au réseau. Toute exception de transport est capturée, journalisée via le logger optionnel en avertissement, et transformée en false.
Un refus est journalisé lui aussi, et c'est à cela que sert le logger. Une réponse non-2xx est écrite en avertissement, nommant le statut et l'adresse : un 401 sur une clé mal collée, un 402 pendant une relance de paiement, un 403 sur une limite de forfait. CurlHttpClient ne lève jamais, il renvoie 0 : le catch ci-dessus ne pouvait donc pas se déclencher sur ces cas, et chacun arrivait à l'appelant sous la forme d'un unique false muet. Passez un logger dans un hôte qui n'a pas de console à lui, sans quoi le client n'a aucun moyen de vous dire qu'il n'a jamais fonctionné.
Le payload d'exception
Payload\ExceptionPayloadBuilder::build() transforme un Throwable en la forme attendue sur le fil :
Depuis la version 0.3, la trace commence au site de levée : le fichier et la ligne de l'exception elle-même forment la frame zéro, puisque getTrace() commence à l'appelant et n'a jamais contenu la ligne fautive. La trace est tronquée à traceLimit frames, cette frame comprise (1 = le site seul), chacune réduite à class, type, function, file, line. Les frames de l'application portent en plus un objet code, produit par Payload\SourceSnippet : start, le numéro de la première ligne, et lines, les cinq lignes de part et d'autre de la ligne de la frame, lues dans le fichier qui a tourné et coupées à 500 caractères, sur au plus dix frames par rapport (ExceptionPayloadBuilder::MAX_SNIPPET_FRAMES). Une frame dont le chemin contient /vendor/ n'en porte jamais, et toute lecture qui échoue répond null plutôt que de perdre le rapport. codeSnippets: false sur Config (et sur le builder, qui le lit en troisième argument) n'envoie que le fichier et la ligne. Le Reporter passe les extraits par Scrubber::scrubSnippets() : une ligne qui nomme une clé de la liste de masquage, ou un mot de Scrubber::LINE_NEEDLES, et lui donne une valeur est remplacée entière par [scrubbed], voir Masquage. Quand release est non nul, il est fusionné dans context (vos clés de contexte explicites l'emportent en cas de collision). Le contrat exact sur le fil est décrit par l'API d'ingestion (/docs/tool/1.0/ingestion-api).
Quatre champs sont coupés à la longueur que le serveur accepte, par Support\Cut, la coupe étant marquée par … [truncated] : message à 8192 caractères, class à 255, file à 1024 et release à 255. L'endpoint les valide et répond 422 pour le rapport entier : une QueryException portant toute sa requête SQL, ou une exception issue d'une classe anonyme dont le nom contient un chemin absolu, étaient donc précisément les rapports qui disparaissaient. La coupe compte des caractères et non des octets, si bien qu'un caractère multi-octets n'est jamais coupé en deux, ce qui produirait une charge refusée comme UTF-8 invalide.
L'abstraction de transport
Http\HttpClient est un contrat volontairement minimal :
Le Http\CurlHttpClient fourni l'implémente avec ext-curl et sans aucune autre dépendance : il encode le corps en JSON et envoie Content-Type: application/json, Accept: application/json et Authorization: Bearer <token>. Il renvoie 0 (au lieu de lever une exception) quand l'encodage JSON ou curl_init() échoue.
Apporter votre propre client
Comme HttpClient est une simple interface, et non PSR-18 lui-même, vous pouvez encapsuler n'importe quel client (un client PSR-18, Guzzle, le client HTTP du framework) derrière elle. Les implémentations ne doivent pas lever d'exception et doivent renvoyer le code de statut (ou 0) :
Passez votre implémentation comme argument http au Reporter. Pour capturer automatiquement les erreurs dans une appli PHP simple, voir le handler d'erreurs global ; pour garder les secrets hors des payloads, voir le masquage.
Vous lisez la documentation PHP Core v1.0.