This guide covers QAuth's observability surface: structured logging, request-id tracking, auth-event logging, failed-login tracking/lockout, Prometheus metrics, and recommended alerting.
| Capability | Mechanism |
|---|---|
| Structured logging | pino (built into Fastify) with secret redaction |
| Request-id tracking | genReqId + REQUEST_ID_HEADER, echoed on responses |
| Auth-event logging | Structured authEvent log lines for login/register/logout/token |
| Failed-login tracking | Redis-backed per-identifier counters with temporary lockout |
| Metrics | GET /metrics in Prometheus text format via prom-client |
| Alerting | Prometheus Alertmanager rules (see below) |
QAuth logs through Fastify's built-in pino logger. Output is JSON by default — suitable for log shippers (Loki, Elasticsearch, Datadog, CloudWatch, ...).
LOG_LEVEL—fatal | error | warn | info | debug | trace(defaultinfo).LOG_PRETTY— whentrueandNODE_ENV != production, output is routed throughpino-prettyfor colourised, human-readable local development. Production always emits JSON.
The logger is configured with a pino redact allowlist that replaces sensitive
values with [Redacted]. Covered fields include passwords, access/refresh/ID
tokens, client secrets, OAuth codes, PKCE verifiers, Authorization headers,
and cookies — at both top-level and nested (*.field) positions. See
apps/auth-server/src/config/logger.ts (LOG_REDACT_PATHS). A regression test
(logger.test.ts) asserts that none of these values reach the log output.
Callers must still avoid passing secrets into log payloads; redaction is a defence-in-depth backstop, not a license to log credentials.
Every request gets a request id, surfaced as reqId on all of its log lines:
- An inbound
REQUEST_ID_HEADER(defaultx-request-id) is honoured for distributed tracing/propagation. - When absent, a UUID is generated (
genReqId). - The id is echoed back on the response's
REQUEST_ID_HEADERby the request-id plugin (apps/auth-server/src/app/plugins/request-id.ts), so a caller can correlate a response — and its server logs — with the request.
Login, registration, logout, and token exchange emit structured log lines via
logAuthEvent (apps/auth-server/src/app/helpers/auth-events.ts), on both
success and failure. Each line carries:
authEvent— e.g.user.login.success,user.login.failure,oauth.token.exchange.success.success,userId/clientId(when known),ip, ISOtimestamp, andreqId.- On failure paths, the email is logged as a SHA-256
emailHashrather than the raw address, to avoid account enumeration. Passwords/tokens are never logged.
These structured logs complement the durable audit_logs database trail.
Failed logins are tracked per identifier (email hash and source IP) in Redis
(apps/auth-server/src/app/helpers/failed-login.ts). After
FAILED_LOGIN_MAX_ATTEMPTS failures inside FAILED_LOGIN_WINDOW, the identifier
is locked out for FAILED_LOGIN_LOCKOUT_DURATION seconds; further login attempts
return 429 Too Many Requests with a Retry-After header before any credential
verification.
- The attempt counter has a TTL equal to the window, so it decays naturally.
- A successful login clears both the counter and any lockout.
- All cache operations are best-effort / fail-open: if Redis is unavailable, logins are never blocked by the tracker.
| Variable | Default | Description |
|---|---|---|
FAILED_LOGIN_TRACKING_ENABLED |
true |
Master switch for throttling/lockout. |
FAILED_LOGIN_MAX_ATTEMPTS |
5 |
Failures in the window before lockout. |
FAILED_LOGIN_WINDOW |
900 |
Sliding window in seconds (15 min). |
FAILED_LOGIN_LOCKOUT_DURATION |
900 |
Lockout duration in seconds (15 min). |
QAuth exposes Prometheus metrics in text exposition format at GET /metrics
(apps/auth-server/src/app/routes/metrics.ts). The endpoint includes default
process/runtime metrics plus QAuth auth counters:
| Metric | Type | Labels | Meaning |
|---|---|---|---|
qauth_login_attempts_total |
counter | result, reason |
Login outcomes (success/failure). |
qauth_tokens_issued_total |
counter | type, grant_type |
Tokens issued by type and grant. |
resultissuccessorfailure;reasonannotates failures (invalid_credentials,locked_out,error).typeisaccessorrefresh;grant_typeispassword,authorization_code,refresh_token,client_credentials, ortoken-exchange.
The endpoint is unauthenticated and rate-limit-exempt (so a scraper can poll
it frequently). Restrict access at the reverse proxy / network layer (e.g. to a
metrics subnet), or disable it entirely with METRICS_ENABLED=false.
scrape_configs:
- job_name: qauth-auth-server
metrics_path: /metrics
static_configs:
- targets: ['auth-server:3000']QAuth does not ship an alerting stack. The following Prometheus Alertmanager
rules are recommended starting points; tune thresholds to your traffic. They
assume the auth-server is also instrumented for HTTP status codes (e.g. via a
proxy exporter or http_requests_total-style metric); the auth-failure rule uses
the built-in qauth_login_attempts_total counter.
groups:
- name: qauth-auth-server
rules:
# Spike in authentication failures — possible credential-stuffing/brute force.
- alert: QAuthHighLoginFailureRate
expr: |
sum(rate(qauth_login_attempts_total{result="failure"}[5m]))
/
clamp_min(sum(rate(qauth_login_attempts_total[5m])), 1) > 0.5
for: 10m
labels:
severity: warning
annotations:
summary: 'High login-failure ratio on QAuth'
description: 'Over 50% of login attempts have failed for 10 minutes.'
# Elevated server errors (requires an HTTP status metric at the proxy/app).
- alert: QAuthHigh5xxRate
expr: |
sum(rate(http_requests_total{job="qauth-auth-server",status=~"5.."}[5m]))
/
clamp_min(sum(rate(http_requests_total{job="qauth-auth-server"}[5m])), 1) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: 'High 5xx error rate on QAuth'
description: 'More than 5% of requests are returning 5xx for 5 minutes.'
# The scrape target is down.
- alert: QAuthInstanceDown
expr: up{job="qauth-auth-server"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: 'QAuth auth-server is down'
description: 'Prometheus cannot scrape {{ $labels.instance }}.'- Add the rules above to a file referenced by
rule_files:inprometheus.yml. - Point Prometheus at Alertmanager under
alerting.alertmanagers:. - Configure Alertmanager receivers (email, Slack, PagerDuty, ...) and routing.
- Reload Prometheus (
SIGHUPorPOST /-/reload) and verify under Status → Rules and Alerts.