Etiqueta: logging estructurado

  • Dejar que PHP narre su runtime: guía práctica

    Dejar que PHP narre su runtime: guía práctica

    \n

    Resumen: este artículo muestra cómo aplicar un patrón de \”Narrator\” en PHP para que la propia aplicación narre su flujo de ejecución. Incluye un diseño básico, integración y ejemplos de código listos para adaptar.

    \n\n\n\n\n\n\n\n

    Introducción

    \n\n\n\n

    Las soluciones tradicionales de observabilidad entregan señales reactivas: métricas, trazas y logs que requieren correlación externa. En lugar de depender exclusivamente de herramientas externas, podemos hacer que la aplicación misma produzca una narración del runtime: eventos con contexto e intención unidos en hilos causales.

    \n\n\n\n

    Este enfoque complementa (no necesariamente reemplaza) la observabilidad convencional: su objetivo es mejorar la comprensibilidad interna —por ejemplo, por qué se tomó una decisión— y facilitar la depuración rápida sin saltar entre múltiples herramientas.

    \n\n\n\n

    Prerrequisitos

    \n\n\n\n
    • Entorno PHP donde puedas anexar objetos al ciclo de vida de la petición (middleware o front controller).
    • Un logger compatible PSR-3 (por ejemplo Monolog) o un adaptador propio para exportar hilos de eventos cuando sea necesario.
    • Mínima estructura para identificar contexto: request id, session id o user id para correlación.
    \n\n\n\n

    Desarrollo

    \n\n\n\n

    Procedimiento

    \n\n\n\n
    1. Diseñar una clase Narrator que viaje con la petición y acumule eventos con metadatos (tipo, intención, contexto, timestamp).
    2. Instrumentar puntos clave: routing, validaciones, intentos de reintento, decisiones que afecten el flujo.
    3. Decidir retención: caducar segmentos de memoria cuando ya no aporten valor, o agrupar eventos en hilos causales.
    4. Exportar o exponer los hilos en un formato que un visor timeline pueda consumir (JSON, evento estructurado a logger, API interna).
    \n\n\n\n

    A continuación se muestra una implementación mínima de una clase Narrator que acumula eventos y permite serializarlos. Es un punto de partida para adaptar a frameworks o middlewares.

    \n\n\n\n
    <?php\nnamespace App\Observability;\n\nclass Narrator\n{\n    private array $threads = [];\n    private array $currentContext = [];\n\n    public function startThread(string $id, array $meta = []): void\n    {\n        $this->threads[$id] = [\n            'id' => $id,\n            'meta' => $meta,\n            'events' => [],\n        ];\n    }\n\n    public function annotate(string $threadId, string $type, string $message, array $context = []): void\n    {\n        $this->threads[$threadId]['events'][] = [\n            'ts' => microtime(true),\n            'type' => $type,\n            'message' => $message,\n            'context' => $context,\n        ];\n    }\n\n    public function setContext(array $context): void\n    {\n        $this->currentContext = $context;\n    }\n\n    public function expireThread(string $threadId): void\n    {\n        unset($this->threads[$threadId]);\n    }\n\n    public function export(): array\n    {\n        return $this->threads;\n    }\n}\n
    \n\n\n\n

    Integrar el Narrator con el flujo de la petición (por ejemplo como middleware) permite que cada controlador o servicio registre decisiones y motivos, y al final de la petición exporte el hilo a un logger o lo envíe a un endpoint interno.

    \n\n\n\n
    <?php\n// Ejemplo simplificado de middleware (framework-agnóstico)\nuse App\\Observability\\Narrator;\nuse Psr\\Log\\LoggerInterface;\n\nclass NarratorMiddleware\n{\n    private Narrator $narrator;\n    private LoggerInterface $logger;\n\n    public function __construct(Narrator $narrator, LoggerInterface $logger)\n    {\n        $this->narrator = $narrator;\n        $this->logger = $logger;\n    }\n\n    public function handle($request, $next)\n    {\n        $requestId = $request->getAttribute('request_id') ?? uniqid('req_', true);\n        $this->narrator->startThread($requestId, ['path' => $request->getUri()]);\n        $this->narrator->setContext(['request_id' => $requestId]);\n\n        $response = $next($request);\n\n        // Al final de la petición exportamos el hilo\n        $threads = $this->narrator->export();\n        $this->logger->info('narrator.threads', ['threads' => $threads]);\n\n        return $response;\n    }\n}\n
    \n\n\n\n

    En el ejemplo anterior, el logger recibe la estructura completa; un visor de timeline o un consumidor puede mostrar los eventos como una historia por petición. También puede enviarse a un índice o guardarse en almacenamiento temporal.

    \n\n\n\n
    {\n  \"req_606e2a\": {\n    \"id\": \"req_606e2a\",\n    \"meta\": {\"path\": \"/checkout\"},\n    \"events\": [\n      {\"ts\": 1690000000.123, \"type\": \"decision\", \"message\": \"cookie override - route B\", \"context\": {\"cookie\": \"promo\"}},\n      {\"ts\": 1690000000.456, \"type\": \"intent\", \"message\": \"calculate delivery estimate\", \"context\": {\"address\": \"...\"}},\n      {\"ts\": 1690000000.789, \"type\": \"notice\", \"message\": \"missing delivery estimate - user exited\", \"context\": {}}\n    ]\n  }\n}\n
    \n\n\n\n

    Ejemplos

    \n\n\n\n

    Ejemplo de uso dentro de una función de negocio: anotar la intención antes de ejecutar y el resultado después. Así se preserva la causalidad entre intención y efecto.

    \n\n\n\n
    <?php\n// Dentro de un servicio\nfunction applyCoupon(Narrator $narrator, string $threadId, array $couponData)\n{\n    $narrator->annotate($threadId, 'intent', 'apply coupon', ['coupon' => $couponData['code']]);\n\n    // Lógica real\n    $applied = false;\n    // ... comprobar validaciones, límites, fecha ...\n\n    if ($applied) {\n        $narrator->annotate($threadId, 'result', 'coupon applied', ['discount' => 10]);\n    } else {\n        $narrator->annotate($threadId, 'result', 'coupon rejected', ['reason' => 'expired']);\n    }\n\n    return $applied;\n}\n
    \n\n\n\n

    Con esta información, los equipos de producto y soporte pueden leer la cadena de eventos y comprender qué decisión tomó la aplicación y por qué, sin reconstruir el estado a partir de múltiples fuentes.

    \n\n\n\n

    Checklist

    \n\n\n\n
    1. Decidir el alcance de narración: qué tipos de eventos y qué contexto incluir.
    2. Agregar un objeto Narrator accesible en el ciclo de vida de la petición.
    3. Instrumentar puntos críticos: routing, validaciones, retries, fallos y decisiones de negocio.
    4. Definir política de expiración o agregación para evitar ruido innecesario.
    5. Elegir destino de exportación: logger estructurado, índice temporal o UI de timeline.
    6. Validar que los eventos incluyan identificadores para correlación (request id, session id).
    \n\n\n\n

    Conclusión

    \n\n\n\n

    Hacer que PHP narre su propio runtime reduce la necesidad de interpretar trazas desconectadas y acelera la resolución de problemas. Empieza por una implementación pequeña: una clase Narrator, puntos de instrumentación selectos y un canal para exportar hilos.

    \n\n\n\n

    Este patrón no elimina herramientas de observabilidad, pero aporta una capa de contexto y causalidad que hace a las aplicaciones más autoexplicativas y a los equipos menos dependientes de correlaciones externas.

    \n\n