Logging
How the package logs, what it logs, and how to wire its log channel into Laravel's stack. Includes the dedicated-channel pattern most plants converge on.
The package logs through Laravel's standard LoggerInterface.
Every log message is in one of two surfaces:
- Client-side — your Laravel process emits log lines for connection events, errors, warnings.
- Daemon-side — the
php artisan opcua:sessionprocess emits log lines for session lifecycle, IPC errors, subscription state.
Both go through Laravel logging, so the same channel configuration applies.
Default channel
Without configuration, the package logs to the global
LOG_CHANNEL. The lines mix in with your application's other
logs:
[2026-05-15 10:15:23] production.INFO: User logged in
[2026-05-15 10:15:24] production.INFO: OPCUA connection opened plc-line-a
[2026-05-15 10:15:25] production.WARNING: User action rate-limited
[2026-05-15 10:15:26] production.ERROR: OPCUA connection failed plc-line-b
That's fine for low-volume use. For production, dedicate a channel.
Dedicated channel pattern
In config/logging.php:
'channels' => [
'opcua' => [
'driver' => 'daily',
'path' => storage_path('logs/opcua.log'),
'level' => env('OPCUA_LOG_LEVEL', 'info'),
'days' => 14,
'replace_placeholders' => true,
],
],
In .env:
OPCUA_LOG_CHANNEL=opcua # consumed by the daemon (session_manager.log_channel)
OPCUA_LOG_CHANNEL is read by config('opcua.session_manager.log_channel')
(it scopes the daemon's logger). The client-side per-connection
log channel is set in config/opcua.php under
connections.{name}.log_channel; it is hard-coded to 'stdout'
in the published default and is not sourced from an env var
unless you change the config file.
There is no OPCUA_LOG_LEVEL env var — the level is controlled
by the channel's own configuration in config/logging.php.
Now every package-emitted log line goes to your configured channel, ready to grep or to ship to external aggregation (Loki, Loggly, Sumo).
What the package logs
| Level | When | Example |
|---|---|---|
debug |
Per-call detail (only when APP_DEBUG=true) |
"Calling read for nodeId='ns=2;s=Speed'" |
info |
Connection lifecycle, daemon state changes | "Connection plc-line-a opened" |
notice |
Recoverable degradation (managed → direct fallback) | "Daemon unreachable, falling back to direct" |
warning |
Transient failures with auto-recovery | "Read retry 2/3 for ns=2;s=Speed" |
error |
Failed operations that surfaced as exceptions | "Write rejected: Bad_TypeMismatch" |
critical |
Failed-state daemon, lost subscriptions | "Daemon publish loop stuck — restart required" |
The package never logs:
- Passwords or auth tokens.
- Cert private keys.
- Raw OPC UA values from data changes (potentially huge, potentially sensitive).
If you want value logging, add an explicit listener and log through your channel — see Data events.
Per-connection log channel
Each connection can override the channel:
'connections' => [
'plc-line-a' => [
'endpoint' => '...',
'log_channel' => 'plc-line-a', // dedicated per line
],
'plc-line-b' => [
'endpoint' => '...',
'log_channel' => 'plc-line-b',
],
],
…with matching channel definitions in config/logging.php. Useful
when ops wants per-line log volumes for troubleshooting.
Stacked channels — Slack on errors
'channels' => [
'opcua' => [
'driver' => 'stack',
'channels' => ['opcua-daily', 'opcua-errors-slack'],
],
'opcua-daily' => [
'driver' => 'daily',
'path' => storage_path('logs/opcua.log'),
'days' => 14,
],
'opcua-errors-slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'OPCUA',
'emoji' => ':zap:',
'level' => 'error',
],
],
Daily file gets everything; Slack only sees error and above.
Structured logging
For machine-readable logs (ELK, Loki):
'opcua-json' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'with' => ['stream' => storage_path('logs/opcua.json')],
'formatter' => JsonFormatter::class,
],
Output:
{"datetime":"2026-05-15T10:15:24+00:00","channel":"opcua","level_name":"INFO",
"message":"Connection opened","context":{"connection":"plc-line-a",
"endpoint":"opc.tcp://...","duration_ms":312}}
…ready to be ingested by anything that speaks JSON.
Daemon-side logging
The daemon logs to the channel passed via --log-channel=:
php artisan opcua:session --log-channel=opcua --cache-store=redis
When started this way, the daemon's PSR-3 logger writes through
Laravel's opcua channel — file rotation, formatting, and
shipping all happen the Laravel way.
Without --log-channel, the daemon falls back to a stderr logger.
Useful only in Docker / containerised environments where stderr
goes to the orchestrator's log system.
Log volume — what to expect
| Workload | Lines per minute |
|---|---|
| 1 connection, no subscriptions | 0-5 |
| 5 connections, low-frequency reads | 5-20 |
| 5 connections, full auto-publish, 100 tags | 50-200 |
| Subscription drops + reconnect storm | 1000+ for a few min |
Set OPCUA_LOG_LEVEL=info for production. debug is too verbose
for sustained use.
Sensitive data in your listener logs
Listeners are your code, not the package's. The package doesn't log values; your listener can. Be deliberate:
use PhpOpcua\Client\Event\DataChangeReceived;
class LogDataChanges
{
public function handle(DataChangeReceived $event): void
{
// OK — handle, status, timestamp. No value.
Log::channel('plc-data')->info("data change", [
'client_handle' => $event->clientHandle,
'status' => $event->dataValue->statusCode,
'at' => $event->dataValue->sourceTimestamp?->format('c'),
]);
}
}
Logging values is fine when:
- The value's PII risk is zero (a temperature reading).
- Volume is low (engineering setpoint changes, not continuous process values).
- You actually need the data downstream.
Logging exceptions
The package raises typed exceptions on failure (see Reference · Exceptions). Catch them and log:
try {
Opcua::write($node, $value);
} catch (\PhpOpcua\Client\Exception\ServiceException $e) {
Log::channel('plc')->warning("Write rejected", [
'node_id' => $node,
'value' => $value,
'status' => $e->getStatusCode(),
'status_name' => \PhpOpcua\Client\Types\StatusCode::getName($e->getStatusCode()),
'message' => $e->getMessage(),
]);
throw $e;
}
Always include the relevant context — log lines without it are unhelpful for forensics.
Where to read next
- Caching — the package's cache surface.
- Debugging — when logs aren't enough.
- Telescope and Pulse — request-level observability.