Monitoring & Alerting

This page is for operators of a single-host Docker Compose install. It covers what you can monitor with no extra setup, how to turn on Prometheus metrics, and how to run the optional monitoring stack with its alert rules, dashboards and tracing.

Install basics, .env.prod and the health endpoint overview are in the Deployment Guide. Run the commands below from your installation directory.

What is available out of the box

SignalWhereAccess
Health/health, /health/live, /health/readyPublic, through Nginx or on 127.0.0.1:3000
Circuit breakers/health/circuitsAdmin session or admin access token
Performance JSON/metrics, /metrics/summary, /metrics/memory, /metrics/slow, /metrics/routes, /metrics/timersAdmin only, not proxied by Nginx
Error, email and recovery historySettings > Logs in the Admin Panel, see Logs Hubsettings:view permission
Update and backup readinessSettings > License & Updates, see License & Updatesupdates:view permission
Container logsdocker compose --env-file .env.prod -f docker-compose.prod.yml logs -f appHost shell

Reading /health

curl -s https://your-domain.com/health
Field or statusMeaning
HTTP 200, "status": "ok"Database and memory checks pass
HTTP 200, "status": "degraded"The database is fine but memory is critical
HTTP 503, "status": "unhealthy"The database check failed
license.statusCurrent license state
maintenanceMaintenance mode is on
liveMigrationEnabled, draining, activeHands, drainDeadlineAtLive session migration state, see Upgrades, Rollback and Live Session Migration

Memory counts as critical only when heap usage is at least 95 percent and process RSS is at least 1024 MB. A request made as an admin also gets the database and memory detail under data.checks, plus migration and any license failure reason.

/health/ready returns 503 when the database is down, the license is not configured or the app is draining. /health/live returns 200 as long as the process can respond.

Calling admin-only endpoints from the server

Only /health, /health/live and /health/ready are exempt from the app’s HTTPS enforcement. In production, any other plain HTTP request is refused with 400 INVALID_HOST_HEADER unless it looks like it came through a proxy. To call the /metrics routes on 127.0.0.1:3000, add the header Nginx would normally set:

curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "X-Forwarded-Proto: https" \
  http://127.0.0.1:3000/metrics/summary

This relies on TRUST_PROXY=1, the value in .env.prod.example. $ADMIN_TOKEN is the accessToken returned when an admin logs in. Access tokens are short-lived.

/health/circuits goes through Nginx, so use the public URL:

curl -s -H "Authorization: Bearer $ADMIN_TOKEN" https://your-domain.com/health/circuits

Enable Prometheus metrics

GET /metrics/prometheus stays dark until you set a token. Without one it returns 404. With one it returns 401 unless the request carries the exact bearer token. Default Node.js process metrics are only collected when the token is set.

  1. Generate a token and add it to .env.prod. It must be at least 32 characters:
    openssl rand -hex 32
    METRICS_SCRAPE_TOKEN=your-generated-token
  2. Recreate the app so it reads the new value. This restarts the app, so see Before you restart the app:
    docker compose --env-file .env.prod -f docker-compose.prod.yml up -d app
  3. Test the endpoint from the server:
    curl -s -H "Authorization: Bearer $METRICS_SCRAPE_TOKEN" \ -H "X-Forwarded-Proto: https" \ http://127.0.0.1:3000/metrics/prometheus | head

Nginx does not proxy /metrics, so the endpoint is not reachable from the internet with the bundled configuration. To scrape from your own Prometheus, it must reach the app’s port and send the same two headers.

Run the monitoring stack

Getting the files

The stack is defined by docker-compose.observability.yml and the deploy/observability/ directory. They are not part of the Docker release packages. They are in the Enterprise source release. If you don’t have them, contact info@onlinepokerscript.com. Copy both into your installation directory, because the overlay uses paths relative to it.

The upgrade scripts only replace docker-compose.prod.yml, so your edits to these files survive upgrades, and the scripts never restart the monitoring containers.

What runs

ServiceImageHost addressPurpose
prometheusprom/prometheus:v2.55.1127.0.0.1:9090Scrapes metrics and evaluates alert rules every 15 seconds
alertmanagerprom/alertmanager:v0.27.0127.0.0.1:9093Routes alerts
grafanagrafana/grafana:10.4.18127.0.0.1:3001Dashboards
otel-collectorotel/opentelemetry-collector-contrib:0.116.1127.0.0.1:4317, 127.0.0.1:4318Receives traces
jaegerjaegertracing/all-in-one:1.62.0127.0.0.1:16686Stores and shows traces
postgres-exporterprometheuscommunity/postgres-exporter:v0.16.0InternalPostgreSQL metrics
redis-exporteroliver006/redis_exporter:v1.67.0InternalRedis metrics

Every published port is bound to loopback. Reach the interfaces over an SSH tunnel rather than opening them publicly:

ssh -L 3001:127.0.0.1:3001 -L 9090:127.0.0.1:9090 -L 9093:127.0.0.1:9093 -L 16686:127.0.0.1:16686 you@your-server

Setup

  1. Add these to .env.prod, alongside METRICS_SCRAPE_TOKEN. If GRAFANA_ADMIN_PASSWORD is missing, Grafana falls back to the password admin. GRAFANA_ADMIN_USER is optional and defaults to admin.
    GRAFANA_ADMIN_PASSWORD=your-generated-password
  2. Write the scrape token where Prometheus expects it. The secrets/ directory is mounted read-only, and the file must be readable by the Prometheus container, whose image runs as user nobody (UID 65534):
    mkdir -p deploy/observability/prometheus/secrets grep '^METRICS_SCRAPE_TOKEN=' .env.prod | cut -d= -f2- | tr -d '\n' > deploy/observability/prometheus/secrets/metrics-token sudo chown 65534:65534 deploy/observability/prometheus/secrets/metrics-token sudo chmod 0400 deploy/observability/prometheus/secrets/metrics-token
  3. Add the proxy header to the poker-engine job in deploy/observability/prometheus/prometheus.yml. This step is required in production. Prometheus scrapes http://app:3000 directly, so without the header the app answers 400 INVALID_HOST_HEADER, the target shows as down and BackendDown fires:
    - job_name: poker-engine metrics_path: /metrics/prometheus http_headers: X-Forwarded-Proto: values: ["https"] authorization: type: Bearer credentials_file: /etc/prometheus/secrets/metrics-token static_configs: - targets: - app:3000
  4. Change the OTLP exporter in deploy/observability/otel-collector/otel-collector.yml. The shipped file uses the logging exporter, which the collector removed in v0.111.0, so version 0.116.1 refuses to start with it. Replace it with debug in both places:
    exporters: otlp/jaeger: endpoint: jaeger:4317 tls: insecure: true debug: verbosity: basic service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlp/jaeger, debug]
  5. Replace the placeholders in deploy/observability/alertmanager/alertmanager.yml.example. Compose mounts this exact file as the live configuration, so edit it in place. See Alert routing.
  6. Apply the two alert rule corrections in Known rule corrections.
  7. Start the production stack with the overlay:
    docker compose --env-file .env.prod -f docker-compose.prod.yml -f docker-compose.observability.yml up -d
  8. Open http://127.0.0.1:9090/targets through your tunnel. The poker-engine, postgres-exporter, redis-exporter and prometheus jobs should all be up.

Use both -f files and --env-file for every command that touches the overlay. To apply later changes to Prometheus configuration or rules, reload it:

curl -X POST http://127.0.0.1:9090/-/reload

Dashboards

Grafana provisions six dashboards into the Poker Engine folder, using Prometheus as the default data source.

DashboardPanels
Poker Engine Platform OverviewBackend Up, HTTP Request Rate, HTTP Error Rate, HTTP p95 Latency, Active Realtime And Tables, Circuit Breaker State, Node Event Loop Lag p95, Heap Used
Poker Engine HTTP APIRequests By Route, Status Codes, Latency p50/p95/p99, Active Requests, Slowest Routes p95
Poker Engine WebSocket RealtimeActive Connections, Inbound Events, Outbound Events, Handler Latency p95, Disconnect Reasons, Broadcast Fanout p95
Poker Engine Game EngineHands Started / Completed, Active Tables And Seats, Hand Duration p95, Pot Size p95/p99, Action Mix And Timeouts, Showdowns, Aborts, Invariants
Poker Engine EconomyRake Collected, Deposits By Provider, Withdrawals By Provider, Balance Mutations, Ledger / Fraud / Chargebacks, Player Win Distribution
Poker Engine InfrastructurePostgres Up And App Errors, Prisma Query Latency p95, Redis App And Exporter Health, Redis Command Latency p95, Database Pool And Migrations, Node Process

What to watch

QuestionMetricLook for
Are players getting errors?pe_http_requests_totalShare of status values matching 5..
Is the API slow?pe_http_request_duration_secondsp95 above 2 seconds
Is the process saturated?nodejs_eventloop_lag_secondsSustained values above 0.2
How much is connected?pe_ws_connections_active, pe_tables_active, pe_seats_occupiedYour normal range, and sudden drops
Are connections dropping?pe_ws_disconnects_totalSpikes, broken down by reason
Are hands flowing?pe_hands_started_total, pe_hands_completed_total, pe_hands_aborted_total, pe_action_timeouts_totalCompletions stopping while seats are occupied
Is the database healthy?pe_db_errors_total, pe_db_query_duration_seconds, pg_upAny connection_* code, or p2024
Is Redis connected?pe_redis_connected, pe_redis_reconnects_totalA value of 0, or reconnects climbing
Are dependencies failing?pe_circuit_breaker_state0 is closed, 1 half-open, 2 open
Are payments working?pe_deposits_total, pe_withdrawals_total, pe_payment_provider_errors_totalFailures by provider
Is money consistent?pe_ledger_drift_detected_total, pe_gameplay_invariant_violations_totalAnything above 0
Is memory growing?nodejs_heap_size_used_bytes, process_resident_memory_bytesSteady growth toward the 2 GB app limit

pe_db_pool_connections reports the configured pool size, not live usage. To see real connection use, query PostgreSQL as shown in Database connection exhaustion.

Also keep an eye on things the metrics don’t cover: free disk space, certificate expiry, and backups/postgres/backup-status.json, which the backup service updates after each backup.

Alert rules

Rules live in deploy/observability/prometheus/rules/.

Outages (outages.yml)

AlertFires whenSeverityTeamFirst response
BackendDownThe app’s scrape target is down for 1 minutecriticalplatformCheck ps and app logs. Confirm the scrape headers if this happens straight after setup
PostgresDownpg_up is 0 or the app sees connection-class errors, for 1 minutecriticalplatformDatabase connection exhaustion
RedisDownpe_redis_connected is 0 for 1 minutecriticalplatformCheck the redis container and REDIS_PASSWORD
HighErrorRate5xx responses above 5 percent for 5 minutescriticalplatformOpen Poker Engine HTTP API, find the route, then check logs
HighLatencyHTTP p95 above 2 seconds for 10 minuteswarningplatformHigh load
CircuitBreakerOpenAny circuit is open for 2 minuteswarningplatformCheck /health/circuits and whether the dependency is really down
DatabaseCircuitBreakerOpenA circuit with database or db in its name is open for 2 minutescriticalplatformThe game-db-hot-path circuit protects game persistence. Check database health first
EventLoopLagHighEvent loop lag above 200 ms for 5 minuteswarningplatformHigh load
WebSocketMassDisconnectDisconnect rate above 3 times the one-hour rate for 5 minuteswarningplatformWebSocket issues
HandsStalledSeats are occupied but no hands complete for 10 minutescriticalgame-integrityOpen Poker Engine Game Engine, find the table in logs, and pause it in Game Management > Tables if it is stuck

Fraud and finance (fraud.yml)

AlertFires whenSeverityTeamFirst response
FraudAlertBurstMore than 5 fraud or AML alerts in 15 minuteswarningfinanceReview fraud alerts, AML reviews and recent deposits and withdrawals
CriticalFraudAlertAny critical fraud or AML alertcriticalfinanceApply your freeze policy, keep the audit evidence, review before releasing funds
ChargebackSpikeMore than 3 chargebacks in an hourwarningfinanceReview provider webhooks and the affected payment systems
WithdrawalErrorSpikeOver 20 percent of withdrawals have status failed or error for 15 minuteswarningfinancePayment failures
LedgerDriftLedger reconciliation found driftcriticalfinanceStop automated payouts, export the affected entries, keep the alert active until explained

Game integrity (anomalous-wins.yml)

AlertFires whenSeverityTeamFirst response
AnomalousWinAmountA single-hand win exceeds 1,000,000 chipswarninggame-integrityFind the hand in Game History and review the account
GameplayInvariantViolationA runtime chip, pot or payout invariant failedcriticalgame-integrityUse tableId and handNumber from the logs, and preserve the hand history before restarting
RakeAnomalyLowRake per hand drops over 50 percent against the 24-hour baseline for 30 minuteswarningfinanceCheck rake settings, table templates and private table overrides
WinRateAnomalyPot-size p99 above 5 times the 24-hour baseline for 15 minuteswarninggame-integrityRule out legitimate high-stakes play, then review accounts and tables

GameplayInvariantViolation depends on RUNTIME_INVARIANTS_ENABLED, which is on by default.

The alerts’ runbook annotations link to a private repository you can’t open. Use this page and the Operations Runbook instead.

Known rule corrections

Two shipped rules compare series with different labels, so as written they never fire. Replace their expr lines.

In rules/outages.yml, HandsStalled:

expr: sum(rate(pe_hands_completed_total[10m])) == 0 and sum(pe_seats_occupied) > 0

In rules/anomalous-wins.yml, AnomalousWinAmount:

expr: sum(increase(pe_player_win_amount_chips_bucket{le="+Inf"}[5m])) - sum(increase(pe_player_win_amount_chips_bucket{le="1000000"}[5m])) > 0

The 1,000,000 chip threshold is a fixed value. Adjust it to suit your stakes, using one of the metric’s bucket boundaries: 1000000, 5000000 or 10000000.

Alert routing

The example Alertmanager configuration routes by labels:

  • severity="critical" goes to the pager webhook, and then continues to the team routes.
  • team="finance", team="game-integrity" and team="platform" go to their own Slack receivers.
  • Anything else goes to default-email.

Replace every placeholder: the example.com addresses and SMTP host, CHANGE_ME, the pager.example.com URL and the hooks.slack.com/services/CHANGE/ME URLs. A single operator can point every receiver at the same inbox or webhook. Leave the team labels in place so you can split routing later.

Alertmanager has no connection to the SMTP settings in the Admin Panel. It needs its own mail or webhook details, and this file then holds a credential, so restrict its permissions.

Silencing

Silence an alert in Alertmanager (http://127.0.0.1:9093) only for a known maintenance window, a provider incident you are already tracking, or a planned load test. Give every silence an owner and an expiry. An upgrade stops the app, so BackendDown fires if the stop lasts more than a minute: silence it for the window. Don’t silence LedgerDrift or GameplayInvariantViolation unless the person responsible for finance or game integrity accepts the risk.

Tracing

Tracing is off by default. The Node.js SDK starts only when OTEL_ENABLED=true, and sends traces only when OTEL_EXPORTER_OTLP_ENDPOINT is set.

OTEL_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
OTEL_TRACES_SAMPLER_ARG=0.1
VariableDefaultNotes
OTEL_ENABLEDfalseMust be the exact value true
OTEL_EXPORTER_OTLP_ENDPOINTNot setThe base URL of the collector. Don’t include /v1/traces: the exporter appends that path itself, so the commented value in .env.prod.example results in a wrong URL
OTEL_TRACES_SAMPLER_ARG0.1Share of traces kept, with the parentbased_traceidratio sampler

Recreate the app after changing these. Then open Jaeger at http://127.0.0.1:16686 and choose the service poker-engine. The collector forwards traces to Jaeger. Jaeger’s all-in-one image is not given a persistent volume here, so treat traces as short-lived.

When tracing is active, application log lines include trace_id and span_id, so you can move from a log line to its trace. /health and the metrics routes behave the same with tracing on or off.

Error tracking

For exception reporting, set SENTRY_ENABLED and SENTRY_DSN. See Sentry Integration.

Try the live demo Pricing

Have questions about Poker Engine? We reply fast.