Bichon exposes a Prometheus/OpenMetrics endpoint for operational monitoring. It is available in every edition and requires an API token with the system:metrics permission.
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /healthz | None | Liveness: 200 ok while the process can serve HTTP, in normal operation and maintenance mode |
GET | /api/status | None | Readiness and version payload: uptime, timezone, version; 503 in maintenance mode |
GET | /api/v1/metrics | Bearer token + system:metrics | Prometheus text exposition (text/plain; version=0.0.4) |
/api/status answers 200 while the process is serving and 503 while the server is in maintenance mode, so it works as a readiness probe and container healthcheck. /healthz answers 200 in both modes and is the correct liveness probe.
Health checks
Bichon separates liveness ("should the process be restarted?") from readiness ("should it receive traffic?"). They are different endpoints and must not be swapped.
| Endpoint | Role | Normal | Maintenance mode | Auth |
|---|---|---|---|---|
/healthz | Liveness | 200 ok | 200 ok | None |
/api/status | Readiness + version payload | 200 | 503 | None |
Why two endpoints
In maintenance mode the process deliberately stays alive — a legacy data layout needs bichon-admin, a startup pre-check failed, or a Pro licence preflight was rejected — so that you can docker exec / kubectl exec into it and fix the problem. /api/status reports 503 there so load balancers and readiness probes stop sending traffic. A liveness probe must not point at /api/status: Kubernetes restarts the container when liveness fails, which would kill the process exactly when you need to exec into it. /healthz therefore answers 200 in both modes.
/healthz is a pure process-alive signal. It does not open the database, the blob store, or the search indexes, and it does not check disk space: it says "the HTTP loop can answer", not "the archive is healthy". Use /api/status, the metrics endpoint, or the UI for deeper checks.
Docker
The bundled Dockerfile healthcheck uses /api/status, so the container is reported unhealthy during maintenance mode while remaining reachable. Docker itself never restarts a container just because it is unhealthy, so this is safe and keeps docker ps honest. Keep the Docker healthcheck on /api/status; pointing it at /healthz would make maintenance mode look healthy to tools that watch container health.
Kubernetes
livenessProbe:
httpGet:
path: /healthz
port: 15630
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/status
port: 15630
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3With this pair, a pod in maintenance mode is taken out of service (readiness 503) but is not restarted (liveness 200), so kubectl exec keeps working while you run the migration or reindex.
Sub-path deployments
/healthz is mounted at the server root and is not affected by --bichon-base-url (default /), so probes keep a stable path even when the UI and API are served under a prefix such as /bichon. /api/status follows --bichon-base-url, like every other API route. In maintenance mode both endpoints are served over plain HTTP, because TLS is not initialized before the startup checks pass.
Creating a scrape token
Create an API token under Settings → API Tokens and enable metrics only. A metrics-only token:
- is accepted only on
GET /api/v1/metricsand rejected on every other endpoint; - can only be created for a user who already holds
system:metrics; - has the same optional expiry as a normal token.
A normal API token belonging to a user with system:metrics also works. Use a metrics-only token for your monitoring system so a leaked scrape credential cannot read the archive.
Example Prometheus scrape configuration:
scrape_configs:
- job_name: bichon
metrics_path: /api/v1/metrics
scheme: http
bearer_token: "<metrics-only API token>"
static_configs:
- targets: ["bichon.example.com:15630"]If the REST API is behind HTTPS or a reverse proxy, adjust scheme and targets accordingly.
Metric reference
Counters reset to zero when the process restarts; use rate() or increase() in queries and alerting. Archive totals (emails_total, attachments_total, storage/index usage) are TTL-cached and refreshed periodically rather than recomputed on every scrape.
Build and uptime
| Metric | Type | Labels | Meaning |
|---|---|---|---|
bichon_build_info | gauge | version | Always 1; version is in the label |
bichon_uptime_seconds | gauge | — | Seconds since process start |
Archive inventory
| Metric | Type | Labels | Meaning |
|---|---|---|---|
bichon_accounts_total | gauge | type | Configured accounts by type (imap, nosync, view) |
bichon_users_total | gauge | — | Configured users |
bichon_account_mailbox_folders_total | gauge | — | Tracked mailbox folders across all accounts |
bichon_emails_total | gauge | — | Archived emails |
bichon_attachments_total | gauge | — | Archived attachments |
bichon_storage_usage_bytes | gauge | — | Blob storage disk usage |
bichon_index_usage_bytes | gauge | — | Full-text index disk usage |
Per-account sync state
All of these carry an account_id label. Join them to bichon_account_info{account_id,email,name,type,enabled} to get readable names.
| Metric | Type | Meaning |
|---|---|---|
bichon_account_sync_running | gauge | 1 while a download session is running |
bichon_account_last_sync_start_timestamp_seconds | gauge | Start of the most recent session |
bichon_account_last_sync_success_timestamp_seconds | gauge | End of the most recent successful session (0 = never) |
bichon_account_last_sync_succeeded | gauge | 1 if the last finished session succeeded |
bichon_account_last_sync_duration_seconds | gauge | Duration of the last finished session |
bichon_account_last_sync_errors | gauge | Errors recorded in the last finished session |
bichon_account_emails_total | gauge | Emails archived for the account |
bichon_account_download_interval_minutes | gauge | Configured interval (0 = no sync) |
bichon_account_gapfill_running | gauge | 1 while a gap-fill run is in progress |
bichon_account_last_gapfill_downloaded | gauge | Messages downloaded by the last gap-fill run |
bichon_account_last_gapfill_failed | gauge | Messages that failed in the last gap-fill run |
Ingestion and sync
| Metric | Type | Labels | Meaning |
|---|---|---|---|
bichon_emails_ingested_total | counter | account_id, source, outcome | Ingestion outcomes: imported, filtered, duplicate, failed |
bichon_sync_errors_total | counter | account_id, kind | Errors classified as auth, throttled, network, timeout, protocol, internal |
bichon_sync_sessions_total | counter | account_id, trigger, status | Completed sessions, including cancelled ones |
bichon_sync_session_duration_seconds | histogram | account_id | Session duration |
Service-level
| Metric | Type | Labels | Meaning |
|---|---|---|---|
bichon_http_requests_total | counter | method, status | REST requests by status class (2xx … 5xx) |
bichon_http_request_duration_seconds | histogram | method | REST latency |
bichon_smtp_messages_total | counter | outcome | SMTP messages overall (accepted / rejected) |
bichon_smtp_rejections_total | counter | reason | SMTP rejections before archiving |
bichon_smtp_active_connections | gauge | — | Open SMTP connections |
bichon_backup_runs_total | counter | outcome | Backup runs (success / failed) |
bichon_backup_duration_seconds | histogram | — | Backup duration |
bichon_backup_last_success_timestamp_seconds | gauge | — | Last successful backup (0 = never) |
bichon_oauth2_token_refresh_total | counter | outcome | OAuth2 token refresh attempts |
Useful alert conditions
bichon_account_last_sync_succeeded == 0for an hour or more — a mailbox is failing to sync.time() - bichon_backup_last_success_timestamp_secondsabove your backup interval — backups have stopped succeeding.rate(bichon_sync_errors_total{kind="auth"}[15m]) > 0— credentials or OAuth refresh need attention.rate(bichon_smtp_rejections_total[15m]) > 0— rejected SMTP deliveries; check whether authentication, TLS, or the target account is misconfigured.