Observability — Logs, Health, Metrics
Purpose: when something goes wrong in prod, this is the map of where to look first. Scope: what exists today on the free tier. Upgrade paths to structured logging / APM are listed at the end.
TL;DR — “the site is broken”
| Symptom | First place to look |
|---|---|
| “I can’t log in” | Keycloak logs: fly logs --app flowershop-keycloak |
| “Map is empty / 500s” | API logs: fly logs --app flowershop-api + /health/ready |
| “Bouquets don’t refresh” | MQTT subscriber status: API logs → grep Mqtt |
| “Checkout fails” | Stripe Dashboard → Events, then API logs → grep stripe |
| “Admin portal won’t load” | Render dashboard → flowershop-admin-portal → Logs |
| “Vendor portal login fails” | VendorPortal logs + Keycloak flowershop-vendor-portal client settings |
| “API intermittently slow” | Aiven metrics + Fly metrics on the API app |
| “Customer app 502” | Fly logs on customer-app; usually an API cold start |
Health endpoints
Every component exposes at least one. Wire them into cron-job.org (§Stage 10 of runbook) for passive monitoring.
| Component | Endpoint | What it checks | Returns |
|---|---|---|---|
FlowerShop.API |
GET /health |
All registered checks (DB, MQTT, any future Redis/RabbitMQ) | {"status":"Healthy"} 200 / {"status":"Unhealthy"} 503 |
FlowerShop.API |
GET /health/ready |
Only checks tagged ready — used by Fly for routing decisions |
200 / 503 |
FlowerShop.API |
GET /health/live |
Liveness only — Predicate = _ => false means it always returns 200 if the process is up |
200 |
| Keycloak | GET /health/ready |
Built-in, requires KC_HEALTH_ENABLED=true (already set in fly.keycloak.toml) |
200 / 503 |
| Keycloak | GET /realms/flowershop/.well-known/openid-configuration |
App-specific — proves realm loaded | 200 with JSON |
AdminPortal / VendorPortal |
GET /health |
ASP.NET default — process liveness | 200 |
CustomerApp |
GET / |
Home page; Fly uses it as health probe | 200/302 |
Check all of them in one shell:
for h in api auth app admin vendor; do
printf "%-8s %s\n" "$h" "$(curl -s -o /dev/null -w '%{http_code}' https://$h.findmyflowers.pl/health 2>/dev/null || echo x)"
done
Logs
Fly apps — live tail
fly logs --app flowershop-api # follow
fly logs --app flowershop-api --no-tail # one shot (last ~100 lines)
Filter by grep client-side:
fly logs --app flowershop-api | grep -iE 'error|warn|stripe|mqtt'
Retention: Fly free tier keeps ~24 h of logs. For longer retention, forward with fly log-shipper (costs extra) or add Serilog sink → Seq / Loki.
Render services
- Dashboard → service → Logs tab. Streaming UI, 7 days of retention on free tier.
- CLI alternative: Render doesn’t ship a first-party CLI for logs yet; use the web UI or the REST API (
GET /v1/services/{id}/events).
Serilog structured output
The API already uses Serilog (see src/backend/FlowerShop.API/Program.cs). Fields automatically added:
RequestId— fromRequestIdMiddlewareTenantId/VendorId— fromTenantResolutionMiddleware(after auth)UserId,Role— from JWT claimsElapsed— fromUseSerilogRequestLogging()
Grep a specific request across logs:
fly logs --app flowershop-api | grep "RequestId=abc-123"
What to grep for
| What you want | grep pattern |
|---|---|
| Failed requests | "StatusCode":5\|"level":"Error" |
| Slow requests (>1s) | Elapsed=[12][0-9]{3} |
| Auth failures | 401\|AuthenticationFailed\|JwtBearer |
| MQTT ingest | MqttDevice\|vase/.*heartbeat |
| Stripe | stripe\|webhook\|payment_intent |
| DB errors | Npgsql\|EntityFramework\|RelationDoesNotExist |
| Migrations | Applied migration\|Pending migration |
Metrics
Fly built-in
Dashboard → app → Metrics. Shows CPU, memory, network, connections, HTTP status breakdown per machine. Free. 7 days retention.
Aiven PostgreSQL
Console → service → Metrics tab. Shows:
- Connections in use vs. max
- Disk usage (5 GB limit on free plan)
- Query throughput
- Replication lag (irrelevant on free tier; no replicas)
Watch: disk hitting 4 GB → start pruning old order data or upgrade plan.
Upstash Redis
Console → database → Usage. Shows commands/month against the 500k free cap.
CloudAMQP
Instance → Metrics. Shows message rate, queue depth, connection count.
HiveMQ Cloud
Cluster → Monitoring. Shows connected clients (100 cap on free), messages/hour, storage.
Cron-job.org (uptime)
Per job, shows “Last execution” and success history. Free-tier dashboards are a reasonable UptimeRobot substitute.
Tracing a request end-to-end
A customer clicks “Add to cart” → three services touch the request:
- CustomerApp (Fly) → look for the session cookie in its logs.
- API (Fly) →
fly logs --app flowershop-api | grep <request-id>—RequestIdis forwarded from CustomerApp if present, otherwise generated. - PostgreSQL (Aiven) →
pg_stat_statementsshows the query; enable it in the Aiven console under Advanced configuration.
For IoT path: vase → HiveMQ → API → SignalR → CustomerApp. Correlate by VaseSerialNumber — it’s stamped on every log line in MqttDeviceCommunicationService.
Alerting
None configured today. The cheapest stack that works:
- cron-job.org → email on 3 consecutive failures. Free. Covers uptime only.
- Cloudflare → Health Checks (one free check) → emails if an origin goes down.
- Stripe → Dashboard → Developers → Webhooks → failing deliveries trigger email automatically.
- Aiven → service → Integrations → “Email notifications” → alerts on disk/CPU thresholds.
For anything richer (paging, PagerDuty, Slack webhooks) you need one of:
- BetterStack / Logtail (free tier with 1 GB logs/mo) — Serilog sink available
- Sentry (5k events/mo free) —
Sentry.AspNetCoreNuGet, one-line wire-up inProgram.cs - Seq self-hosted on Fly — ~$2/mo extra, full-text log search
Pick one when on-call becomes a real thing. Before then, cron-job.org + email is adequate.
Known noisy log lines (safe to ignore)
| Pattern | Source | Why it’s safe |
|---|---|---|
HealthReport with status Healthy (every 15s) |
Fly probes /health/ready |
Just liveness; mute in Serilog filter if annoying |
Keycloak: realm 'master' - Password policy not configured |
Keycloak boot | master realm unused in prod |
Idempotency key not found, proceeding |
IdempotencyMiddleware on GETs |
GETs don’t need idempotency; check only on mutations |
InMemoryEventBus: Published <event> |
Events system (EP-09 pending) | Stubbed — replace when RabbitMQ is real |
Upgrade path when the above isn’t enough
- Structured logs off-site: add a Serilog sink to BetterStack (free tier) — 10 min of work, immediate searchable history.
- APM: Sentry Performance (free tier) or Datadog free trial. Wire via middleware; catches slow queries and exceptions with stack traces.
- Distributed tracing: OpenTelemetry → Grafana Cloud (free 50 GB traces). Requires instrumentation changes, but correlates Stripe/DB/MQTT calls with UI requests.
- Synthetic checks: Checkly or UptimeRobot Pro for multi-step journeys (login → checkout). Replaces manual §10 of POST-DEPLOY-SMOKE-TESTS.md.
Estimated upgrade cost to “production-grade observability”: $0–25/mo depending on retention.