Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ To work on the service without Docker, Rust is required:
env ROCKET_CONFIG=conf/config.dev.toml cargo run
```

## Monitoring

Cryptify exposes Prometheus metrics at `GET /metrics` for scraping from the
monitoring network. See [`docs/grafana/`](docs/grafana/) for the reference
dashboard JSON and the Prometheus scrape configuration.

The endpoint is unauthenticated — restrict access at the firewall or reverse
proxy.

## Releasing

Releases are automated with [release-plz](https://release-plz.ieni.dev/). Merging to `main` triggers a release, and Docker images are published automatically.
Expand Down
33 changes: 33 additions & 0 deletions api-description.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ servers:
tags:
- name: "Health"
description: "Health check"
- name: "Metrics"
description: "Prometheus scrape endpoint"
- name: "File upload"
description: "Upload files"
- name: "File download"
Expand All @@ -30,6 +32,37 @@ paths:
schema:
type: "string"
example: "OK"
/metrics:
get:
tags:
- "Metrics"
summary: "Prometheus text-format metrics"
description: |
Returns usage counters and gauges suitable for Prometheus scraping
by the Grafana instance on Scaleway. Intended to be reachable only
from the internal monitoring network; firewall or reverse-proxy
allow-list in front of Cryptify.

Exposed metrics:
* `cryptify_uploads_total{channel}` — counter of finalized uploads.
* `cryptify_upload_bytes_total{channel}` — counter of bytes.
* `cryptify_storage_bytes` — gauge, current disk usage.
* `cryptify_active_files` — gauge, current file count.
* `cryptify_expired_files_total` — counter of uploads purged
before finalization.

The `channel` label is derived from the `X-Cryptify-Source` header,
falling back to `Authorization`/`X-Api-Key` (→ `api`), then the
`Origin` header (`website` / `staging-website`), then `User-Agent`
(`outlook` / `thunderbird`), then `unknown`.
operationId: "metrics"
responses:
"200":
description: "Prometheus text exposition format"
content:
text/plain:
schema:
type: "string"
/fileupload/init:
post:
tags:
Expand Down
67 changes: 67 additions & 0 deletions docs/grafana/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Grafana dashboards

This directory ships dashboard JSON files intended to be imported into the
Scaleway Grafana instance that monitors PostGuard.

Files:

- `postguard-usage.json` — covers everything issue
[encryption4all/cryptify#101](https://github.com/encryption4all/cryptify/issues/101)
asks for: messages sent per channel (website / staging / Outlook /
Thunderbird / API) and Cryptify storage usage for staging vs. Procolix
production.

## Metrics source

Dashboards query Prometheus metrics exposed by Cryptify at `GET /metrics`.
The scrape target must be configured in Prometheus with two labels that the
dashboards rely on:

- `instance` — the hostname of the Cryptify instance
- `environment` — `staging` or `production`

Example Prometheus scrape config:

```yaml
scrape_configs:
- job_name: cryptify
metrics_path: /metrics
static_configs:
- targets: ['cryptify-staging.postguard.eu:8000']
labels:
environment: staging
- targets: ['cryptify.postguard.eu:8000']
labels:
environment: production
```

The `/metrics` endpoint is unauthenticated — restrict access to the Prometheus
network segment via firewall or reverse-proxy allow-list.

## Channel label

The `channel` label on upload counters is derived inside Cryptify from the
request headers (in order of priority):

1. `X-Cryptify-Source` (explicit) — set by the Outlook and Thunderbird addons
once their follow-up PRs land. Expected values: `outlook`, `thunderbird`,
`api`.
2. `Authorization: Bearer ...` or `X-Api-Key` — labelled `api`.
3. `Origin` — `staging.postguard.*` → `staging-website`, any other
`postguard.*` → `website`.
4. `User-Agent` substrings — `outlook` / `thunderbird`.
5. Fallback — `unknown`.

## Importing

1. In Grafana, navigate to **Dashboards → Import**.
2. Upload `postguard-usage.json` or paste its contents.
3. Select the Prometheus datasource that scrapes Cryptify.

## Follow-up work

- Outlook addon: send `X-Cryptify-Source: outlook` on every upload request
(via the SDK wrapper it uses to talk to Cryptify).
- Thunderbird addon: same with `thunderbird`.
- Until those land, requests from the addons fall back to the `User-Agent`
rule, which is approximate but functional.
130 changes: 130 additions & 0 deletions docs/grafana/postguard-usage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
{
"title": "PostGuard Usage",
"uid": "postguard-usage",
"schemaVersion": 39,
"timezone": "browser",
"refresh": "1m",
"time": { "from": "now-7d", "to": "now" },
"tags": ["postguard", "cryptify"],
"templating": {
"list": [
{
"name": "environment",
"type": "query",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"query": "label_values(cryptify_uploads_total, environment)",
"includeAll": true,
"multi": true,
"refresh": 2
}
]
},
"panels": [
{
"id": 1,
"type": "timeseries",
"title": "Messages sent per channel (per hour)",
"description": "Rate of finalized Cryptify uploads, split by source channel.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 0 },
"fieldConfig": { "defaults": { "unit": "short" } },
"options": { "legend": { "displayMode": "table", "placement": "right", "calcs": ["sum"] } },
"targets": [
{
"refId": "A",
"expr": "sum by (channel) (increase(cryptify_uploads_total{environment=~\"$environment\"}[1h]))",
"legendFormat": "{{channel}}"
}
]
},
{
"id": 2,
"type": "stat",
"title": "Total messages (selected window)",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 6, "w": 6, "x": 0, "y": 9 },
"fieldConfig": { "defaults": { "unit": "short", "decimals": 0 } },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"textMode": "value_and_name",
"colorMode": "value"
},
"targets": [
{
"refId": "A",
"expr": "sum by (channel) (increase(cryptify_uploads_total{environment=~\"$environment\"}[$__range]))",
"legendFormat": "{{channel}}"
}
]
},
{
"id": 3,
"type": "stat",
"title": "Bytes uploaded (selected window)",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 6, "w": 6, "x": 6, "y": 9 },
"fieldConfig": { "defaults": { "unit": "decbytes" } },
"options": {
"reduceOptions": { "calcs": ["lastNotNull"] },
"textMode": "value_and_name",
"colorMode": "value"
},
"targets": [
{
"refId": "A",
"expr": "sum by (channel) (increase(cryptify_upload_bytes_total{environment=~\"$environment\"}[$__range]))",
"legendFormat": "{{channel}}"
}
]
},
{
"id": 4,
"type": "timeseries",
"title": "Cryptify storage usage",
"description": "Bytes currently held on disk per environment.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 15 },
"fieldConfig": { "defaults": { "unit": "decbytes" } },
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
"targets": [
{
"refId": "A",
"expr": "cryptify_storage_bytes{environment=~\"$environment\"}",
"legendFormat": "{{environment}} ({{instance}})"
}
]
},
{
"id": 5,
"type": "timeseries",
"title": "Cryptify active file count",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 15 },
"fieldConfig": { "defaults": { "unit": "short" } },
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
"targets": [
{
"refId": "A",
"expr": "cryptify_active_files{environment=~\"$environment\"}",
"legendFormat": "{{environment}} ({{instance}})"
}
]
},
{
"id": 6,
"type": "timeseries",
"title": "Expired (un-finalized) uploads",
"description": "Rate of uploads that expired before finalization — should remain low. Spikes can indicate a broken client or slow network.",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 24 },
"fieldConfig": { "defaults": { "unit": "short" } },
"targets": [
{
"refId": "A",
"expr": "sum by (environment) (increase(cryptify_expired_files_total{environment=~\"$environment\"}[1h]))",
"legendFormat": "{{environment}}"
}
]
}
]
}
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub struct RawCryptifyConfig {
smtp_tls: Option<bool>,
allowed_origins: String,
pkg_url: String,
metrics_scan_interval_secs: Option<u64>,
chunk_size: Option<u64>,
}

Expand All @@ -28,6 +29,7 @@ pub struct CryptifyConfig {
smtp_tls: bool,
allowed_origins: String,
pkg_url: String,
metrics_scan_interval_secs: u64,
chunk_size: u64,
}

Expand All @@ -47,6 +49,7 @@ impl From<RawCryptifyConfig> for CryptifyConfig {
smtp_tls: config.smtp_tls.unwrap_or(true),
allowed_origins: config.allowed_origins,
pkg_url: config.pkg_url,
metrics_scan_interval_secs: config.metrics_scan_interval_secs.unwrap_or(60),
chunk_size: config.chunk_size.unwrap_or(5_000_000),
}
}
Expand Down Expand Up @@ -93,6 +96,10 @@ impl CryptifyConfig {
&self.pkg_url
}

pub fn metrics_scan_interval_secs(&self) -> u64 {
self.metrics_scan_interval_secs
}

pub fn chunk_size(&self) -> u64 {
self.chunk_size
}
Expand Down
Loading