Observability
What Signals ships for monitoring — Nightwatch as a tenant-configured APM, health probes, structured log channels, request correlation, slow-query logging, and failed-job alerting.
The shape of it
Signals treats observability as a tenant-configured integration, not a build-time decision. Nothing here requires a vendor account: every layer degrades to a durable local artefact — a log channel, a health card, an in-app notification — and an installation that does subscribe to an APM gets the richer view on top.
| Layer | Ships as | Configured in |
|---|---|---|
| APM | Laravel Nightwatch | Admin → Integrations → Monitoring (nightwatch settings group) |
| Queue depth | System Health card + GET /api/v1/system/health |
config/queue.php workers map |
| Health probes | /health/live, /health/ready, GET /api/v1/system/health |
Nothing to configure |
| Health diagnostics | Admin → System → System Health, php artisan doctor |
config/observability.php thresholds |
| Structured logs | webhooks, imports, auth, api, usage channels |
config/logging.php |
| Request correlation | X-Request-Id, end to end |
config/observability.php |
| Slow queries | Log warning per query over threshold | config/observability.php |
| Failed jobs | queue.job_failed notification |
Admin → Notifications (channels + recipients) |
Health endpoints
Two unauthenticated probes, registered outside the web and api middleware groups
so they never touch sessions, CSRF, or API metering:
| Endpoint | Answers | Cost |
|---|---|---|
GET /health/live |
Is the process up? | Touches no dependency at all |
GET /health/ready |
Can it serve traffic? | Database, Redis, and queue reachability; cached 5s, lock-guarded against probe stampedes, throttled 60/min |
GET /api/v1/system/health is the authenticated equivalent (system.read permission,
system:read ability), deriving its status from the same readiness subset.
Readiness deliberately stays cheap. The richer diagnostics — disk space, pending
migrations, S3, scheduler heartbeat, PHP limits, per-queue depth, failed-job detail —
live on the System Health page and in
php artisan doctor, which are pulled rather than polled.
Structured log channels
Domain traffic is separated so it can be retained and monitored independently:
| Channel | Retention | Carries |
|---|---|---|
webhooks |
7 days | Outbound delivery attempts, refusals, permanent failures |
imports |
7 days | Import plan start, per-step completion, pause reason, completion |
auth |
90 days | Sign-in, sign-out, failed attempt, lockout, password reset, SSO callback |
api |
1 day | Request/response detail when API metering capture is on |
usage |
14 days | Throughput-bucket accounting |
All are JSON-formatted. Two of them are security-sensitive by construction and are
written accordingly: the auth channel never records a credential (only the
account identifier from the attempt), and slow-query logging never records
bindings. Both routinely carry passwords, tokens, and personal data, and a
diagnostic log must not become a second copy of them.
Request correlation
Every request gets an X-Request-Id — echoed on the response, honoured if the caller
supplied one — and it follows the work all the way through:
App\Http\Middleware\LogContextmints it and sharesrequest_id,user_id, andwarehouse_idinto the log context, so every channel inherits them.App\Support\Logging\QueueRequestIdPropagationstamps it into every queued job's payload and adopts it in the worker, chaining through nested dispatches.- A global HTTP client middleware carries it onto outgoing calls, so a trace survives into whatever Signals talks to. A caller that sets its own header keeps it.
Turn step 3 off with OBSERVABILITY_PROPAGATE_REQUEST_ID=false if an upstream rejects
unexpected headers.
Slow queries
Any single query slower than the threshold is logged at warning with its SQL
template, duration, and the ambient request context:
OBSERVABILITY_SLOW_QUERY_LOGGING=true
OBSERVABILITY_SLOW_QUERY_THRESHOLD_MS=500
OBSERVABILITY_SLOW_QUERY_LOG_CHANNEL=stack
Nightwatch detects the same queries and additionally flags N+1 patterns; this is the floor underneath it for installations without a subscription.
Failed-job alerting
A job that exhausts its retries now tells someone rather than only landing in
failed_jobs:
- Always — an
errorline on the configured channel with job, queue, connection, exception class, and message. - Then — the
queue.job_failednotification, whose channels and recipients are configured per installation in Admin → Notifications. Default recipients are the active owners.
Identical failures are deduplicated by a fingerprint of job name, exception class, and message, so one broken job cannot flood an inbox:
OBSERVABILITY_FAILED_JOB_ALERTS=true
OBSERVABILITY_FAILED_JOB_ALERT_COOLDOWN=60
OBSERVABILITY_FAILED_JOB_LOG_CHANNEL=stack
The log line is never suppressed — only the notification is.
Error tracking
Unhandled exceptions are reported with request_id, user_id, and warehouse_id
attached, even when they escape outside the middleware stack. Reports go to the
configured log channels and to Nightwatch when it is enabled. Signals does not bundle
a second error-tracking vendor; adding one is an application-level dependency choice.
Multi-tenancy
Cloud tenancy plumbing lives in core behind SIGNALS_CLOUD /
config('signals.cloud') (false by default). There are still no tenant_id
columns in log context. When the gate is on, VerifyProxySecret then
ResolveTenant bind a request-scoped TenantContext from the edge-router
headers; self-hosted installs skip that path.