-module(prometheus_http). -export([reply/1, setup/0]). -define(SCRAPE_DURATION, telemetry_scrape_duration_seconds). -define(SCRAPE_SIZE, telemetry_scrape_size_bytes). -define(SCRAPE_ENCODED_SIZE, telemetry_scrape_encoded_size_bytes). %% @doc %% Render metrics %% @end reply(#{path := Path, headers := Headers, registry := Registry, standalone := Standalone}) -> case prometheus_http_config:valid_path_and_registry(Path, Registry) of {true, RealRegistry} -> if_authorized(Path, Headers, fun () -> format_metrics(Headers, RealRegistry) end); {registry_conflict, _ReqR, _ConfR} -> {409, [], <<>>}; {registry_not_found, _ReqR} -> {404, [], <<>>}; false -> maybe_render_index(Standalone, Path, Headers) end. %% @doc %% Initializes telemetry metrics.
%% *NOTE:* If you plug `prometheus_httpd' in your existing httpd instance, %% you have to call this function manually. %% @end setup() -> TelemetryRegistry = prometheus_http_config:telemetry_registry(), ScrapeDuration = [{name, ?SCRAPE_DURATION}, {help, "Scrape duration"}, {labels, ["registry", "content_type"]}, {registry, TelemetryRegistry}], ScrapeSize = [{name, ?SCRAPE_SIZE}, {help, "Scrape size, not encoded"}, {labels, ["registry", "content_type"]}, {registry, TelemetryRegistry}], ScrapeEncodedSize = [{name, ?SCRAPE_ENCODED_SIZE}, {help, "Scrape size, encoded"}, {labels, ["registry", "content_type", "encoding"]}, {registry, TelemetryRegistry}], prometheus_summary:declare(ScrapeDuration), prometheus_summary:declare(ScrapeSize), prometheus_summary:declare(ScrapeEncodedSize). %% =================================================================== %% Private Parts %% =================================================================== format_metrics(Headers, Registry) -> Accept = Headers("accept", "text/plain"), AcceptEncoding = Headers("accept-encoding", undefined), format_metrics(Accept, AcceptEncoding, Registry). maybe_render_index(Standalone, Path, Headers) -> case Standalone of true -> case Path of "/favicon.ico" -> %% https://www.iconfinder.com/icons/85652/fire_torch_icon#size=30 %% http://www.iconbeast.com/ {200, [{"content-type", "image/png"}], base64:decode("iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAABaklEQ" "VRIS+2UsU0EQQxF3+UElAASBUAHUAACKgACcqjg6AByAqACOFEAJU" "ABBHRAQg760liaG+2u7QF0CROd9sb/+dsez+g7G8AzcAE89kjMeoK" "AbeClxF6XBFJSveAH4LAipXXSAcAVcN7YS+tkA26Bk4GaSmcXOIiW" "PQMegyoP6Vj5d4BXr+FR8CUwnxC7qyqh36e/Bf4A1j2xzLBFHX8NQ" "N/LN73p9ri67oWi2IIFVS/VVw3Vn4G1pWqAemh91dDVR0ltem2JOl" "Y5tamsz3VcO+1HkTUaBcuA1qScC17HqRL6rmOV8AwvCbiXC1QOF6X" "UitFCOS6Lw32/Bsk4jiQWvhMFWymjwnvexSjY00n/nwFHXbtulWUG" "/ASsOdY+gf2I/QzYpndK976a9kl+BiyhG2BrRPENOIu4zZbaNIech" "53+9B23gxYaqLoa2VJb7MrASsDgabe9PW5d/4NDL6p3uFba45CzsU" "vfrgozHx5iraMAAAAASUVORK5CYII=")}; _ -> MetricsPath = prometheus_http_config:path(), if_authorized(Path, Headers, fun () -> {200, [], prepare_index(MetricsPath)} end) end; false -> false end. if_authorized(URI, Headers, Fun) -> case prometheus_http_config:authorization() of {invalid_authorize, _} -> {500, [], <<>>}; Auth -> case Auth(#{uri => URI, headers => Headers}) of true -> Fun(); false -> {403, [], <<>>} end end. prepare_index(Path) -> FileName = filename:join([code:priv_dir(prometheus_httpd), "index.html"]), {ok, Content} = file:read_file(FileName), re:replace(Content, "M_E_T_R_I_C_S", Path, [global, {return, list}]). format_metrics(Accept, AcceptEncoding, Registry) -> case negotiate_format(Accept) of undefined -> {406, [], <<>>}; Format -> {ContentType, Scrape} = render_format(Format, Registry), case negotiate_encoding(AcceptEncoding) of undefined -> {406, [], <<>>}; Encoding -> encode_format(ContentType, binary_to_list(Encoding), Scrape, Registry) end end. negotiate_format(Accept) -> case prometheus_http_config:format() of auto -> Alternatives = prometheus_http_config:allowed_formats(), accept_header:negotiate(Accept, Alternatives); Format0 -> Format0 end. negotiate_encoding(AcceptEncoding) -> %% curl and other tools do not send Accept-Encoding field. %% RFC says it means we can render any encoding. %% But people want to see something meaningful in console! accept_encoding_header:negotiate(AcceptEncoding, [<<"identity">>, <<"gzip">>, <<"deflate">>]). render_format(Format, Registry) -> ContentType = Format:content_type(), TelemetryRegistry = prometheus_http_config:telemetry_registry(), Scrape = prometheus_summary:observe_duration( TelemetryRegistry, ?SCRAPE_DURATION, [Registry, ContentType], fun () -> Format:format(Registry) end), prometheus_summary:observe(TelemetryRegistry, ?SCRAPE_SIZE, [Registry, ContentType], iolist_size(Scrape)), {ContentType, Scrape}. encode_format(ContentType, Encoding, Scrape, Registry) -> Encoded = encode_format_(Encoding, Scrape), TelemetryRegistry = prometheus_http_config:telemetry_registry(), prometheus_summary:observe(TelemetryRegistry, ?SCRAPE_ENCODED_SIZE, [Registry, ContentType, Encoding], iolist_size(Encoded)), {200, [{content_type, binary_to_list(ContentType)}, {content_encoding, Encoding}], Encoded}. encode_format_("gzip", Scrape) -> zlib:gzip(Scrape); encode_format_("deflate", Scrape) -> ZStream = zlib:open(), zlib:deflateInit(ZStream), try zlib:deflate(ZStream, Scrape, finish) after zlib:deflateEnd(ZStream) end; encode_format_("identity", Scrape) -> Scrape.