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¶
Appendix: Container Lifecycle —
AppScope::boot()’s registration-lock discipline in full.Middleware —
ExceptionHandlerMiddleware’s place in the global pipeline.Database —
TransactionGuard’s full request-lifecycle role.Model Context Protocol (MCP) — the JSON-RPC error-handling convention
McpServerfollows.