Logging

Who this is for

You are running DeskFlow (or any Pionia app) and need to see what happened — action debug lines, SQL traces, or uncaught errors — without leaking passwords in log output.

What you will learn

  • Where logs go by default in v3
  • How to call logger() and report() from services
  • How to configure [logging] in environment/settings.ini
  • How to add a file channel under storage/logs/

Before you start

Before you start
  • A booted app (php pionia serve)
  • Familiarity with Exception pipeline (errors flow through report())

How logging fits together

flowchart LR Svc[TaskService action] --> Logger[logger] Err[Uncaught throwable] --> Report[report via pipeline] Logger --> LM[LogManager] Report --> LM LM --> Handlers[Monolog handlers] Handlers --> Out[stderr / storage/logs/]

Pionia v3 uses Monolog behind a thin wrapper:

PieceRole
logger()Default application logger (PSR-3)
logger('api')Named channel via LogManager
report($e)Log throwables through the exception pipeline (no HTTP response)
[logging] in settings.iniFormat, redaction, optional response logging
storage/logs/Where file channels and RoadRunner detach logs live

Not server.log

Older guides referred to a server.log file in the project root and a [SERVER] LOG_DESTINATION key. v3 apps do not use that layout. Default output goes through Monolog’s ErrorLogHandler (typically stderr / the PHP error log for your SAPI). For a project log file, use a file channel (below) or storage/logs/roadrunner.log when running RoadRunner detached.


Step 1 — Log from a service

In DeskFlow’s TaskService, log when a task is listed:

protected function listAction(Arrayable $data): ApiResponse
{
    logger()->info('task.list', [
        'status_filter' => $data->get('status'),
        'user' => 'alex@northwind.studio',
    ]);

    return response(0, 'OK', ['tasks' => [/* … */]]);
}

Available levels (PSR-3): debug, info, notice, warning, error, critical, alert, emergency.

Example line (TEXT format):

[2026-07-04 03:15:02] deskflow-api.info >> task.list  {"status_filter":"open","user":"alex@northwind.studio"}

The channel prefix (deskflow-api) comes from APP_NAME in environment/.env.


Step 2 — Log errors with report()

Uncaught exceptions in HTTP and CLI should go through the exception pipeline, not ad-hoc try/catch + logger() in kernel code.

In your action code, when you catch and rethrow or need to log without rendering:

try {
    // business logic
} catch (\Throwable $e) {
    report($e);
    throw $e;
}

Register pipeline behaviour on boot:

// bootstrap/application.php or a Provider
app()->exceptions()
    ->dontReport(\Pionia\Exceptions\ValidationException::class)
    ->reportable(fn (\Throwable $e) => /* Sentry, etc. */);

See Exceptions for maps, handlers, and debug JSON payloads.


Step 3 — Configure [logging]

All logging keys live under [logging] in environment/settings.ini (not [SERVER] or [LOGGER]).

[logging]
LOG_FORMAT=TEXT
HIDE_IN_LOGS=password,pin,token,secret
HIDE_SUB=*********
KeyPurpose
LOG_FORMATTEXT / LINE (default line), JSON, SCALAR, HTML, SYSLOG
HIDE_IN_LOGSComma-separated field names redacted in log context arrays
HIDE_SUBReplacement string for redacted values (default *********)
LOG_RESPONSESWhen true, Moonlight responses are logged at debug in ApiSwitch
LOG_HANDLERSAdvanced: extra Monolog handler classes (comma-separated)
LOG_PROCESSORSAdvanced: Monolog processor classes

Redaction applies when you pass context arrays — e.g. logger()->info('member.login', ['password' => '…']) masks password.

SQL query logging

To log every Porm query (verbose — use locally only):

[server]
LOG_QUERIES=true

Or in .env: LOG_QUERIES=true / SHOW_QUERIES=true.


Step 4 — Watch logs locally

Built-in server (php pionia serve) — logs usually appear in the same terminal running the server.

File channel (recommended for tailing):

Register in an app provider:

use Pionia\Logging\LogManager;

public function configureLogging(LogManager $log): void
{
    $log->extend('file', [
        'driver' => 'file',
        'path' => 'storage/logs/app.log',
        'level' => 'debug',
    ]);
}

Then in .env: LOG_CHANNEL=file, or call logger('file') explicitly.

Tail the file:

tail -f storage/logs/app.log

RoadRunner detached — background workers log to:

php pionia runserver --detach
php pionia runserver:logs   # tails storage/logs/roadrunner.log

Named channels

logger('api')->warning('Rate limit approaching', ['ip' => $request->ip()]);

Register channels in Provider::configureLogging() with $log->extend('api', ['driver' => 'single']) or a file driver as above.


Production notes

TopicRecommendation
SecretsExtend HIDE_IN_LOGS; never log raw JWTs or passwords
VolumeAvoid LOG_RESPONSES=true in production — responses can be large
RotationUse logrotate on storage/logs/*.log
External sinksWire ->reportable() to Sentry/Datadog; or add Monolog handlers via LOG_HANDLERS
Debug modeDEBUG=false hides stack traces from HTTP JSON — it does not disable logger()

Common mistakes

  • tail -f server.log — that path is v1/v2; use the terminal, storage/logs/app.log, or runserver:logs
  • Settings in [SERVER] — use [logging] for LOG_FORMAT, HIDE_IN_LOGS, LOG_RESPONSES
  • Logging inside switches — use services/actions; let the exception pipeline handle uncaught errors
  • Expecting request bodies in logs — enable LOG_RESPONSES only when debugging; bodies are not logged by default

What’s next

Exceptions

Pipeline, dontReport, debug payloads.

Developer stats

Request metrics at /stats.

Helpers

logger(), report(), shouldLogResponses().