Monitoring the daemon
Liveness probes, session counts, log channels, and a Symfony-native /health/opcua endpoint pattern.
A production daemon needs five questions answered at any moment:
- Is it up?
- How many sessions are open?
- Are notifications flowing?
- What was the last error?
- How is memory looking?
This page covers the Symfony-native answers.
1 — Liveness probe controller
namespace App\Controller;
use PhpOpcua\SymfonyOpcua\OpcuaManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
final class HealthController extends AbstractController
{
public function __construct(private OpcuaManager $opcua) {}
#[Route('/health/opcua', methods: ['GET'])]
public function opcua(): JsonResponse
{
try {
$alive = $this->opcua->isSessionManagerRunning();
} catch (\Throwable $e) {
return $this->json(['status' => 'down', 'error' => $e->getMessage()], 503);
}
return $this->json([
'status' => $alive ? 'up' : 'direct-mode',
]);
}
}
up— the daemon's socket file exists (a passivefile_exists()check; not an active ping).direct-mode— the socket doesn't exist, the bundle would fall through to direct connections.503— probe itself failed.
Wire into your liveness aggregator (Kubernetes probe, Pingdom, Better Uptime).
2 — Detailed health probe
OpcuaManager does not expose a daemonStats() method. The
only daemon-side probe it ships is isSessionManagerRunning()
(a file_exists check on the Unix socket — see the previous
section).
For richer daemon stats — session count, uptime, in-flight
requests — open an IPC connection directly and call a
daemon-side admin verb if your opcua-session-manager version
supports one. Alternatively, surface the same information via a
listener-side counter (see "Notification flow" below) plus
systemd's MainPID / log inspection.
3 — Notification flow
Track event throughput with a tiny listener + cache counter:
namespace App\EventListener;
use PhpOpcua\Client\Event\DataChangeReceived;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
final class TrackOpcuaFlow
{
public function __construct(private CacheInterface $cache) {}
#[AsEventListener]
public function __invoke(DataChangeReceived $event): void
{
$total = $this->cache->get('opcua.notif.total', function (ItemInterface $i) {
$i->expiresAfter(86400);
return 0;
});
$this->cache->delete('opcua.notif.total');
$this->cache->get('opcua.notif.total', function (ItemInterface $i) use ($total) {
$i->expiresAfter(86400);
return $total + 1;
});
$this->cache->delete('opcua.notif.last');
$this->cache->get('opcua.notif.last', fn(ItemInterface $i) =>
$i->expiresAfter(3600) ?? (new \DateTimeImmutable())->format('c')
);
}
}
Then probe:
#[Route('/health/opcua/flow', methods: ['GET'])]
public function flow(CacheInterface $cache): JsonResponse
{
$total = $cache->get('opcua.notif.total', fn() => 0);
$last = $cache->get('opcua.notif.last', fn() => null);
$stale = $last !== null
&& (new \DateTimeImmutable())->diff(new \DateTimeImmutable($last))->i > 5;
return $this->json([
'total' => $total,
'last_notification' => $last,
'stale' => $stale,
], $stale ? 503 : 200);
}
If you expect notifications every few seconds and the last one was 5 minutes ago, something's wrong upstream.
For high-volume deployments, don't use the cache contract for counters — use a dedicated Redis counter or a metrics exporter (see below). The example above is fine for moderate throughput.
4 — Last error
The daemon logs to your Monolog OPCUA_LOG_CHANNEL. For
real-time error visibility:
monolog:
channels: ['opcua']
handlers:
opcua_file:
type: rotating_file
path: '%kernel.logs_dir%/opcua.log'
max_files: 14
channels: ['opcua']
level: info
opcua_slack:
type: slack
token: '%env(SLACK_TOKEN)%'
channel: '#ops-alerts'
channels: ['opcua']
level: error
include_extra: true
Daemon errors land both in the daily file (for forensics) and in Slack (for immediate response).
5 — Memory
The daemon's memory grows with session count + monitored items. 5 sessions, 100 items = ~80-150 MB. Spikes warrant a look.
Probe via the OS:
systemctl status opcua-session-manager # shows RSS
ps -o rss,pid,command -p "$(pgrep -f opcua:session)"
…or via daemonStats() if you've added memory to its response.
Prometheus exporter
A scheduled Symfony command exports daemon stats to a Prometheus push gateway:
namespace App\Command;
use PhpOpcua\SymfonyOpcua\OpcuaManager;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
#[AsCommand(name: 'app:opcua:metrics')]
final class ExportOpcuaMetricsCommand extends Command
{
public function __construct(
private OpcuaManager $opcua,
private HttpClientInterface $http,
private string $pushGatewayUrl,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
// Push the only signal the manager natively exposes — whether the
// daemon's Unix socket exists. For richer stats (session count,
// uptime) you'll need to wire your own IPC-level probe to the
// daemon (see opcua-session-manager's IPC docs) or accumulate
// counters from event listeners.
$alive = (int) $this->opcua->isSessionManagerRunning();
$metrics = sprintf("opcua_socket_present %d\n", $alive);
$this->http->request('POST', $this->pushGatewayUrl . '/metrics/job/opcua', [
'body' => $metrics,
]);
return Command::SUCCESS;
}
}
Schedule every 15 s via Symfony Scheduler:
use Symfony\Component\Console\Messenger\RunCommandMessage;
#[AsSchedule('opcua-metrics')]
final class OpcuaMetricsSchedule implements ScheduleProviderInterface
{
public function getSchedule(): Schedule
{
return (new Schedule())
->add(RecurringMessage::every('15 seconds', new RunCommandMessage('app:opcua:metrics')));
}
}
Profiler / data collectors
Symfony's WebProfilerBundle can show OPC UA calls — see Observability · Profiler and data collectors.
Common alarm rules
| Rule | Trigger / threshold | Likely cause |
|---|---|---|
isSessionManagerRunning() returns false |
> 1 minute | Daemon down, socket gone |
No DataChangeReceived |
> 5 minutes with auto-publish on | All subscriptions dropped |
| ERROR-level logs | > 10/minute | Something's actively wrong |
| Daemon memory | > 1.5× normal | Leak in a third-party module/listener |
Debugging from the terminal
For one-shot diagnostics:
# The IPC envelope is length-prefixed JSON. The request shape is
# {"command":"...", "sessionId":"...", "method":"...", "params":{}, "authToken":"..."}.
# Use the helper script in opcua-session-manager's docs (it handles
# the 4-byte length prefix).
opcua-session-manager-probe /var/run/opcua/sessions.sock
See opcua-session-manager's
debugging-with-netcat recipe.
Where to read next
You've finished Session manager. Continue with Events overview for the listener side.