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: :auto, ## or :protobuf, or :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: :auto, 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)) auth = Config.auth(module_name) format = normalize_format(Config.format(module_name)) 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)) _ -> conn end end defp scrape_data(conn) do {content_type, format} = negotiate(conn) labels = [unquote(registry), content_type] scrape = Summary.observe_duration( [registry: unquote(registry), name: :telemetry_scrape_duration_seconds, labels: labels], fn () -> format.format(unquote(registry)) end) Summary.observe( [registry: unquote(registry), name: :telemetry_scrape_size_bytes, labels: labels], :erlang.iolist_size(scrape)) {content_type, scrape} end defp negotiate(conn) do unquote(if format == :auto do quote do try do [accept] = Plug.Conn.get_req_header(conn, "accept") format = :accept_header.negotiate(accept, [{:prometheus_text_format.content_type, :prometheus_text_format}, {:prometheus_protobuf_format.content_type, :prometheus_protobuf_format}]) {format.content_type, format} rescue ErlangError -> {unquote(:prometheus_text_format.content_type), :prometheus_text_format} end end else {format.content_type, format} end) 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) do case auth do false -> send_metrics() {:basic, username, password} -> quote do if valid_basic_credentials?(Plug.Conn.get_req_header(conn, "authorization"), unquote(username), unquote(password)) do unquote(send_metrics()) else unquote(send_unauthorized("metrics")) end end end end defp send_metrics() do quote do {content_type, scrape} = scrape_data(conn) conn |> put_resp_content_type(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(:auto), do: :auto 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