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\nIntroducción
\n\n\n\nLas 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\nEste 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\nPrerrequisitos
\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.
Desarrollo
\n\n\n\nProcedimiento
\n\n\n\n- Diseñar una clase Narrator que viaje con la petición y acumule eventos con metadatos (tipo, intención, contexto, timestamp).
- Instrumentar puntos clave: routing, validaciones, intentos de reintento, decisiones que afecten el flujo.
- Decidir retención: caducar segmentos de memoria cuando ya no aporten valor, o agrupar eventos en hilos causales.
- Exportar o exponer los hilos en un formato que un visor timeline pueda consumir (JSON, evento estructurado a logger, API interna).
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\nIntegrar 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\nEn 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\nEjemplos
\n\n\n\nEjemplo 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\nCon 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\nChecklist
\n\n\n\n- Decidir el alcance de narración: qué tipos de eventos y qué contexto incluir.
- Agregar un objeto Narrator accesible en el ciclo de vida de la petición.
- Instrumentar puntos críticos: routing, validaciones, retries, fallos y decisiones de negocio.
- Definir política de expiración o agregación para evitar ruido innecesario.
- Elegir destino de exportación: logger estructurado, índice temporal o UI de timeline.
- Validar que los eventos incluyan identificadores para correlación (request id, session id).
Conclusión
\n\n\n\nHacer 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\nEste 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