Librería satélite opt-in de logging estructurado para Phobos Framework. Logs JSON line-delimited con contexto de traza automático (trace_id/request_id), redacción de datos sensibles y writers intercambiables. Implementa PSR-3 sin arrastrar Monolog ni ninguna otra dependencia pesada — fiel al minimalismo del núcleo, que a propósito no trae logger.
- 📋 PSR-3 completo -
Psr\Log\LoggerInterface, interpola placeholders{clave}, valida niveles - 🧱 JSON estructurado - una línea = un objeto JSON, listo para Loki/agentes de logs (patrón K8s)
- 🔗 Contexto de traza -
trace_id/request_idinyectados en cada registro desde unLogContextrequest-scoped - 🌐 W3C Trace Context - adopta el
traceparententrante (Kong/OTel) y lo propaga saliente contraceParent() - 🕵️ Redacción de secretos - enmascara claves sensibles (
password,token,authorization, ...) y patrones sueltos (JWT, PAN) - 🚦 Access-log integrado -
request.handledcon método, path, status y duración; nivel según status (4xx=warning, 5xx=error) - 🖥️ Writers intercambiables -
stdout(prod),pretty(dev, coloreado),file,memory(tests),null - 💥 Excepciones expandidas -
context['exception']con unThrowablese expande a{class, message, file, line, trace} - 🧪 Testeable por diseño - DI por constructor en todo;
MemoryWriterpara afirmar logs en tests - 🚫 Cero magia - sin facades, sin estado estático: la forma canónica de Phobos
composer require mongoose-studio/phobos-framework-loggermongoose-studio/phobos-framework^3.1psr/log^3.0- PHP >= 8.4
public function providers(): array {
return [AppServiceProvider::class, LoggerServiceProvider::class];
}Primero en la cadena, para que todo lo demás ya loguee con contexto:
Phobos::init(ROOT, APPLICATION)
->loadEnvironment()->loadConfig()
->middleware(RequestLogContextMiddleware::class)
->middleware(CorsMiddleware::class)
->bootstrap(ApiModule::class)
->run()->send();use Psr\Log\LoggerInterface;
class PostService {
public function __construct(private LoggerInterface $log) {} // autowiring
public function publish(object $data): array {
$this->log->info('post.publishing', ['kind' => $data->kind]);
try {
// ...
} catch (\Throwable $e) {
$this->log->error('post.publish_failed', ['exception' => $e]);
throw $e; // nunca tragarse la excepción
}
return ['status' => 'ok'];
}
}Depende de LoggerInterface (PSR-3), no de la clase concreta: testeable y reemplazable.
try {
Phobos::init(ROOT, APPLICATION)->/* ... */->run()->send();
} catch (\Throwable $e) {
inject(Psr\Log\LoggerInterface::class)->critical('unhandled.exception', ['exception' => $e]);
// ... luego la traducción a JSON del template
}// A una llamada HTTP saliente (Guzzle) o al sobre de un evento del bus:
$headers['traceparent'] = logContext()->traceParent();{
"timestamp": "2026-07-15T12:00:00.123Z",
"level": "info",
"service": "core-social",
"message": "request.handled",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"request_id": "0af7651916cd43dd8448eb211c80319c",
"tenant_id": null,
"actor_id": null,
"context": { "method": "GET", "path": "/v1/feed", "status": 200, "duration_ms": 42.1 }
}tenant_id/actor_id se promueven a la raíz: se fijan una vez en el LogContext (p. ej. en el middleware de auth) y salen en todos los registros del request.
logContext()->setActorId($userId);
logContext()->setTenantId($clientId);Todo es opcional; sin archivo, los defaults funcionan.
<?php
return [
'service' => env('SERVICE_NAME', 'app'),
'level' => env('LOG_LEVEL', 'info'), // umbral mínimo PSR-3
'writer' => env('LOG_WRITER', is_prod() ? 'stdout' : 'pretty'),
'access_log' => filter_var(env('LOG_ACCESS', true), FILTER_VALIDATE_BOOL),
'redact' => ['authorization', 'cookie', 'password', 'token',
'refresh_token', 'access_token', 'secret', 'pan', 'card'],
'file_path' => storage_path('logs/app.log'), // solo si writer=file
];| Writer | Uso | Salida |
|---|---|---|
stdout |
Producción (K8s) | JSON line-delimited a stdout; el agente (Promtail/Grafana Agent) lo lleva a Loki |
pretty |
Desarrollo | Consola coloreada y legible |
file |
VPS/scripts | JSON append a archivo con lock (la rotación es de logrotate) |
memory |
Tests | Acumula en memoria (records(), last(), clear()) |
null |
Apagado | Descarta todo |
Sin PII ni secretos en los logs, en ningún ambiente. Dos mecanismos:
- Por clave (recursivo, insensible a mayúsculas, por contención):
password,card_number,reset_token_hash... →•••••• - Por patrón (sobre strings sueltos): JWT y PAN de tarjeta se enmascaran aunque la clave no delate nada
$log->info('login.attempt', ['email' => $email, 'password' => $pass]);
// → "context": {"email":"a@b.c","password":"••••••"}Bindea el writer de memoria y afirma sobre lo logueado:
$writer = new MemoryWriter();
container()->instance(WriterInterface::class, $writer);
// ... ejercitar el código ...
$this->assertSame('post.publishing', $writer->last()['message']);El Observer (trace(), Observer::dumpFormatted()) es una línea de tiempo en memoria para depurar un request puntual; no persiste ni sale del proceso. Este Logger es para producción: estructurado, persistente (stdout → Loki) y correlacionado por trace_id. Son complementarios.
composer testEste proyecto está licenciado bajo la Licencia MIT - ver el archivo LICENSE para más detalles.
Marcel Rojas
marcelrojas16@gmail.com
Mongoose Studio
Las contribuciones son bienvenidas. Por favor:
- Fork el proyecto
- Crea una rama para tu feature (
git checkout -b feature/amazing-feature) - Commit tus cambios (
git commit -m 'Add amazing feature') - Push a la rama (
git push origin feature/amazing-feature) - Abre un Pull Request
Phobos Framework by Mongoose Studio