feat: add verified operations tooling
This commit is contained in:
parent
945c060006
commit
4d5efea419
|
|
@ -36,6 +36,7 @@ POOL_SIZE=10
|
||||||
SECRET_KEY_BASE=generate-with-mix-phx-gen-secret
|
SECRET_KEY_BASE=generate-with-mix-phx-gen-secret
|
||||||
HANDOVER_SECRET=generate-an-independent-random-secret
|
HANDOVER_SECRET=generate-an-independent-random-secret
|
||||||
RELEASE_COOKIE=generate-an-independent-beam-cluster-cookie
|
RELEASE_COOKIE=generate-an-independent-beam-cluster-cookie
|
||||||
|
METRICS_TOKEN=generate-an-independent-random-bearer-token
|
||||||
|
|
||||||
# Mailpit settings for local development. Replace these with the selected SMTP
|
# Mailpit settings for local development. Replace these with the selected SMTP
|
||||||
# provider for public registration and magic links.
|
# provider for public registration and magic links.
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ RUN mix release
|
||||||
FROM ${RUNNER_IMAGE} AS final
|
FROM ${RUNNER_IMAGE} AS final
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 locales ca-certificates curl \
|
&& apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 libsctp1 locales ca-certificates curl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Set the locale
|
# Set the locale
|
||||||
|
|
|
||||||
20
README.md
20
README.md
|
|
@ -84,6 +84,17 @@ docker compose -p who_need_help logs -f web worker
|
||||||
./scripts/verify-realtime-cluster.sh compose
|
./scripts/verify-realtime-cluster.sh compose
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Create and restore-test a database backup without restoring over the source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
backup_output=$(./scripts/backup-compose.sh)
|
||||||
|
backup_path=$(printf '%s\n' "$backup_output" | sed -n 's/^Backup: //p')
|
||||||
|
./scripts/restore-drill-compose.sh "$backup_path"
|
||||||
|
```
|
||||||
|
|
||||||
|
The commands, boundaries, and unclaimed production properties are documented
|
||||||
|
in [the operations runbook](docs/operations.md).
|
||||||
|
|
||||||
Local defaults are intentionally limited to local development. Copy
|
Local defaults are intentionally limited to local development. Copy
|
||||||
`.env.example` to `.env` and replace every secret before any public deployment.
|
`.env.example` to `.env` and replace every secret before any public deployment.
|
||||||
Generate independent values with `mix phx.gen.secret`; do not reuse the session
|
Generate independent values with `mix phx.gen.secret`; do not reuse the session
|
||||||
|
|
@ -189,10 +200,10 @@ credential fields.
|
||||||
For an external cluster, provide a real PostgreSQL/PostGIS service and a
|
For an external cluster, provide a real PostgreSQL/PostGIS service and a
|
||||||
pre-created Secret through required `existingSecret`; the chart never renders
|
pre-created Secret through required `existingSecret`; the chart never renders
|
||||||
credentials from tracked values. The Secret must contain `DATABASE_URL`,
|
credentials from tracked values. The Secret must contain `DATABASE_URL`,
|
||||||
`SECRET_KEY_BASE`, `HANDOVER_SECRET`, and `RELEASE_COOKIE`, and may additionally
|
`SECRET_KEY_BASE`, `HANDOVER_SECRET`, `RELEASE_COOKIE`, and `METRICS_TOKEN`, and
|
||||||
contain both GitHub OAuth variables described above. The chart intentionally
|
may additionally contain both GitHub OAuth variables described above. The chart
|
||||||
has no invented CPU/RAM limits or HPA thresholds; measure this application in
|
intentionally has no invented CPU/RAM limits or HPA thresholds; measure this
|
||||||
the target environment before setting them.
|
application in the target environment before setting them.
|
||||||
|
|
||||||
## Local Codex category review
|
## Local Codex category review
|
||||||
|
|
||||||
|
|
@ -220,6 +231,7 @@ and no fallback provider. Recommendations require a human moderator action.
|
||||||
- [Product specification](docs/product-spec.md)
|
- [Product specification](docs/product-spec.md)
|
||||||
- [Architecture](docs/architecture.md)
|
- [Architecture](docs/architecture.md)
|
||||||
- [Trust and safety](docs/trust-safety.md)
|
- [Trust and safety](docs/trust-safety.md)
|
||||||
|
- [Operations runbook](docs/operations.md)
|
||||||
- [Implementation verification and known limits](docs/verification.md)
|
- [Implementation verification and known limits](docs/verification.md)
|
||||||
- [Verified dependency baseline](docs/dependency-baseline.md)
|
- [Verified dependency baseline](docs/dependency-baseline.md)
|
||||||
- [PostgreSQL/PostGIS ADR](docs/decisions/0001-postgresql-postgis-over-spacetimedb.md)
|
- [PostgreSQL/PostGIS ADR](docs/decisions/0001-postgresql-postgis-over-spacetimedb.md)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ x-app-environment: &app-environment
|
||||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE:?Set SECRET_KEY_BASE in .env}
|
SECRET_KEY_BASE: ${SECRET_KEY_BASE:?Set SECRET_KEY_BASE in .env}
|
||||||
HANDOVER_SECRET: ${HANDOVER_SECRET:?Set HANDOVER_SECRET in .env}
|
HANDOVER_SECRET: ${HANDOVER_SECRET:?Set HANDOVER_SECRET in .env}
|
||||||
RELEASE_COOKIE: ${RELEASE_COOKIE:?Set RELEASE_COOKIE in .env}
|
RELEASE_COOKIE: ${RELEASE_COOKIE:?Set RELEASE_COOKIE in .env}
|
||||||
|
METRICS_TOKEN: ${METRICS_TOKEN:?Set an independent METRICS_TOKEN in .env}
|
||||||
DNS_CLUSTER_QUERY: web
|
DNS_CLUSTER_QUERY: web
|
||||||
PHX_HOST: ${PHX_HOST:?Set PHX_HOST in .env}
|
PHX_HOST: ${PHX_HOST:?Set PHX_HOST in .env}
|
||||||
PHX_SCHEME: ${PHX_SCHEME:?Set PHX_SCHEME in .env}
|
PHX_SCHEME: ${PHX_SCHEME:?Set PHX_SCHEME in .env}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,21 @@ github_oauth =
|
||||||
|
|
||||||
config :who_need_help, :social_oauth, github_oauth
|
config :who_need_help, :social_oauth, github_oauth
|
||||||
|
|
||||||
|
if config_env() == :prod and app_role == :web do
|
||||||
|
metrics_token =
|
||||||
|
System.get_env("METRICS_TOKEN") ||
|
||||||
|
raise """
|
||||||
|
environment variable METRICS_TOKEN is missing for the web role.
|
||||||
|
Generate an independent random value and store it in the deployment secret.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if metrics_token == "" do
|
||||||
|
raise "METRICS_TOKEN must not be empty for the web role."
|
||||||
|
end
|
||||||
|
|
||||||
|
config :who_need_help, :metrics_token, metrics_token
|
||||||
|
end
|
||||||
|
|
||||||
if dns_query = System.get_env("DNS_CLUSTER_QUERY") do
|
if dns_query = System.get_env("DNS_CLUSTER_QUERY") do
|
||||||
config :who_need_help, :dns_cluster_query, dns_query
|
config :who_need_help, :dns_cluster_query, dns_query
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ config :who_need_help, WhoNeedHelpWeb.Endpoint,
|
||||||
# In test we don't send emails
|
# In test we don't send emails
|
||||||
config :who_need_help, WhoNeedHelp.Mailer, adapter: Swoosh.Adapters.Test
|
config :who_need_help, WhoNeedHelp.Mailer, adapter: Swoosh.Adapters.Test
|
||||||
config :who_need_help, :social_oauth_adapter, WhoNeedHelp.SocialOAuthFake
|
config :who_need_help, :social_oauth_adapter, WhoNeedHelp.SocialOAuthFake
|
||||||
|
config :who_need_help, :metrics_token, "test-metrics-token"
|
||||||
|
|
||||||
# Disable swoosh api client as it is only required for production adapters
|
# Disable swoosh api client as it is only required for production adapters
|
||||||
config :swoosh, :api_client, false
|
config :swoosh, :api_client, false
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ app:
|
||||||
smtpPort: "1025"
|
smtpPort: "1025"
|
||||||
|
|
||||||
# Required. The Secret must contain DATABASE_URL, SECRET_KEY_BASE,
|
# Required. The Secret must contain DATABASE_URL, SECRET_KEY_BASE,
|
||||||
# HANDOVER_SECRET, and RELEASE_COOKIE. It may also contain both
|
# HANDOVER_SECRET, RELEASE_COOKIE, and METRICS_TOKEN. It may also contain both
|
||||||
# GITHUB_OAUTH_CLIENT_ID and GITHUB_OAUTH_CLIENT_SECRET to enable verified
|
# GITHUB_OAUTH_CLIENT_ID and GITHUB_OAUTH_CLIENT_SECRET to enable verified
|
||||||
# GitHub linking.
|
# GitHub linking.
|
||||||
existingSecret: ""
|
existingSecret: ""
|
||||||
|
|
|
||||||
111
docs/operations.md
Normal file
111
docs/operations.md
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
# Operations runbook
|
||||||
|
|
||||||
|
This runbook describes the commands that are implemented and verified in this
|
||||||
|
repository. It does not claim a production recovery point objective, recovery
|
||||||
|
time objective, retention period, storage capacity, or high-availability model;
|
||||||
|
those values require product policy and measurements from the eventual
|
||||||
|
production environment.
|
||||||
|
|
||||||
|
## Compose database backup
|
||||||
|
|
||||||
|
Create a PostgreSQL 18 custom-format archive, validate its table of contents,
|
||||||
|
and write a SHA-256 manifest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/backup-compose.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The default destination is the ignored `output/backups/` directory. An explicit
|
||||||
|
new destination may be supplied as the only argument. The command refuses to
|
||||||
|
overwrite either an archive or its checksum manifest and writes through
|
||||||
|
temporary files before publishing the final pair. Files and newly created
|
||||||
|
directories are restricted by `umask 077`.
|
||||||
|
|
||||||
|
The archive covers the configured application database. PostgreSQL cluster
|
||||||
|
globals such as roles and tablespaces are not part of `pg_dump`; deployment
|
||||||
|
credentials and database roles must be provisioned separately from secrets.
|
||||||
|
Local backup files on the same workstation are not an off-site backup.
|
||||||
|
|
||||||
|
## Isolated restore drill
|
||||||
|
|
||||||
|
Run a real restore into a uniquely named temporary database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/restore-drill-compose.sh output/backups/compose-YYYYMMDD-HHMMSS.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
The drill:
|
||||||
|
|
||||||
|
1. validates the SHA-256 manifest;
|
||||||
|
2. validates the archive table of contents;
|
||||||
|
3. creates a pristine database from `template0`;
|
||||||
|
4. restores with `pg_restore --exit-on-error`;
|
||||||
|
5. reads every restored public application table and checks the PostGIS
|
||||||
|
library;
|
||||||
|
6. runs the current immutable release's migrations and migration-readiness check
|
||||||
|
against only the temporary database;
|
||||||
|
7. drops only the temporary drill database and verifies that it is gone.
|
||||||
|
|
||||||
|
A trap also attempts to drop the exact temporary database if a check fails.
|
||||||
|
The source application database is never passed to `pg_restore`, `dropdb`, a
|
||||||
|
clean operation, or the drill migration runner. The drill intentionally does
|
||||||
|
not compare an older backup's row counts to the live source, because concurrent
|
||||||
|
legitimate writes or a historical archive would make that comparison invalid.
|
||||||
|
|
||||||
|
## Service checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
curl --fail http://localhost:4010/healthz/live
|
||||||
|
curl --fail http://localhost:4010/healthz/ready
|
||||||
|
./scripts/verify-realtime-cluster.sh compose
|
||||||
|
```
|
||||||
|
|
||||||
|
`live` verifies that the web process can serve HTTP. `ready` additionally runs
|
||||||
|
`SELECT 1` through the configured Ecto repository. The cluster probe subscribes
|
||||||
|
on one connected BEAM node and broadcasts from another.
|
||||||
|
|
||||||
|
Health checks do not replace alerting, database backups, restore drills, or
|
||||||
|
application-level synthetic checks.
|
||||||
|
|
||||||
|
## Protected Prometheus metrics
|
||||||
|
|
||||||
|
The web role exposes Prometheus text format at `/metrics`. It requires the
|
||||||
|
independent `METRICS_TOKEN` deployment secret:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --fail \
|
||||||
|
--header "Authorization: Bearer $METRICS_TOKEN" \
|
||||||
|
http://localhost:4010/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
The endpoint returns `401` without the exact token, disables response caching,
|
||||||
|
and does not put the credential in a URL. The reporter exports cumulative HTTP
|
||||||
|
request and duration, router exception, database query and duration, WebSocket
|
||||||
|
connection, VM memory, and scheduler run-queue metrics. Cumulative durations are
|
||||||
|
integer microseconds because the selected reporter's sum accumulator is
|
||||||
|
integer-based; divide by `1_000_000` in PromQL when seconds are required.
|
||||||
|
Definitions intentionally have no request path, user, request, or event-name
|
||||||
|
labels that could create unbounded cardinality.
|
||||||
|
|
||||||
|
Prometheus itself, durable metrics retention, alert rules, notification
|
||||||
|
destinations, and measured alert thresholds are deployment responsibilities and
|
||||||
|
are not claimed by this repository. In Kubernetes, put the token in
|
||||||
|
`existingSecret`; configure the external scraper to send it as a Bearer token.
|
||||||
|
|
||||||
|
Metrics are local to each BEAM process. Discover and scrape every web pod or
|
||||||
|
container as a distinct target and preserve Prometheus's `instance` label. A
|
||||||
|
request through the load-balanced public route reaches only one replica and is
|
||||||
|
therefore useful as an authorization/smoke check, not as a cluster-wide
|
||||||
|
aggregate.
|
||||||
|
|
||||||
|
## Rollback boundary
|
||||||
|
|
||||||
|
The release image is immutable and migrations run as a separate one-shot role.
|
||||||
|
Before a schema rollout, create and restore-test a current backup. Application
|
||||||
|
rollback and database migration rollback are separate decisions: do not run an
|
||||||
|
Ecto down migration merely because an image is rolled back. Inspect the exact
|
||||||
|
migration and compatibility boundary first.
|
||||||
|
|
||||||
|
The repository intentionally does not ship an automatic destructive production
|
||||||
|
restore command.
|
||||||
|
|
@ -174,6 +174,32 @@ Local ignored browser evidence:
|
||||||
|
|
||||||
- `.playwright-cli/page-2026-07-18T18-09-44-303Z.png`
|
- `.playwright-cli/page-2026-07-18T18-09-44-303Z.png`
|
||||||
|
|
||||||
|
## Operations and metrics verification
|
||||||
|
|
||||||
|
On 2026-07-18, the Compose database was archived with PostgreSQL 18
|
||||||
|
custom-format `pg_dump`. The published archive and SHA-256 sidecar were
|
||||||
|
validated before an isolated restore. The restore drill created a database from
|
||||||
|
`template0`, restored with `pg_restore --exit-on-error`, read 23 public
|
||||||
|
application tables and 1106 restored rows, reported PostGIS 3.6.4, observed all
|
||||||
|
8 current migrations, and removed the exact temporary database. A follow-up
|
||||||
|
catalog query returned zero remaining restore-drill databases. Source counts
|
||||||
|
before and after remained `2 users / 1 help request / 7 messages / 14 categories
|
||||||
|
/ 1 assignment / 0 activities / 0 reports / 0 social identities`.
|
||||||
|
|
||||||
|
The same rollout started 2 healthy web replicas and 2 worker replicas. Local
|
||||||
|
live/readiness, public HTTPS readiness, and the public root returned HTTP 200.
|
||||||
|
The cross-node PubSub probe passed across all four connected BEAM nodes. The
|
||||||
|
protected metrics route returned HTTP 401 both locally and through public HTTPS
|
||||||
|
without credentials, returned valid Prometheus text with the generated ignored
|
||||||
|
local token, and was scraped directly from each web container. Helm 4.2.3 lint
|
||||||
|
and template rendering passed.
|
||||||
|
|
||||||
|
Before the runtime image change, every BEAM container logged that
|
||||||
|
`libsctp.so.1` was unavailable. The rebuilt Debian trixie release contains the
|
||||||
|
`libsctp1` package and the exact shared library; no SCTP, application error, or
|
||||||
|
application warning appeared in the post-rollout web/worker logs checked during
|
||||||
|
this verification window.
|
||||||
|
|
||||||
## Known work before a public production launch
|
## Known work before a public production launch
|
||||||
|
|
||||||
- Replace the temporary staging origin with the production-owned domain and
|
- Replace the temporary staging origin with the production-owned domain and
|
||||||
|
|
|
||||||
31
lib/who_need_help_web/controllers/metrics_controller.ex
Normal file
31
lib/who_need_help_web/controllers/metrics_controller.ex
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
defmodule WhoNeedHelpWeb.MetricsController do
|
||||||
|
use WhoNeedHelpWeb, :controller
|
||||||
|
|
||||||
|
@content_type "text/plain; version=0.0.4"
|
||||||
|
|
||||||
|
def show(conn, _params) do
|
||||||
|
token = Application.get_env(:who_need_help, :metrics_token)
|
||||||
|
|
||||||
|
if authorized?(get_req_header(conn, "authorization"), token) do
|
||||||
|
body = TelemetryMetricsPrometheus.Core.scrape(:prometheus_metrics)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_header("cache-control", "no-store")
|
||||||
|
|> put_resp_content_type(@content_type)
|
||||||
|
|> send_resp(:ok, body)
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
|> put_resp_header("cache-control", "no-store")
|
||||||
|
|> put_resp_header("www-authenticate", "Bearer")
|
||||||
|
|> send_resp(:unauthorized, "Unauthorized\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp authorized?(["Bearer " <> candidate], expected)
|
||||||
|
when is_binary(expected) and expected != "" and
|
||||||
|
byte_size(candidate) == byte_size(expected) do
|
||||||
|
Plug.Crypto.secure_compare(candidate, expected)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp authorized?(_authorization, _expected), do: false
|
||||||
|
end
|
||||||
|
|
@ -33,6 +33,10 @@ defmodule WhoNeedHelpWeb.Router do
|
||||||
get "/ready", HealthController, :ready
|
get "/ready", HealthController, :ready
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scope "/", WhoNeedHelpWeb do
|
||||||
|
get "/metrics", MetricsController, :show
|
||||||
|
end
|
||||||
|
|
||||||
scope "/mobile", WhoNeedHelpWeb do
|
scope "/mobile", WhoNeedHelpWeb do
|
||||||
pipe_through :mobile
|
pipe_through :mobile
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,87 @@ defmodule WhoNeedHelpWeb.Telemetry do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(_arg) do
|
def init(_arg) do
|
||||||
children = [
|
reporter_children =
|
||||||
|
if Application.fetch_env!(:who_need_help, :app_role) == :web do
|
||||||
|
[
|
||||||
|
{TelemetryMetricsPrometheus.Core,
|
||||||
|
name: :prometheus_metrics, metrics: prometheus_metrics(), start_async: false}
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
children =
|
||||||
|
reporter_children ++
|
||||||
|
[
|
||||||
# Telemetry poller will execute the given period measurements
|
# Telemetry poller will execute the given period measurements
|
||||||
# every 10_000ms. Learn more here: https://telemetry-metrics.hexdocs.pm
|
# every 10_000ms. Learn more here: https://telemetry-metrics.hexdocs.pm
|
||||||
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
|
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
|
||||||
# Add reporters as children of your supervision tree.
|
|
||||||
# {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
Supervisor.init(children, strategy: :one_for_one)
|
Supervisor.init(children, strategy: :one_for_one)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def prometheus_metrics do
|
||||||
|
[
|
||||||
|
counter("who_need_help.http.requests.total",
|
||||||
|
event_name: [:phoenix, :endpoint, :stop],
|
||||||
|
measurement: :duration,
|
||||||
|
description: "Completed HTTP requests"
|
||||||
|
),
|
||||||
|
sum("who_need_help.http.request.duration.microseconds.total",
|
||||||
|
event_name: [:phoenix, :endpoint, :stop],
|
||||||
|
measurement: fn measurements ->
|
||||||
|
System.convert_time_unit(measurements.duration, :native, :microsecond)
|
||||||
|
end,
|
||||||
|
description: "Cumulative HTTP request duration"
|
||||||
|
),
|
||||||
|
counter("who_need_help.http.exceptions.total",
|
||||||
|
event_name: [:phoenix, :router_dispatch, :exception],
|
||||||
|
measurement: :duration,
|
||||||
|
description: "Router dispatch exceptions"
|
||||||
|
),
|
||||||
|
counter("who_need_help.database.queries.total",
|
||||||
|
event_name: [:who_need_help, :repo, :query],
|
||||||
|
measurement: :total_time,
|
||||||
|
description: "Completed database queries"
|
||||||
|
),
|
||||||
|
sum("who_need_help.database.query.duration.microseconds.total",
|
||||||
|
event_name: [:who_need_help, :repo, :query],
|
||||||
|
measurement: fn measurements ->
|
||||||
|
System.convert_time_unit(measurements.total_time, :native, :microsecond)
|
||||||
|
end,
|
||||||
|
description: "Cumulative database query duration"
|
||||||
|
),
|
||||||
|
counter("who_need_help.websocket.connections.total",
|
||||||
|
event_name: [:phoenix, :socket_connected],
|
||||||
|
measurement: :duration,
|
||||||
|
description: "Completed WebSocket connection attempts"
|
||||||
|
),
|
||||||
|
last_value("who_need_help.vm.memory.total.bytes",
|
||||||
|
event_name: [:vm, :memory],
|
||||||
|
measurement: :total,
|
||||||
|
unit: :byte,
|
||||||
|
description: "Total memory used by the Erlang VM"
|
||||||
|
),
|
||||||
|
last_value("who_need_help.vm.run_queue.total",
|
||||||
|
event_name: [:vm, :total_run_queue_lengths],
|
||||||
|
measurement: :total,
|
||||||
|
description: "Total scheduler run queue length"
|
||||||
|
),
|
||||||
|
last_value("who_need_help.vm.run_queue.cpu",
|
||||||
|
event_name: [:vm, :total_run_queue_lengths],
|
||||||
|
measurement: :cpu,
|
||||||
|
description: "CPU scheduler run queue length"
|
||||||
|
),
|
||||||
|
last_value("who_need_help.vm.run_queue.io",
|
||||||
|
event_name: [:vm, :total_run_queue_lengths],
|
||||||
|
measurement: :io,
|
||||||
|
description: "I/O scheduler run queue length"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
def metrics do
|
def metrics do
|
||||||
[
|
[
|
||||||
# Phoenix Metrics
|
# Phoenix Metrics
|
||||||
|
|
|
||||||
1
mix.exs
1
mix.exs
|
|
@ -73,6 +73,7 @@ defmodule WhoNeedHelp.MixProject do
|
||||||
{:req, "~> 0.5"},
|
{:req, "~> 0.5"},
|
||||||
{:assent, "~> 0.3.1"},
|
{:assent, "~> 0.3.1"},
|
||||||
{:telemetry_metrics, "~> 1.0"},
|
{:telemetry_metrics, "~> 1.0"},
|
||||||
|
{:telemetry_metrics_prometheus_core, "~> 1.2.1"},
|
||||||
{:telemetry_poller, "~> 1.0"},
|
{:telemetry_poller, "~> 1.0"},
|
||||||
{:gettext, "~> 1.0"},
|
{:gettext, "~> 1.0"},
|
||||||
{:jason, "~> 1.2"},
|
{:jason, "~> 1.2"},
|
||||||
|
|
|
||||||
1
mix.lock
1
mix.lock
|
|
@ -47,6 +47,7 @@
|
||||||
"tailwind": {:hex, :tailwind, "0.5.1", "35435b13158c90d37da11e1cfc808755fca1d7b6c5ab87b1b19c5de87e2f0a10", [:mix], [], "hexpm", "c4e26302a59fec72abc5610ecb6ad2116d9aa31f31aab2d4b8eb6e95d25a689c"},
|
"tailwind": {:hex, :tailwind, "0.5.1", "35435b13158c90d37da11e1cfc808755fca1d7b6c5ab87b1b19c5de87e2f0a10", [:mix], [], "hexpm", "c4e26302a59fec72abc5610ecb6ad2116d9aa31f31aab2d4b8eb6e95d25a689c"},
|
||||||
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
|
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
|
||||||
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
||||||
|
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"},
|
||||||
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
|
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
|
||||||
"thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"},
|
"thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"},
|
||||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||||
|
|
|
||||||
60
scripts/backup-compose.sh
Executable file
60
scripts/backup-compose.sh
Executable file
|
|
@ -0,0 +1,60 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
if [ ! -f "$ROOT/.env" ]; then
|
||||||
|
echo "Missing $ROOT/.env." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a
|
||||||
|
. "$ROOT/.env"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
: "${POSTGRES_DB:?Set POSTGRES_DB in .env}"
|
||||||
|
: "${POSTGRES_USER:?Set POSTGRES_USER in .env}"
|
||||||
|
|
||||||
|
timestamp=$(date -u +%Y%m%d-%H%M%S)
|
||||||
|
target=${1:-"$ROOT/output/backups/compose-$timestamp.dump"}
|
||||||
|
target_dir=$(dirname -- "$target")
|
||||||
|
target_name=$(basename -- "$target")
|
||||||
|
partial="$target.partial.$$"
|
||||||
|
checksum="$target.sha256"
|
||||||
|
checksum_partial="$checksum.partial.$$"
|
||||||
|
|
||||||
|
if [ -e "$target" ] || [ -e "$checksum" ]; then
|
||||||
|
echo "Refusing to overwrite $target or $checksum." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$target_dir"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$partial" "$checksum_partial"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
docker compose exec -T db \
|
||||||
|
pg_dump \
|
||||||
|
--username "$POSTGRES_USER" \
|
||||||
|
--dbname "$POSTGRES_DB" \
|
||||||
|
--format custom \
|
||||||
|
--no-owner >"$partial"
|
||||||
|
|
||||||
|
test -s "$partial"
|
||||||
|
|
||||||
|
docker compose exec -T db pg_restore --list <"$partial" >/dev/null
|
||||||
|
|
||||||
|
hash=$(sha256sum "$partial" | awk '{print $1}')
|
||||||
|
printf '%s %s\n' "$hash" "$target_name" >"$checksum_partial"
|
||||||
|
|
||||||
|
mv "$partial" "$target"
|
||||||
|
mv "$checksum_partial" "$checksum"
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
|
||||||
|
echo "Backup: $target"
|
||||||
|
echo "Checksum: $checksum"
|
||||||
|
echo "SHA-256: $hash"
|
||||||
54
scripts/ensure-local-metrics-token.sh
Executable file
54
scripts/ensure-local-metrics-token.sh
Executable file
|
|
@ -0,0 +1,54 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
ENV_FILE="$ROOT/.env"
|
||||||
|
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
echo "Missing $ENV_FILE. Copy .env.example to .env first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for command in openssl perl; do
|
||||||
|
if ! command -v "$command" >/dev/null 2>&1; then
|
||||||
|
echo "Required command is unavailable: $command" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
current=$(
|
||||||
|
perl -ne '
|
||||||
|
if (/^METRICS_TOKEN=(.*)$/) {
|
||||||
|
print $1;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
' "$ENV_FILE"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -n "$current" ]; then
|
||||||
|
chmod 600 "$ENV_FILE"
|
||||||
|
echo "METRICS_TOKEN already exists; no secret was changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
new_metrics_token=$(openssl rand -hex 32)
|
||||||
|
tmp_env=$(mktemp "${ENV_FILE}.metrics.XXXXXX")
|
||||||
|
trap 'rm -f "$tmp_env"' EXIT HUP INT TERM
|
||||||
|
chmod 600 "$tmp_env"
|
||||||
|
|
||||||
|
NEW_METRICS_TOKEN=$new_metrics_token perl -0pe '
|
||||||
|
if (s/^METRICS_TOKEN=.*$/METRICS_TOKEN=$ENV{NEW_METRICS_TOKEN}/m) {
|
||||||
|
$replaced = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
END {
|
||||||
|
print "\nMETRICS_TOKEN=$ENV{NEW_METRICS_TOKEN}\n" unless $replaced;
|
||||||
|
}
|
||||||
|
' "$ENV_FILE" >"$tmp_env"
|
||||||
|
|
||||||
|
mv "$tmp_env" "$ENV_FILE"
|
||||||
|
chmod 600 "$ENV_FILE"
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
unset new_metrics_token
|
||||||
|
|
||||||
|
echo "Generated METRICS_TOKEN in the ignored .env without printing it."
|
||||||
|
|
@ -108,6 +108,7 @@ if ! kube --namespace "$NAMESPACE" get secret "$SECRET_NAME" >/dev/null 2>&1; th
|
||||||
secret_key_base=$(openssl rand -hex 64)
|
secret_key_base=$(openssl rand -hex 64)
|
||||||
handover_secret=$(openssl rand -hex 64)
|
handover_secret=$(openssl rand -hex 64)
|
||||||
release_cookie=$(openssl rand -hex 64)
|
release_cookie=$(openssl rand -hex 64)
|
||||||
|
metrics_token=$(openssl rand -hex 32)
|
||||||
|
|
||||||
kube --namespace "$NAMESPACE" create secret generic "$SECRET_NAME" \
|
kube --namespace "$NAMESPACE" create secret generic "$SECRET_NAME" \
|
||||||
--from-literal=POSTGRES_DB=who_need_help \
|
--from-literal=POSTGRES_DB=who_need_help \
|
||||||
|
|
@ -116,9 +117,18 @@ if ! kube --namespace "$NAMESPACE" get secret "$SECRET_NAME" >/dev/null 2>&1; th
|
||||||
--from-literal="DATABASE_URL=ecto://postgres:${postgres_password}@postgis/who_need_help" \
|
--from-literal="DATABASE_URL=ecto://postgres:${postgres_password}@postgis/who_need_help" \
|
||||||
--from-literal="SECRET_KEY_BASE=$secret_key_base" \
|
--from-literal="SECRET_KEY_BASE=$secret_key_base" \
|
||||||
--from-literal="HANDOVER_SECRET=$handover_secret" \
|
--from-literal="HANDOVER_SECRET=$handover_secret" \
|
||||||
--from-literal="RELEASE_COOKIE=$release_cookie"
|
--from-literal="RELEASE_COOKIE=$release_cookie" \
|
||||||
|
--from-literal="METRICS_TOKEN=$metrics_token"
|
||||||
|
|
||||||
unset postgres_password secret_key_base handover_secret release_cookie
|
unset postgres_password secret_key_base handover_secret release_cookie metrics_token
|
||||||
|
elif [ -z "$(kube --namespace "$NAMESPACE" get secret "$SECRET_NAME" -o jsonpath='{.data.METRICS_TOKEN}')" ]; then
|
||||||
|
metrics_token=$(openssl rand -hex 32)
|
||||||
|
|
||||||
|
kube --namespace "$NAMESPACE" patch secret "$SECRET_NAME" \
|
||||||
|
--type merge \
|
||||||
|
--patch "{\"stringData\":{\"METRICS_TOKEN\":\"$metrics_token\"}}" >/dev/null
|
||||||
|
|
||||||
|
unset metrics_token
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$legacy_backup" ]; then
|
if [ -n "$legacy_backup" ]; then
|
||||||
|
|
|
||||||
169
scripts/restore-drill-compose.sh
Executable file
169
scripts/restore-drill-compose.sh
Executable file
|
|
@ -0,0 +1,169 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
if [ "$#" -ne 1 ]; then
|
||||||
|
echo "Usage: $0 PATH_TO_CUSTOM_FORMAT_DUMP" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
dump=$1
|
||||||
|
checksum="$dump.sha256"
|
||||||
|
|
||||||
|
if [ ! -f "$dump" ] || [ ! -s "$dump" ]; then
|
||||||
|
echo "Dump does not exist or is empty: $dump" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$checksum" ]; then
|
||||||
|
echo "Checksum manifest is required: $checksum" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$ROOT/.env" ]; then
|
||||||
|
echo "Missing $ROOT/.env." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a
|
||||||
|
. "$ROOT/.env"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
: "${POSTGRES_DB:?Set POSTGRES_DB in .env}"
|
||||||
|
: "${POSTGRES_USER:?Set POSTGRES_USER in .env}"
|
||||||
|
: "${DATABASE_URL:?Set DATABASE_URL in .env}"
|
||||||
|
|
||||||
|
dump_dir=$(CDPATH= cd -- "$(dirname -- "$dump")" && pwd)
|
||||||
|
dump_name=$(basename -- "$dump")
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$dump_dir"
|
||||||
|
sha256sum --check --status "$(basename -- "$checksum")"
|
||||||
|
)
|
||||||
|
|
||||||
|
docker compose exec -T db pg_restore --list <"$dump" >/dev/null
|
||||||
|
|
||||||
|
drill_db="who_need_help_restore_drill_$(date -u +%Y%m%d%H%M%S)_$$"
|
||||||
|
created=false
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ "$created" = true ]; then
|
||||||
|
docker compose exec -T db \
|
||||||
|
dropdb --username "$POSTGRES_USER" --if-exists "$drill_db" >/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
docker compose exec -T db \
|
||||||
|
createdb \
|
||||||
|
--username "$POSTGRES_USER" \
|
||||||
|
--template template0 \
|
||||||
|
"$drill_db"
|
||||||
|
created=true
|
||||||
|
|
||||||
|
docker compose exec -T db \
|
||||||
|
pg_restore \
|
||||||
|
--username "$POSTGRES_USER" \
|
||||||
|
--dbname "$drill_db" \
|
||||||
|
--exit-on-error \
|
||||||
|
--no-owner \
|
||||||
|
--no-privileges <"$dump"
|
||||||
|
|
||||||
|
tables=$(
|
||||||
|
docker compose exec -T db \
|
||||||
|
psql --username "$POSTGRES_USER" --dbname "$drill_db" --tuples-only --no-align \
|
||||||
|
--command "
|
||||||
|
SELECT tablename
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
AND tablename <> 'spatial_ref_sys'
|
||||||
|
ORDER BY tablename;
|
||||||
|
"
|
||||||
|
)
|
||||||
|
|
||||||
|
table_count=0
|
||||||
|
total_rows=0
|
||||||
|
|
||||||
|
for table in $tables; do
|
||||||
|
restored_count=$(
|
||||||
|
docker compose exec -T db \
|
||||||
|
psql --username "$POSTGRES_USER" --dbname "$drill_db" --tuples-only --no-align \
|
||||||
|
--command "SELECT count(*) FROM \"$table\";"
|
||||||
|
)
|
||||||
|
|
||||||
|
table_count=$((table_count + 1))
|
||||||
|
total_rows=$((total_rows + restored_count))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$table_count" -eq 0 ]; then
|
||||||
|
echo "The restored database contains no public application tables." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
postgis_version=$(
|
||||||
|
docker compose exec -T db \
|
||||||
|
psql --username "$POSTGRES_USER" --dbname "$drill_db" --tuples-only --no-align \
|
||||||
|
--command "SELECT PostGIS_Lib_Version();"
|
||||||
|
)
|
||||||
|
|
||||||
|
case "$DATABASE_URL" in
|
||||||
|
*\?*)
|
||||||
|
database_base=${DATABASE_URL%%\?*}
|
||||||
|
database_query="?${DATABASE_URL#*\?}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
database_base=$DATABASE_URL
|
||||||
|
database_query=
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
database_prefix=${database_base%/*}
|
||||||
|
|
||||||
|
if [ "$database_prefix" = "$database_base" ]; then
|
||||||
|
echo "Could not derive the temporary drill DATABASE_URL." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
drill_database_url="$database_prefix/$drill_db$database_query"
|
||||||
|
|
||||||
|
docker compose run --rm --no-deps \
|
||||||
|
--env APP_ROLE=migrate \
|
||||||
|
--env "DATABASE_URL=$drill_database_url" \
|
||||||
|
migrate /app/bin/migrate
|
||||||
|
|
||||||
|
docker compose run --rm --no-deps \
|
||||||
|
--env APP_ROLE=migrate \
|
||||||
|
--env "DATABASE_URL=$drill_database_url" \
|
||||||
|
migrate /app/bin/who_need_help eval 'WhoNeedHelp.Release.await_migrations()'
|
||||||
|
|
||||||
|
migration_count=$(
|
||||||
|
docker compose exec -T db \
|
||||||
|
psql --username "$POSTGRES_USER" --dbname "$drill_db" --tuples-only --no-align \
|
||||||
|
--command "SELECT count(*) FROM schema_migrations;"
|
||||||
|
)
|
||||||
|
|
||||||
|
docker compose exec -T db \
|
||||||
|
dropdb --username "$POSTGRES_USER" "$drill_db"
|
||||||
|
created=false
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
|
||||||
|
remaining=$(
|
||||||
|
docker compose exec -T db \
|
||||||
|
psql --username "$POSTGRES_USER" --dbname postgres --tuples-only --no-align \
|
||||||
|
--command "SELECT count(*) FROM pg_database WHERE datname = '$drill_db';"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "$remaining" != "0" ]; then
|
||||||
|
echo "Restore drill database was not removed: $drill_db" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Restore drill passed for: $dump_name"
|
||||||
|
echo "Readable public tables: $table_count"
|
||||||
|
echo "Restored rows read: $total_rows"
|
||||||
|
echo "Applied migrations: $migration_count"
|
||||||
|
echo "PostGIS library: $postgis_version"
|
||||||
|
echo "Temporary database removed: $drill_db"
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
defmodule WhoNeedHelpWeb.MetricsControllerTest do
|
||||||
|
use WhoNeedHelpWeb.ConnCase, async: false
|
||||||
|
|
||||||
|
test "rejects a request without credentials", %{conn: conn} do
|
||||||
|
conn = get(conn, "/metrics")
|
||||||
|
|
||||||
|
assert response(conn, 401) == "Unauthorized\n"
|
||||||
|
assert get_resp_header(conn, "www-authenticate") == ["Bearer"]
|
||||||
|
assert get_resp_header(conn, "cache-control") == ["no-store"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejects an incorrect bearer token", %{conn: conn} do
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("authorization", "Bearer incorrect-token")
|
||||||
|
|> get("/metrics")
|
||||||
|
|
||||||
|
assert response(conn, 401) == "Unauthorized\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "exports Prometheus metrics for the configured bearer token", %{conn: conn} do
|
||||||
|
get(conn, "/healthz/live")
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("authorization", "Bearer test-metrics-token")
|
||||||
|
|> get("/metrics")
|
||||||
|
|
||||||
|
body = response(conn, 200)
|
||||||
|
|
||||||
|
assert get_resp_header(conn, "content-type") == [
|
||||||
|
"text/plain; version=0.0.4; charset=utf-8"
|
||||||
|
]
|
||||||
|
|
||||||
|
assert get_resp_header(conn, "cache-control") == ["no-store"]
|
||||||
|
assert body =~ "# TYPE who_need_help_http_requests_total counter"
|
||||||
|
assert body =~ "who_need_help_http_requests_total "
|
||||||
|
assert body =~ "who_need_help_http_request_duration_microseconds_total "
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue
Block a user