Logging

Kinetis’s logging is plain PSR-3 — Psr\Log\LoggerInterface — not a Kinetis-specific contract. Any PSR-3 logger works: register it once, and everything inside Kinetis that logs uses it automatically.

Registering your own logger

use Kinetis\Container\AppScope;
use Psr\Log\LoggerInterface;

$app = new AppScope();
$app->instance(LoggerInterface::class, $yourLogger);
$app->boot();

Register it before boot(), the same discipline as every other AppScope binding — see Appendix: Container Lifecycle.

If you never register one, AppScope::boot() binds a default that depends on the environment: in development (APP_ENV=development), Kinetis\Logging\ErrorLogLogger, a minimal PSR-3 logger writing through error_log() — so an exception during local development always leaves a trail in the server log with zero setup; in production, Psr\Log\NullLogger, which silently discards everything. Framework internals resolve LoggerInterface through the container, so something is always bound by the time they run, and your own registration wins in both environments.

Where Kinetis logs, and why

Nowhere by default on a successful request — logging every request regardless of outcome is something you opt into with your own global middleware, not something the framework forces. Every place framework internals log on their own is a genuine anomaly signal, not routine chatter:

ExceptionHandlerMiddleware

A global middleware every Kernel wires in unconditionally (see Middleware). Resolved through the container, so its LoggerInterface autowires from whatever you registered:

error: Unhandled exception while handling {method} {path}: {message}
  method: POST
  path: /users
  message: ...
  exception: <the Throwable itself>

The exception context key follows PSR-3 convention — Monolog and other real loggers look for exactly that key to render a stack trace.

A broken Kinetis\Http\Exception\HttpStatusExceptionInterface implementation — httpStatus() throwing, or returning a value outside the 400-599 range the interface requires (see Middleware) — logs the same way, exception still the original, and one further context key, mappingFailure, holding a Middleware\Exception\HttpStatusMappingException describing what went wrong trying to map it, chaining the real secondary cause via getPrevious() when there was one.

A Kinetis\Http\ValidationExceptionRendererInterface that throws logs the same way too — exception the ValidationException being rendered, and renderFailure whatever the renderer threw. A validation failure rendered normally logs nothing at all: it reports a client mistake, not a framework fault.

This log call is best-effort: a registered logger that itself throws cannot prevent the 500 this middleware exists to guarantee. The same is true wherever else a framework internal logs from inside a fallback it must still honor regardless — Http\Kernel’s request-scope disposal, for one, see Appendix: System Layout. See Kinetis\Logging\SafeLogger.

TransactionGuard::rollbackDangling()

With kinetis/database-bridge installed, every request scope receives a lazy TransactionGuard binding, and rollbackDangling() is registered on a scope’s disposal only once that scope resolves the guard (see Appendix: Container Lifecycle’s “Request-scope initializers” and Database). It logs a warning only once it has actually closed an active transaction, so a unit of work that ended every transaction it began logs nothing and the warning stays an anomaly signal. If closing a transaction fails, that’s logged as an error instead, one line per failure — a strictly more severe signal than the routine warning, since it means a transaction survived the unit of work. See Appendix: Databases’s “What happens when cleanup itself fails” for the full failure behavior, including that this is all independent of the logger itself: an exception the logger throws is discarded rather than allowed to affect cleanup or misreport what actually happened.

McpServer’s top-level exception handler

The JSON-RPC transport’s counterpart to ExceptionHandlerMiddleware: an unexpected Throwable reaching McpServer::handle()’s outer catch becomes a -32603 Internal error response, logged the same way (see Model Context Protocol (MCP)). kinetis/mcp’s own package bootstrap binds McpServer and resolves LoggerInterface from the container to build it, so the /mcp route and bin/kinetis mcp:serve share one server carrying whatever logger you registered. Installing the package is the whole setup.

RequestScope disposal failures

Every owner that disposes a RequestScope after its own unit of work has already produced a real outcome — Kernel, kinetis/queue’s QueueWorker/SyncQueue, the MCP stdio and streamed-HTTP transports, and bin/kinetis — logs a disposal failure separately from that outcome, rather than letting it replace or suppress it (see Appendix: Container Lifecycle’s own general explanation of why, and each owner’s own page — Middleware, Queue, Model Context Protocol (MCP), CLI — for its exact precedence rule). Every one of these calls goes through Kinetis\Logging\SafeLogger::logFrom(), not log(): log() alone only protects the resolved logger’s own log() call, but $container->get(LoggerInterface::class) is itself evaluated as a plain argument expression before any containment is ever entered, so a throwing LoggerInterface binding would escape uncaught right at the point disposal tries to report its own failure. logFrom() takes the resolution itself as a callable instead, so a broken binding is contained the same way a broken logger’s own log() call already is — the same best-effort discipline as every other log call on this page: a registered logger that itself throws, or fails to resolve at all, cannot affect the real outcome these calls report on.

See also