defmodule Prometheus.PlugExporter do @moduledoc """ Exports Prometheus metrics via configurable endpoint: ``` elixir # define plug defmodule MetricsPlugExporter do use Prometheus.PlugExporter end # on app startup (e.g. supervisor setup) MetricsPlugExporter.setup() # in your plugs pipeline plug MetricsPlugExporter ``` **Do not add to Phoenix Router pipelines!** You will be getting 404! ``` Note that router pipelines are only invoked after a route is found. No plug is invoked in case no matches were found. ``` ### Metrics Also maintains telemetry metrics: - telemetry_scrape_duration_seconds - telemetry_scrape_size_bytes Do not forget to call `setup/0` before using plug, for example on application start! ### Configuration Plugs exporter can be configured via PlugsExporter key of `:prometheus` app env. Default configuration: ```elixir config :prometheus, MetricsPlugExporter, # (you should replace this with the name of your plug) path: "/metrics", format: :text, registry: :default, auth: false ``` Export endpoint can be optionally secured using HTTP Basic Authentication: ```elixir auth: {:basic, "username", "password"} ``` """ require Logger use Prometheus.Metric use Prometheus.Config, [path: "/metrics", format: :text, registry: :default, auth: false] ## TODO: support multiple endpoints [for example separate endpoint for each registry] ## endpoints: [[registry: :qwe, ## path: "/metrics1"], ## [registry: :default, ## path: "/metrics", ## format: :protobuf]] defmacro __using__(_opts) do module_name = __CALLER__.module registry = Config.registry(module_name) path = Plug.Router.Utils.split(Config.path(module_name)) format = normalize_format(Config.format(module_name)) auth = Config.auth(module_name) content_type = format.content_type labels = [registry, format.content_type] quote do @behaviour Plug import Plug.Conn use Prometheus.Metric def setup() do Summary.declare([name: :telemetry_scrape_duration_seconds, help: "Scrape duration", labels: ["registry", "content_type"], registry: unquote(registry)]) Summary.declare([name: :telemetry_scrape_size_bytes, help: "Scrape size, uncompressed", labels: ["registry", "content_type"], registry: unquote(registry)]) end def init(_opts) do end def call(conn, _opts) do case conn.path_info do unquote(path) -> unquote(handle_auth(auth, content_type)) _ -> conn end end defp scrape_data do scrape = Summary.observe_duration( [registry: unquote(registry), name: :telemetry_scrape_duration_seconds, labels: unquote(labels)], fn () -> unquote(format).format(unquote(registry)) end) Summary.observe( [registry: unquote(registry), name: :telemetry_scrape_size_bytes, labels: unquote(labels)], :erlang.iolist_size(scrape)) scrape end unquote( case auth do {:basic, _, _} -> quote do ## borrowed from https://github.com/CultivateHQ/basic_auth ## The MIT License (MIT) ## Copyright (c) 2015 Mark Connell defp valid_basic_credentials?(["Basic " <> encoded_string], username, password) do Base.decode64!(encoded_string) == "#{username}:#{password}" end defp valid_basic_credentials?(_, _, _) do false end ## end of basic_auth code end _ -> :ok end) end end defp handle_auth(auth, content_type) do case auth do false -> send_metrics(content_type) {:basic, username, password} -> quote do if valid_basic_credentials?(Plug.Conn.get_req_header(conn, "authorization"), unquote(username), unquote(password)) do unquote(send_metrics(content_type)) else unquote(send_unauthorized("metrics")) end end end end defp send_metrics(content_type) do quote do scrape = scrape_data() conn |> put_resp_content_type(unquote(content_type), nil) |> send_resp(200, scrape) |> halt end end defp send_unauthorized(realm) do quote do Plug.Conn.put_resp_header(conn, "www-authenticate", "Basic realm=\"#{unquote(realm)}\"") |> Plug.Conn.send_resp(401, "401 Unauthorized") |> Plug.Conn.halt end end defp normalize_format(:text), do: :prometheus_text_format defp normalize_format(:protobuf), do: :prometheus_protobuf_format defp normalize_format(Prometheus.Format.Text), do: :prometheus_text_format defp normalize_format(Prometheus.Format.Protobuf), do: :prometheus_protobuf_format defp normalize_format(format), do: format end