feat(telemetry): PromEx Prometheus exporter at /metrics
Wires PromEx with its built-in Application / BEAM / Phoenix / Ecto /
Oban plugins plus a custom `Microwaveprop.PromEx.InstrumentPlugin`
that registers Prometheus histograms for every
`Microwaveprop.Instrument` span (HRRR/NEXRAD/IEM/GEFS/NARR/UWYO
/Elevation fetches, DB batch upserts, terrain analysis, radar
aggregation, propagation score band). Queue-depth gauges from the
10-second poller land at `microwaveprop_oban_queue_count{queue,state}`.
Router forwards `/metrics` to MicrowavepropWeb.MetricsPlug which
optionally requires a bearer token (`PROMETHEUS_AUTH_TOKEN` env var in
runtime.exs); leave it unset to expose metrics publicly and restrict
at the ingress / Cloudflare Access layer instead.
Example external Prometheus scrape config:
scrape_configs:
- job_name: microwaveprop
scrape_interval: 30s
metrics_path: /metrics
scheme: https
authorization:
type: Bearer
credentials: <token>
static_configs:
- targets: ['prop.w5isp.com:443']
This commit is contained in:
parent
0833ee44f9
commit
c7bc3ed5d0
9 changed files with 305 additions and 0 deletions
|
|
@ -41,6 +41,19 @@ config :logger, :default_formatter,
|
|||
# at the `config/runtime.exs`.
|
||||
config :microwaveprop, Microwaveprop.Mailer, adapter: Swoosh.Adapters.Local
|
||||
|
||||
# PromEx metrics exporter. We surface the `/metrics` Prometheus scrape
|
||||
# endpoint via MicrowavepropWeb.Router rather than PromEx's own HTTP
|
||||
# server, so the router (and optional bearer-token auth in
|
||||
# MicrowavepropWeb.MetricsPlug) stay the single ingress path. Grafana
|
||||
# auto-provisioning is off — dashboards live in the dedicated Grafana
|
||||
# instance and are managed separately.
|
||||
config :microwaveprop, Microwaveprop.PromEx,
|
||||
disabled: false,
|
||||
manual_metrics_start_delay: :no_delay,
|
||||
drop_metrics_groups: [],
|
||||
grafana: :disabled,
|
||||
metrics_server: :disabled
|
||||
|
||||
# Configure the endpoint
|
||||
config :microwaveprop, MicrowavepropWeb.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ end
|
|||
|
||||
config :microwaveprop, MicrowavepropWeb.Endpoint, http: [port: String.to_integer(System.get_env("PORT", "4000"))]
|
||||
|
||||
if prometheus_token = System.get_env("PROMETHEUS_AUTH_TOKEN") do
|
||||
config :microwaveprop, :prometheus_auth_token, prometheus_token
|
||||
end
|
||||
|
||||
if qrz_username = System.get_env("QRZ_USERNAME") do
|
||||
config :microwaveprop, Microwaveprop.Qrz.Client,
|
||||
username: qrz_username,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ defmodule Microwaveprop.Application do
|
|||
|
||||
children = [
|
||||
MicrowavepropWeb.Telemetry,
|
||||
Microwaveprop.PromEx,
|
||||
Microwaveprop.Repo,
|
||||
{Cluster.Supervisor, [topologies, [name: Microwaveprop.ClusterSupervisor]]},
|
||||
{Phoenix.PubSub, name: Microwaveprop.PubSub},
|
||||
|
|
|
|||
47
lib/microwaveprop/prom_ex.ex
Normal file
47
lib/microwaveprop/prom_ex.ex
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
defmodule Microwaveprop.PromEx do
|
||||
@moduledoc """
|
||||
Prometheus metrics exporter. Combines PromEx's built-in plugins for
|
||||
Application/BEAM/Phoenix/Ecto/Oban with our own custom metrics
|
||||
(Microwaveprop.Instrument spans, Oban queue-depth gauges).
|
||||
|
||||
A dedicated Prometheus server outside the Kubernetes cluster scrapes
|
||||
`/metrics` over the app's normal HTTP endpoint. The `MicrowavepropWeb.Router`
|
||||
is responsible for making that route reachable.
|
||||
"""
|
||||
use PromEx, otp_app: :microwaveprop
|
||||
|
||||
alias PromEx.Plugins
|
||||
|
||||
@impl true
|
||||
def plugins do
|
||||
[
|
||||
# Defaults: VM memory, process counts, scheduler util, app version.
|
||||
Plugins.Application,
|
||||
Plugins.Beam,
|
||||
|
||||
# Phoenix: request duration by route/status, channel/socket events.
|
||||
{Plugins.Phoenix, router: MicrowavepropWeb.Router, endpoint: MicrowavepropWeb.Endpoint},
|
||||
|
||||
# Ecto: query duration, queue time, pool size.
|
||||
{Plugins.Ecto, otp_app: :microwaveprop, repos: [Microwaveprop.Repo]},
|
||||
|
||||
# Oban: job duration, queue time, state counts, queue depth.
|
||||
Plugins.Oban,
|
||||
|
||||
# Our own spans: Instrument.span/3 emits [:microwaveprop | suffix]
|
||||
# events; this plugin converts them to Prometheus histograms.
|
||||
Microwaveprop.PromEx.InstrumentPlugin
|
||||
]
|
||||
end
|
||||
|
||||
@impl true
|
||||
def dashboards do
|
||||
[
|
||||
{:prom_ex, "application.json"},
|
||||
{:prom_ex, "beam.json"},
|
||||
{:prom_ex, "phoenix.json"},
|
||||
{:prom_ex, "ecto.json"},
|
||||
{:prom_ex, "oban.json"}
|
||||
]
|
||||
end
|
||||
end
|
||||
174
lib/microwaveprop/prom_ex/instrument_plugin.ex
Normal file
174
lib/microwaveprop/prom_ex/instrument_plugin.ex
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
defmodule Microwaveprop.PromEx.InstrumentPlugin do
|
||||
@moduledoc """
|
||||
PromEx plugin that registers every `Microwaveprop.Instrument` span as
|
||||
a Prometheus histogram. Keep the catalog in sync with the call sites
|
||||
in `HrrrClient`, `NexradClient`, `IemClient`, `GefsClient`,
|
||||
`NarrClient`, `UwyoSoundingClient`, `ElevationClient`, `Weather` batch
|
||||
upserts, `Propagation.replace_scores`, `PropagationGridWorker`,
|
||||
`TerrainAnalysis`, and `CommonVolumeRadarWorker`.
|
||||
"""
|
||||
use PromEx.Plugin
|
||||
|
||||
@impl true
|
||||
def event_metrics(_opts) do
|
||||
[
|
||||
http_client_events(),
|
||||
db_batch_events(),
|
||||
worker_phase_events(),
|
||||
oban_queue_depth_event()
|
||||
]
|
||||
end
|
||||
|
||||
# PromEx interprets `Telemetry.Metrics` specs the same way the
|
||||
# in-process telemetry_metrics module does, so we re-use the same
|
||||
# event_name / measurement / tags shape from MicrowavepropWeb.Telemetry.
|
||||
defp http_client_events do
|
||||
Event.build(
|
||||
:microwaveprop_http_client_events,
|
||||
[
|
||||
distribution("microwaveprop.hrrr.fetch_grid.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :hrrr, :fetch_grid, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: default_buckets()]
|
||||
),
|
||||
distribution("microwaveprop.hrrr.fetch_profile.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :hrrr, :fetch_profile, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: default_buckets()]
|
||||
),
|
||||
distribution("microwaveprop.hrrr.fetch_idx.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :hrrr, :fetch_idx, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: default_buckets()]
|
||||
),
|
||||
distribution("microwaveprop.nexrad.fetch_frame.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :nexrad, :fetch_frame, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: default_buckets()]
|
||||
),
|
||||
distribution("microwaveprop.nexrad.decode_png.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :nexrad, :decode_png, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: default_buckets()]
|
||||
),
|
||||
distribution("microwaveprop.iem.fetch_asos.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :iem, :fetch_asos, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: default_buckets()]
|
||||
),
|
||||
distribution("microwaveprop.iem.fetch_raob.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :iem, :fetch_raob, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: default_buckets()]
|
||||
),
|
||||
distribution("microwaveprop.gefs.fetch_grid_profiles.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :gefs, :fetch_grid_profiles, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: default_buckets()]
|
||||
),
|
||||
distribution("microwaveprop.narr.fetch_profile.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :narr, :fetch_profile, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: default_buckets()]
|
||||
),
|
||||
distribution("microwaveprop.uwyo.fetch_sounding.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :uwyo, :fetch_sounding, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: default_buckets()]
|
||||
),
|
||||
distribution("microwaveprop.elevation.fetch_profile.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :elevation, :fetch_profile, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: default_buckets()]
|
||||
)
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
defp db_batch_events do
|
||||
Event.build(
|
||||
:microwaveprop_db_batch_events,
|
||||
[
|
||||
distribution("microwaveprop.db.upsert_hrrr_profiles.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :db, :upsert_hrrr_profiles, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: db_buckets()]
|
||||
),
|
||||
distribution("microwaveprop.db.upsert_gefs_profiles.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :db, :upsert_gefs_profiles, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: db_buckets()]
|
||||
),
|
||||
distribution("microwaveprop.db.replace_scores.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :db, :replace_scores, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: db_buckets()]
|
||||
)
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
defp worker_phase_events do
|
||||
Event.build(
|
||||
:microwaveprop_worker_phase_events,
|
||||
[
|
||||
distribution("microwaveprop.propagation_grid.score_band.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :propagation_grid, :score_band, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: long_buckets()]
|
||||
),
|
||||
distribution("microwaveprop.terrain.analyse.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :terrain, :analyse, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: default_buckets()]
|
||||
),
|
||||
distribution("microwaveprop.radar.aggregate_stats.duration.milliseconds",
|
||||
event_name: [:microwaveprop, :radar, :aggregate_stats, :stop],
|
||||
measurement: :duration,
|
||||
unit: {:native, :millisecond},
|
||||
reporter_options: [buckets: default_buckets()]
|
||||
)
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
defp oban_queue_depth_event do
|
||||
Event.build(
|
||||
:microwaveprop_oban_queue_depth,
|
||||
[
|
||||
last_value("microwaveprop.oban.queue.count",
|
||||
event_name: [:microwaveprop, :oban, :queue, :depth],
|
||||
measurement: :count,
|
||||
tags: [:queue, :state],
|
||||
description: "Oban job count per (queue, state) sampled every 10s"
|
||||
)
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
# Buckets in milliseconds. HTTP + compute phases: up to ~30s.
|
||||
defp default_buckets, do: [10, 50, 100, 250, 500, 1_000, 2_500, 5_000, 10_000, 30_000]
|
||||
|
||||
# DB batch upserts: can take much longer on cold cache or full
|
||||
# forecast-hour writes — up to ~2 min.
|
||||
defp db_buckets, do: [50, 100, 250, 500, 1_000, 2_500, 5_000, 15_000, 60_000, 120_000]
|
||||
|
||||
# Score computation for a full grid-point batch — up to ~5 min.
|
||||
defp long_buckets, do: [500, 1_000, 5_000, 15_000, 30_000, 60_000, 120_000, 300_000]
|
||||
end
|
||||
51
lib/microwaveprop_web/metrics_plug.ex
Normal file
51
lib/microwaveprop_web/metrics_plug.ex
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
defmodule MicrowavepropWeb.MetricsPlug do
|
||||
@moduledoc """
|
||||
Serves PromEx metrics at `/metrics`. Optionally gated by a bearer
|
||||
token via `config :microwaveprop, :prometheus_auth_token`, sourced
|
||||
from the `PROMETHEUS_AUTH_TOKEN` env var in `runtime.exs`.
|
||||
"""
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
@impl true
|
||||
def init(opts), do: opts
|
||||
|
||||
@impl true
|
||||
def call(conn, _opts) do
|
||||
case authorized?(conn) do
|
||||
:ok ->
|
||||
PromEx.Plug.call(conn, prom_ex_module: Microwaveprop.PromEx, path: "/metrics")
|
||||
|
||||
:denied ->
|
||||
conn
|
||||
|> put_resp_header("www-authenticate", ~s(Bearer realm="metrics"))
|
||||
|> send_resp(401, "unauthorized")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
defp authorized?(conn) do
|
||||
case Application.get_env(:microwaveprop, :prometheus_auth_token) do
|
||||
token when is_binary(token) and token != "" ->
|
||||
check_token(conn, token)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp check_token(conn, expected) do
|
||||
conn
|
||||
|> get_req_header("authorization")
|
||||
|> List.first()
|
||||
|> case do
|
||||
"Bearer " <> token ->
|
||||
if Plug.Crypto.secure_compare(token, expected), do: :ok, else: :denied
|
||||
|
||||
_ ->
|
||||
:denied
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -135,6 +135,14 @@ defmodule MicrowavepropWeb.Router do
|
|||
# Health check — no pipeline, minimal overhead
|
||||
get "/health", MicrowavepropWeb.HealthController, :check, log: false
|
||||
|
||||
# Prometheus scrape endpoint. No pipeline so there's no session /
|
||||
# CSRF overhead — the intent is a dedicated external Prometheus
|
||||
# server polling every 15-30s. If `PROMETHEUS_AUTH_TOKEN` is set
|
||||
# (runtime.exs), the endpoint requires a matching bearer token;
|
||||
# otherwise it's unauthenticated and expected to be restricted at
|
||||
# the ingress / Cloudflare Access layer.
|
||||
forward "/metrics", MicrowavepropWeb.MetricsPlug
|
||||
|
||||
# API discovery (RFC 9727 / RFC 9264). These are JSON-only, no pipeline,
|
||||
# no session — agents and crawlers fetch them without auth.
|
||||
get "/.well-known/api-catalog", MicrowavepropWeb.ApiCatalogController, :catalog
|
||||
|
|
|
|||
1
mix.exs
1
mix.exs
|
|
@ -66,6 +66,7 @@ defmodule Microwaveprop.MixProject do
|
|||
{:req, "~> 0.5"},
|
||||
{:telemetry_metrics, "~> 1.0"},
|
||||
{:telemetry_poller, "~> 1.0"},
|
||||
{:prom_ex, "~> 1.11"},
|
||||
{:gettext, "~> 0.26"},
|
||||
{:jason, "~> 1.2"},
|
||||
{:bandit, "~> 1.5"},
|
||||
|
|
|
|||
6
mix.lock
6
mix.lock
|
|
@ -3,6 +3,7 @@
|
|||
"bandit": {:hex, :bandit, "1.10.4", "02b9734c67c5916a008e7eb7e2ba68aaea6f8177094a5f8d95f1fb99069aac17", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "a5faf501042ac1f31d736d9d4a813b3db4ef812e634583b6a457b0928798a51d"},
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"castore": {:hex, :castore, "1.0.18", "5e43ef0ec7d31195dfa5a65a86e6131db999d074179d2ba5a8de11fe14570f55", [:mix], [], "hexpm", "f393e4fe6317829b158fb74d86eb681f737d2fe326aa61ccf6293c4104957e34"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
||||
"circular_buffer": {:hex, :circular_buffer, "1.0.0", "25c004da0cba7bd8bc1bdabded4f9a902d095e20600fd15faf1f2ffbaea18a07", [:mix], [], "hexpm", "c829ec31c13c7bafd1f546677263dff5bfb006e929f25635878ac3cfba8749e5"},
|
||||
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
||||
|
|
@ -41,7 +42,9 @@
|
|||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"nx": {:hex, :nx, "0.11.0", "d37723dbd6cfa274a5def6d6664f5680c32e2eb8a1ce25ec6d91751967fa0abf", [:mix], [{:complex, "~> 0.6", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36157b21239aeb251d6cbac23eb0eb3495a5e1e0cbc2e6df16afd2ede1575205"},
|
||||
"oban": {:hex, :oban, "2.21.1", "4b6af7b901ef9baca09e239b5a991ef2fa429cf5a13799bc429a131d610ff692", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.20", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8162a160924cf4a25905fed2a9242e7787d88e320e3b5b0dcf324eb17c51c4e6"},
|
||||
"octo_fetch": {:hex, :octo_fetch, "0.5.0", "f50701568b9fc752656367f82cc134d5fbefff37c5a0e8ddfcceb02ceee3f5fc", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "6226cc3c14ca948ee9f25fb0446322e5c288e215da9beba7899b6b5f4cd3ccb0"},
|
||||
"owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"},
|
||||
"peep": {:hex, :peep, "3.5.0", "9f6ead7b0f2c684494200c8fc02e7e62e8c459afe861b29bd859e4c96f402ed8", [:mix], [{:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:plug, "~> 1.16", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5a73a99c6e60062415efeb7e536a663387146463a3d3df1417da31fd665ac210"},
|
||||
"phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
|
||||
|
|
@ -54,11 +57,13 @@
|
|||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||
"polaris": {:hex, :polaris, "0.1.0", "dca61b18e3e801ecdae6ac9f0eca5f19792b44a5cb4b8d63db50fc40fc038d22", [:mix], [{:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "13ef2b166650e533cb24b10e2f3b8ab4f2f449ba4d63156e8c569527f206e2c2"},
|
||||
"postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"},
|
||||
"prom_ex": {:hex, :prom_ex, "1.11.0", "1f6d67f2dead92224cb4f59beb3e4d319257c5728d9638b4a5e8ceb51a4f9c7e", [:mix], [{:absinthe, ">= 1.7.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.1.0", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.11.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.18", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.10.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.4", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:peep, "~> 3.0", [hex: :peep, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.20.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.16.0", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 2.6.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.1", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "76b074bc3730f0802978a7eb5c7091a65473eaaf07e99ec9e933138dcc327805"},
|
||||
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
||||
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
|
||||
"rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"},
|
||||
"sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"},
|
||||
"spitfire": {:hex, :spitfire, "0.3.10", "19aea9914132456515e8f7d592f63ab9f3130876b0252e834d2390bdd8becb24", [:mix], [], "hexpm", "6a6a5f77eb4165249c76199cd2d01fb595bac9207aed3de551918ac1c2bc9267"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"},
|
||||
"styler": {:hex, :styler, "1.11.0", "35010d970689a23c2bcc8e97bd8bf7d20e3561d60c49be84654df5c37d051a9c", [:mix], [], "hexpm", "70f36165d0cf238a32b7a456fdef6a9c72e77e657d7ac4a0ace33aeba3f2b8c0"},
|
||||
"sutra_ui": {:hex, :sutra_ui, "0.3.0", "c7a9790279c28aacbf0174d3a92da70af0913ada63db2eb64c499ec8f8995e83", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "d9ecb24b299090b891c50168806a894ad6359c9b3623c2bb7ebf0c7c1876cf28"},
|
||||
|
|
@ -67,6 +72,7 @@
|
|||
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
|
||||
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"},
|
||||
"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"},
|
||||
"text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue