Logging
A PSR-3 logger and a structured context — that's the whole logging surface. The library logs at every protocol step; what you see is governed by your logger's level.
The library logs through any PSR-3 LoggerInterface. There is no
custom log facade, no global configuration — pass the logger to the
builder and the library writes through it.
Wiring a logger
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use PhpOpcua\Client\ClientBuilder;
$logger = new Logger('opcua');
$logger->pushHandler(new StreamHandler('php://stderr', Logger::DEBUG));
$client = ClientBuilder::create()
->setLogger($logger)
->connect('opc.tcp://plc.local:4840');
Any PSR-3 logger works — Monolog, Laravel's logger, Symfony's logger,
your own. Without setLogger(), the client uses a NullLogger and
emits nothing.
What gets logged
The library logs at four PSR-3 levels:
| Level | What |
|---|---|
error |
Failures the caller will see as exceptions: connection, security, encoding. |
warning |
Recoverable surprises: retries triggered, ServiceFault on optional service sets, cache corruption recovered. |
info |
Lifecycle and significant transitions: connect, disconnect, subscription create/delete, channel open/close, certificate trust decisions. |
debug |
Per-call protocol detail: every Read / Write / Browse / Call request and response with timing. |
Default Monolog handlers ship at Logger::DEBUG. In production, set
the threshold to INFO or higher unless you are actively debugging —
DEBUG produces one log entry per service call, which is a lot.
Per-call context
Every log entry carries a structured context. The fields that appear on most entries:
| Key | Type | Meaning |
|---|---|---|
endpoint |
string |
The configured opc.tcp:// URL |
state |
string |
ConnectionState at the time of the call |
requestId |
int |
The protocol-level request ID, when one is allocated |
service |
string |
Short name (read, browse, createSession, …) |
nodeId |
string |
Stringified NodeId for node-scoped calls |
duration_ms |
float |
Call duration in milliseconds (debug level only) |
statusCode |
int |
OPC UA status (when present) |
These keys are stable across versions — safe to grep, index, alert on.
Reading the debug stream
Wire a logger at DEBUG for a session, run one of your normal calls,
and the protocol shape becomes obvious. A typical read() looks
like:
DEBUG opcua.connecting {"endpoint":"opc.tcp://plc.local:4840"}
DEBUG opcua.transport.hello {"endpoint":"opc.tcp://plc.local:4840"}
DEBUG opcua.transport.ack {"endpoint":"opc.tcp://plc.local:4840"}
DEBUG opcua.opn.request {"endpoint":"opc.tcp://plc.local:4840","requestId":1}
DEBUG opcua.opn.response {"endpoint":"opc.tcp://plc.local:4840","requestId":1,"duration_ms":12.7}
DEBUG opcua.session.create {"endpoint":"opc.tcp://plc.local:4840","requestId":2}
DEBUG opcua.session.activate {"endpoint":"opc.tcp://plc.local:4840","requestId":3}
INFO opcua.connected {"endpoint":"opc.tcp://plc.local:4840"}
DEBUG opcua.read.request {"nodeId":"i=2261","requestId":4}
DEBUG opcua.read.response {"nodeId":"i=2261","requestId":4,"statusCode":0,"duration_ms":3.1}
That trace is the protocol — if you ever wondered "what is the client actually doing", this is where you find out.
Routing by source
Most loggers support routing by channel name. The library writes
under the channel you configured ('opcua' in the Monolog example).
A common production setup:
opcuachannel atINFOto a regular log handleropcuachannel atDEBUGto a per-host file, rotated daily and shipped on demand to the troubleshooting workflow
Avoid mixing OPC UA debug logs into the main application channel — they will dominate volume.
Sensitive payloads
The library logs:
- Endpoint URLs and NodeIds (architectural information)
- Status codes and timings
- Subscription IDs and request IDs
The library does not log:
- Authentication tokens (
getAuthToken()is opaque on the wire and never serialised into log context) - Username / password values
- Certificate bodies (only fingerprints, at trust decisions)
- Variant
valuefields
The trace above is safe to share for support. If you nonetheless need
to redact further, wrap your PSR-3 logger with a filter that strips
the nodeId field — server architecture is the most sensitive
remaining signal.
Logging vs events
Logs are append-only, human-readable, and best for diagnostics. Events (see Events) are typed, addressable, and best for programmatic reactions (metrics, alerts, business logic).
Wire both: the dispatcher for things you want to react to, the logger for things you want to read later.
Tip
For a quick first run, set the logger to DEBUG and dump to
php://stderr. Once you have a feel for the protocol cadence, lift
the threshold to INFO and start wiring events.
Performance
Logging cost is dominated by the handler. Monolog at DEBUG to a
file is fast enough to leave on in production; the same logger to
syslog over a network is slow enough to dominate a busy worker. Use a
BufferHandler or FingersCrossedHandler if you want debug detail
on error and silence otherwise.
The library does not memoize log messages — they are formatted at the
call site. A NullLogger skips the formatting entirely.