diff --git a/images/proton-bridge/Dockerfile b/images/proton-bridge/Dockerfile index c43e4b2..e6f763b 100644 --- a/images/proton-bridge/Dockerfile +++ b/images/proton-bridge/Dockerfile @@ -55,11 +55,13 @@ ARG ENV_BRIDGE_HOST=127.0.0.1 # Change ENV_CONTAINER_SMTP_PORT only if you have a docker port conflict on host network namespace. ARG ENV_CONTAINER_SMTP_PORT=1026 ARG ENV_CONTAINER_IMAP_PORT=1144 +ARG ENV_CONTAINER_METRICS_PORT=9154 ENV PROTON_BRIDGE_SMTP_PORT=$ENV_BRIDGE_SMTP_PORT \ PROTON_BRIDGE_IMAP_PORT=$ENV_BRIDGE_IMAP_PORT \ PROTON_BRIDGE_HOST=$ENV_BRIDGE_HOST \ CONTAINER_SMTP_PORT=$ENV_CONTAINER_SMTP_PORT \ CONTAINER_IMAP_PORT=$ENV_CONTAINER_IMAP_PORT \ + CONTAINER_METRICS_PORT=$ENV_CONTAINER_METRICS_PORT \ ENV_TARGET_PLATFORM=$TARGETPLATFORM # Install dependencies @@ -93,6 +95,8 @@ COPY --from=build /build/proton-bridge/bridge /build/proton-bridge/proton-bridge WORKDIR /app/ COPY --chmod=0755 entrypoint.sh /app/entrypoint.sh COPY --chmod=0755 healthcheck.sh /app/healthcheck.sh +COPY --chmod=0755 metrics/metrics.cgi /app/metrics/metrics.cgi +COPY --chmod=0755 metrics/serve-http.sh /app/metrics/serve-http.sh COPY GPGparams.txt /app/GPGparams.txt COPY LICENSE /app/licenses/LICENSE COPY NOTICE /app/licenses/NOTICE @@ -105,6 +109,8 @@ COPY --chmod=0755 services/socat-smtp/run /app/services/socat-smtp/run COPY --chmod=0755 services/socat-smtp/finish /app/services/socat-smtp/finish COPY --chmod=0755 services/socat-imap/run /app/services/socat-imap/run COPY --chmod=0755 services/socat-imap/finish /app/services/socat-imap/finish +COPY --chmod=0755 services/metrics/run /app/services/metrics/run +COPY --chmod=0755 services/metrics/finish /app/services/metrics/finish COPY --from=build /build/BRIDGE_VERSION /app/VERSION RUN chown -R bridge:bridge /app diff --git a/images/proton-bridge/README.md b/images/proton-bridge/README.md index 8d1f2bb..db32e73 100644 --- a/images/proton-bridge/README.md +++ b/images/proton-bridge/README.md @@ -10,6 +10,7 @@ The container runs these long-lived services under `s6`: - `gpg-agent` (launched and monitored via `gpgconf`) - SMTP forwarder (`socat` on `${CONTAINER_SMTP_PORT}` -> `${PROTON_BRIDGE_HOST}:${PROTON_BRIDGE_SMTP_PORT}`) - IMAP forwarder (`socat` on `${CONTAINER_IMAP_PORT}` -> `${PROTON_BRIDGE_HOST}:${PROTON_BRIDGE_IMAP_PORT}`) +- metrics endpoint (`socat` listener on `${CONTAINER_METRICS_PORT}` serving `/cgi-bin/metrics`) Bootstrap-only initialization in `entrypoint.sh`: @@ -25,6 +26,7 @@ Required: - `PROTON_BRIDGE_HOST` - `CONTAINER_SMTP_PORT` - `CONTAINER_IMAP_PORT` +- `CONTAINER_METRICS_PORT` Optional runtime tuning: @@ -83,6 +85,30 @@ The container `HEALTHCHECK` script validates: - `gpg-agent` control socket responsiveness (`gpg-connect-agent /bye`) - listening state for SMTP/IMAP container ports - lightweight SMTP and IMAP banner-level handshake probes on local forwarded ports +- metrics endpoint HTTP readiness (`/metrics`) + +## Metrics + +The image now exposes Prometheus-formatted metrics from inside the container. + +- listen port: `${CONTAINER_METRICS_PORT}` (default `9154`) +- scrape path: `/metrics` (`/cgi-bin/metrics` also supported) + +Current metrics include: + +- `proton_bridge_service_up{service=...}` +- `proton_bridge_service_restart_count{service=...}` +- `proton_bridge_service_start_time_seconds{service=...}` +- `proton_bridge_gpg_agent_up` +- `proton_bridge_port_listening{listener=...,port=...}` +- `proton_bridge_smtp_banner_probe_up` +- `proton_bridge_imap_banner_probe_up` +- `proton_bridge_pass_entry_count` + +Limitations: + +- Proton Bridge does not currently expose a stable machine API for per-account sync status in this image mode. +- You can scrape transport/process readiness today; account-level sync state likely requires either upstream Bridge support or a dedicated log/CLI parser sidecar. ## Build Supply Chain diff --git a/images/proton-bridge/entrypoint.sh b/images/proton-bridge/entrypoint.sh index 83e89f2..9307846 100644 --- a/images/proton-bridge/entrypoint.sh +++ b/images/proton-bridge/entrypoint.sh @@ -15,7 +15,8 @@ for required_var in \ PROTON_BRIDGE_IMAP_PORT \ PROTON_BRIDGE_HOST \ CONTAINER_SMTP_PORT \ - CONTAINER_IMAP_PORT + CONTAINER_IMAP_PORT \ + CONTAINER_METRICS_PORT do require_env "${required_var}" done diff --git a/images/proton-bridge/healthcheck.sh b/images/proton-bridge/healthcheck.sh index 2dd45a6..16b35bb 100644 --- a/images/proton-bridge/healthcheck.sh +++ b/images/proton-bridge/healthcheck.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh set -eu -for service in bridge gpg-agent socat-smtp socat-imap; do +for service in bridge gpg-agent socat-smtp socat-imap metrics; do s6-svstat "/app/services/${service}" | grep -q '^up ' done @@ -9,6 +9,10 @@ gpg-connect-agent /bye >/dev/null 2>&1 netstat -ltn 2>/dev/null | grep -q "[.:]${CONTAINER_SMTP_PORT}[[:space:]]" netstat -ltn 2>/dev/null | grep -q "[.:]${CONTAINER_IMAP_PORT}[[:space:]]" +netstat -ltn 2>/dev/null | grep -q "[.:]${CONTAINER_METRICS_PORT}[[:space:]]" printf 'QUIT\r\n' | nc -w 3 127.0.0.1 "${CONTAINER_SMTP_PORT}" | grep -Eq '^220 ' printf 'a1 LOGOUT\r\n' | nc -w 3 127.0.0.1 "${CONTAINER_IMAP_PORT}" | grep -Eq '^\* (OK|PREAUTH|BYE) ' +metrics_resp="$(printf 'GET /metrics HTTP/1.0\r\nHost: localhost\r\n\r\n' \ + | nc -w 3 127.0.0.1 "${CONTAINER_METRICS_PORT}")" +printf '%s\n' "${metrics_resp}" | grep -Eq '^HTTP/[0-9.]+ 200 ' diff --git a/images/proton-bridge/metrics/metrics.cgi b/images/proton-bridge/metrics/metrics.cgi new file mode 100644 index 0000000..d6314dd --- /dev/null +++ b/images/proton-bridge/metrics/metrics.cgi @@ -0,0 +1,137 @@ +#!/bin/sh +set -eu + +state_dir="/tmp/proton-bridge-s6" + +metric_bool() { + name="$1" + value="$2" + printf '%s %s\n' "${name}" "${value}" +} + +service_up() { + service="$1" + if s6-svstat "/app/services/${service}" 2>/dev/null | grep -q '^up '; then + value=1 + else + value=0 + fi + printf 'proton_bridge_service_up{service="%s"} %s\n' "${service}" "${value}" +} + +service_restart_count() { + service="$1" + state_file="${state_dir}/${service}.state" + count=0 + if [ -f "${state_file}" ]; then + count="$(cat "${state_file}" 2>/dev/null || echo 0)" + fi + printf 'proton_bridge_service_restart_count{service="%s"} %s\n' "${service}" "${count}" +} + +service_start_time() { + service="$1" + start_file="${state_dir}/${service}.start" + if [ -f "${start_file}" ]; then + started_at="$(cat "${start_file}" 2>/dev/null || echo 0)" + else + started_at=0 + fi + printf 'proton_bridge_service_start_time_seconds{service="%s"} %s\n' "${service}" "${started_at}" +} + +port_listening() { + name="$1" + port="$2" + if netstat -ltn 2>/dev/null | grep -q "[.:]${port}[[:space:]]"; then + value=1 + else + value=0 + fi + printf 'proton_bridge_port_listening{listener="%s",port="%s"} %s\n' "${name}" "${port}" "${value}" +} + +smtp_probe_up() { + if printf 'QUIT\r\n' | nc -w 3 127.0.0.1 "${CONTAINER_SMTP_PORT}" 2>/dev/null | grep -Eq '^220 '; then + value=1 + else + value=0 + fi + metric_bool proton_bridge_smtp_banner_probe_up "${value}" +} + +imap_probe_up() { + if printf 'a1 LOGOUT\r\n' | nc -w 3 127.0.0.1 "${CONTAINER_IMAP_PORT}" 2>/dev/null | grep -Eq '^\* (OK|PREAUTH|BYE) '; then + value=1 + else + value=0 + fi + metric_bool proton_bridge_imap_banner_probe_up "${value}" +} + +gpg_agent_up() { + if gpg-connect-agent /bye >/dev/null 2>&1; then + value=1 + else + value=0 + fi + metric_bool proton_bridge_gpg_agent_up "${value}" +} + +pass_entry_count() { + count=0 + if [ -d "${HOME}/.password-store" ]; then + count="$(find "${HOME}/.password-store" -type f -name '*.gpg' 2>/dev/null | wc -l | tr -d ' ')" + fi + metric_bool proton_bridge_pass_entry_count "${count}" +} + +print_metrics() { + printf '# HELP proton_bridge_service_up Whether an s6 service is currently up (1) or down (0).\n' + printf '# TYPE proton_bridge_service_up gauge\n' + service_up bridge + service_up gpg-agent + service_up socat-smtp + service_up socat-imap + service_up metrics + + printf '# HELP proton_bridge_service_restart_count Number of rapid restarts seen by the s6 finish policy per service.\n' + printf '# TYPE proton_bridge_service_restart_count gauge\n' + service_restart_count bridge + service_restart_count gpg-agent + service_restart_count socat-smtp + service_restart_count socat-imap + service_restart_count metrics + + printf '# HELP proton_bridge_service_start_time_seconds Last recorded service start time as Unix epoch seconds.\n' + printf '# TYPE proton_bridge_service_start_time_seconds gauge\n' + service_start_time bridge + service_start_time gpg-agent + service_start_time socat-smtp + service_start_time socat-imap + service_start_time metrics + + printf '# HELP proton_bridge_gpg_agent_up Whether gpg-agent responds to gpg-connect-agent.\n' + printf '# TYPE proton_bridge_gpg_agent_up gauge\n' + gpg_agent_up + + printf '# HELP proton_bridge_port_listening Whether the expected listener port is open inside the container.\n' + printf '# TYPE proton_bridge_port_listening gauge\n' + port_listening smtp "${CONTAINER_SMTP_PORT}" + port_listening imap "${CONTAINER_IMAP_PORT}" + port_listening metrics "${CONTAINER_METRICS_PORT}" + + printf '# HELP proton_bridge_smtp_banner_probe_up Whether SMTP banner probe on localhost succeeds.\n' + printf '# TYPE proton_bridge_smtp_banner_probe_up gauge\n' + smtp_probe_up + + printf '# HELP proton_bridge_imap_banner_probe_up Whether IMAP banner probe on localhost succeeds.\n' + printf '# TYPE proton_bridge_imap_banner_probe_up gauge\n' + imap_probe_up + + printf '# HELP proton_bridge_pass_entry_count Number of pass entries in the mounted bridge password store.\n' + printf '# TYPE proton_bridge_pass_entry_count gauge\n' + pass_entry_count +} + +print_metrics diff --git a/images/proton-bridge/metrics/serve-http.sh b/images/proton-bridge/metrics/serve-http.sh new file mode 100644 index 0000000..bca2343 --- /dev/null +++ b/images/proton-bridge/metrics/serve-http.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -eu + +method="" +path="" +protocol="" +IFS=' ' read -r method path protocol || true + +while IFS= read -r header; do + case "${header}" in + ''|$'\r') + break + ;; + esac +done + +if [ "${method}" = "GET" ] && { [ "${path}" = "/metrics" ] || [ "${path}" = "/cgi-bin/metrics" ]; }; then + printf 'HTTP/1.1 200 OK\r\n' + printf 'Content-Type: text/plain; version=0.0.4\r\n' + printf 'Connection: close\r\n' + printf '\r\n' + exec /app/metrics/metrics.cgi +fi + +printf 'HTTP/1.1 404 Not Found\r\n' +printf 'Content-Type: text/plain\r\n' +printf 'Connection: close\r\n' +printf '\r\n' +printf 'not found\n' diff --git a/images/proton-bridge/services/metrics/finish b/images/proton-bridge/services/metrics/finish new file mode 100644 index 0000000..7a2361d --- /dev/null +++ b/images/proton-bridge/services/metrics/finish @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu + +exec /app/services/.s6-finish-policy.sh "metrics" "false" "${1:-0}" "${2:-0}" diff --git a/images/proton-bridge/services/metrics/run b/images/proton-bridge/services/metrics/run new file mode 100644 index 0000000..522b245 --- /dev/null +++ b/images/proton-bridge/services/metrics/run @@ -0,0 +1,10 @@ +#!/bin/sh +set -eu + +state_dir="/tmp/proton-bridge-s6" +mkdir -p "${state_dir}" +date +%s > "${state_dir}/metrics.start" + +exec socat \ + TCP-LISTEN:"${CONTAINER_METRICS_PORT}",fork,reuseaddr \ + SYSTEM:"/app/metrics/serve-http.sh"